3. 함수와 보조 함수 경계
함수 선언 형태와 시그니처를 일관되게 유지합니다. 보조 함수는 재사용되거나 함수 형태가 필수일 때, 또는 렌더 파일 밖으로 요청 조립을 옮길 때 이름을 붙입니다. 보조 함수는 결과가 드러나는 이름을 붙여 정해진 위치에 둡니다. 변수는 재계산을 막거나 판정을 설명할 때만 만듭니다. 파일 안 선언 순서를 정하고, 넓은 스코프에서 let 재할당과 push로 값을 누적하지 않습니다.
3.1 함수는 const 화살표로 선언합니다
함수 선언과 본문 형식을 통일해 변경 범위를 줄이고 선언 순서를 확인하기 쉽게 합니다.
선언과 본문 형식
이름 붙인 함수는 const 화살표로 선언하고, 객체에 담는 함수도 화살표로 씁니다.
본문은 블록으로 열고 값을 반환할 때 return을 적으며, 반환값이 없으면 생략합니다.
| 대상 | 선언, 본문 형식 |
|---|---|
| 이름 붙인 함수 | const name = (…) => { … } |
| 객체에 담긴 함수 | name: (…) => { … }. 메서드 축약형은 쓰지 않습니다 |
| 일회성 인라인 콜백 | rows.map((row) => row.id)처럼 표현식 본문을 허용합니다 |
| 커링의 바깥 화살표 | (id) => (event) => { … }처럼 안쪽 함수만 블록으로 엽니다 |
| 클래스 메서드 | 메서드 문법을 유지하고 화살표 필드로 바꾸지 않습니다 |
| 제너레이터 | function* 문법을 씁니다 |
| 오버로드 | function 선언을 허용합니다. 호출 시그니처 타입을 const에 붙일 수 있으면 그쪽을 씁니다 |
this를 쓰는 함수
this를 쓰는 함수는 기계적으로 치환하지 않습니다.
축약 메서드의 this는 호출 방식에 따라 달라지고, 화살표는 선언된 바깥 스코프의 this를 사용합니다.
외부 콜백이 호출 시점의 this를 요구하면 계약을 유지하고 이유 주석을 남깁니다.
이름 붙인 함수를 function으로 선언합니다.
/**
* 제목 공백 정리. 검색어 비교에서 공백 차이 무시
*/
export function toTrimmedTitle(rawTitle: string): string {
return rawTitle.trim().replace(/\s+/g, " ");
}
같은 함수를 const 화살표와 블록 본문으로 선언합니다.
/**
* 제목 공백 정리. 검색어 비교에서 공백 차이 무시
*/
export const toTrimmedTitle = (rawTitle: string): string => {
return rawTitle.trim().replace(/\s+/g, " ");
};
객체 속성의 함수를 메서드 축약형으로 씁니다.
export const cellFormatterByValueType = {
text(value: string): string {
return value.trim();
},
} as const;
객체 속성의 함수는 화살표, 인라인 콜백은 한 줄로 씁니다.
/**
* 값 종류별 표 셀 표시 함수. 문자열은 앞뒤 공백 제거 후 표시
*/
export const cellFormatterByValueType = {
text: (value: string): string => {
return value.trim();
},
} as const;
/**
* 상품 식별자 목록
*/
export const toProductIds = (products: Product[]): string[] => {
return products.map((product) => product.id);
};
클래스 메서드와 제너레이터는 그대로 둡니다.
export class ProductCursor {
private buffer: Product[] = [];
*pages(): Generator<Product[]> {
yield this.buffer;
}
reset(): void {
this.buffer = [];
}
}
함께 보기
3.2 시그니처가 복잡해지면 이름 붙인 객체 매개변수로 묶습니다
긴 시그니처를 읽기 쉽게 유지하고 인자 위치를 헷갈리지 않으면서 입력을 늘립니다.
매개변수가 셋을 넘거나 같은 계열 값이 함께 넘어오면 위치 인자를 객체 하나로 묶습니다.
객체 매개변수 타입은 파일 위쪽에 이름을 붙여 선언합니다.
받은 객체는 규칙 4.3에 따라 구조분해하지 않고 체인으로 읽습니다.
뜻이 같은 계약이 이미 있으면 그대로 쓰고, 규칙을 지키려고 *Params나 *Args를 새로 만들지 않습니다.
React 컴포넌트의 프롭스는 프레임워크 규칙이 정합니다.
위치 인자가 넷이라 호출부에서 순서를 외워야 합니다.
const fetchProductPage = (baseUrl: string, page: number, pageSize: number, keyword?: string): Promise<ProductPage> => {
/* … */
};
fetchProductPage(api_base_url, urlParams.page, pagination_default_page_size, undefined);
매개변수를 객체로 묶고 그 타입을 파일 위쪽에 이름 붙여 선언합니다.
/**
* 상품 목록 한 페이지 요청 조건
*/
interface ProductPageRequest {
/**
* 요청 기준 주소
*/
baseUrl: string;
/**
* 1부터 세는 페이지 번호
*/
page: number;
/**
* 한 페이지에 담을 개수
*/
pageSize: number;
/**
* 검색어. 비우면 전체 목록
*/
keyword?: string;
}
const fetchProductPage = (request: ProductPageRequest): Promise<ProductPage> => {
return client.get(`${request.baseUrl}/products`, {params: request});
};
fetchProductPage({baseUrl: api_base_url, page: urlParams.page, pageSize: pagination_default_page_size});
함께 보기
3.3 보조 함수는 재사용되거나 함수 형태가 필수일 때만 이름을 붙입니다
불필요한 함수 분리를 줄여 호출부에서 처리 흐름을 읽을 수 있습니다.
이름을 붙이는 사유
한 곳에서만 쓰는 단계는 호출부에 둡니다. 추출한 함수는 바깥 변수, 훅, 컴포넌트 상태 없이도 뜻이 통해야 합니다.
flowchart LR
q1{"두 자리 이상이<br>부르는가?"} -- 아니요 --> q2{"삼항 하나로<br>담기는가?"} -- 예 --> r3("호출부에 그대로 둠")
q1 -- 예 --> r1("이름을 붙여 추출")
q2 -- 아니요 --> r2("return 함수로 추출")| 허용 사유 | 조건 |
|---|---|
| 실제 재사용 | 변경 후 코드에서 두 자리 이상이 그 함수를 부름. 한 줄 함수여도 해당함 |
| 함수 형태가 필수 | 삼항 하나로 표현할 수 없는 판정, value is T 타입 가드, 재귀 |
추출을 검토할 때
| 추출을 검토하는 이유 | 처리 |
|---|---|
.map() 콜백 하나에서만 쓰는 변환 | 그 콜백에 둡니다 |
| 값이 두 분기로 갈림 | 호출부에서 삼항 하나로 씁니다 |
| 값이 세 분기 이상으로 갈림 | 함수로 추출하고 분기마다 return으로 끝냅니다 |
분기를 줄이려면 추출 전에 규칙 5.4와 규칙 4.8를 먼저 봅니다. 함수 배치는 규칙 3.4, 루트 승격은 규칙 3.6이 정합니다.
한 자리에서만 쓰는 단계를 함수로 떼어 내 흐름이 파일 안에서 흩어집니다.
page/orders/_function/to-order-summary.ts
toOrderSummary 내보낸 함수. 본문은 세 줄이고 나머지는 아래 함수로 갔다
toComparisonRows toOrderSummary만 부름
toStatusGroups toOrderSummary만 부름
toStockCard toOrderSummary만 부름
formatAmount toComparisonRows와 toStatusGroups가 부름
/**
* 주문 요약 영역의 표시 데이터. 재고 카드는 재고 수량이 있을 때만
*/
export const toOrderSummary = (params: ToOrderSummaryParams): OrderSummaryContent => {
return {
metrics: toComparisonRows(params),
statusGroups: toStatusGroups(params),
stockCount: toStockCard(params),
};
};한 번 쓰는 단계는 호출부에 두고 재사용하는 계산은 함수로 추출합니다.
page/orders/_function/to-order-summary/
├── to-order-summary.ts 본문 안에 // 1. 비교 행 // 2. 상태 그룹 // 3. 재고 카드
└── _format-amount.ts 비교 행과 상태 그룹 두 자리가 부름
/**
* 주문 요약 영역의 표시 데이터. 재고 카드는 재고 수량이 있을 때만
*/
export const toOrderSummary = (params: ToOrderSummaryParams): OrderSummaryContent => {
// 1. 고른 기간 기준으로 갱신되는 비교 수치 행
const metrics = [
{id: "orderAmount", label: "주문 금액", value: formatAmount(params.orderSummary.orderAmount)},
{id: "orderCount", label: "주문 건수", value: params.orderSummary.orderCount},
];
// 2. 상품 상태 그룹. 설명이 비면 그룹 제목만
const statusGroups = [
{
id: "product-status",
title: "상품 상태",
description: params.orderSummary.statusDescription,
total: formatAmount(params.orderSummary.totalAmount),
rows: metrics,
},
];
// 3. 재고 카드. 재고 수량이 있을 때만
return {metrics, statusGroups, stockCount: params.stockCount};
};한 번만 쓰는 한 줄 계산을 파일로 떼어 내 호출부가 가져옵니다.
// (previous + 1) % pageCount 한 줄을 page/profile/_function/get-next-page.ts로 옮김
import {getNextPage} from "@/page/profile/_function/get-next-page";
const handleNextClick = () => {
setPage((previous) => getNextPage(previous, pageCount));
};작은 계산은 쓰는 자리에 그대로 둡니다.
const handleNextClick = () => {
setPage((previous) => (previous + 1) % pageCount);
};서로 다른 파일 둘이 이미 부르는 순수 함수를 뺍니다.
/**
* 프로필 저장 페이로드 조립. 서버가 앞뒤 공백이 붙은 displayName 거부
*/
export const toProfileSaveRequest = (formValues: ProfileFormValues) => {
return {
displayName: formValues.displayName.trim(),
};
};// page/profile/_pg-profile-form.tsx와 page/profile/_pg-profile-drawer.tsx가 함께 호출
import {toProfileSaveRequest} from "@/page/profile/_function/to-profile-save-request";
삼항 하나에 담기지 않는 판정은 호출부가 하나여도 함수로 추출하고 분기마다 return으로 끝냅니다.
/**
* 상태 문자열의 강조 톤. API가 상태를 자유 문자열로 주므로 값 포함 여부로 판정
*/
export const toStatusTone = (status: string): Tone => {
const normalizedStatus = status.trim().toLowerCase();
if (status_positive_values.some((value) => normalizedStatus.includes(value))) {
return "positive";
}
if (status_negative_values.some((value) => normalizedStatus.includes(value))) {
return "negative";
}
return "neutral";
};함께 보기
3.4 보조 함수는 대표 함수 폴더의 _ 파일 하나에 하나씩 둡니다
보조 함수를 개별 파일로 관리하고 폴더로 소유 관계를 드러냅니다.
호출부에 따른 자리
보조 함수에 이름을 붙일지는 규칙 3.3이 정합니다. 이름을 붙였다면 함수마다 파일을 하나 두고, 부르는 대표 함수에 따라 배치합니다.
flowchart LR
q1{"부르는 대표 함수가<br>둘 이상인가?"} -- 예 --> q2{"다른 소유자도<br>부르는가?"} -- 예 --> r3("루트 승격 판단")
q1 -- 아니요 --> r1("대표 폴더의 _ 파일")
q2 -- 아니요 --> r2("_function 바로 아래")| 호출부 | 위치 |
|---|---|
| 대표 함수 하나 | _function/<대표>/<대표>.ts와 같은 폴더의 _<보조>.ts |
| 같은 소유자의 대표 함수 둘 이상 | _function/<보조>.ts. 기존 _ 접두사를 뗍니다 |
| 다른 소유자 | 규칙 3.6로 루트 승격 여부를 판단합니다 |
파일 배치 규범
| 배치 대상 | 규범 |
|---|---|
| 대표 함수 | 내보낸 함수 하나당 파일 하나이며 파일 이름은 함수 이름입니다 |
| 대표만 쓰는 보조 | 대표 파일 아래 비공개 const로 쌓지 않고 같은 이름 폴더의 _ 파일로 둡니다 |
| 보조의 보조 | 같은 폴더의 _ 파일로 둡니다. 하위 폴더를 더 만들지 않습니다 |
| 타입, 상수 | 이 폴더에 두지 않고 프레임워크 규칙이 정한 소유자의 역할 폴더에 둡니다 |
helper.ts, helpers.ts, utils.ts | 여러 보조를 모으는 파일로 만들지 않습니다 |
_ 파일은 같은 폴더에서만 가져옵니다.
대표와 _ 파일 사이의 호출은 대표의 내부이며 재노출 사슬이 아닙니다.
여러 보조를 모은 파일에서 내보낸 함수가 세 단계로 이어집니다.
export const toTrimmedName = (name: string) => {
return name.trim();
};
export const toProductPayload = (values: ProductFormValues) => {
return {name: toTrimmedName(values.name)};
};
export const toProductSaveRequest = (values: ProductFormValues) => {
return {body: toProductPayload(values)};
};소유자 아래 대표 함수 하나에 파일 하나를 둡니다.
/**
* 상품 저장 요청 조립. 서버가 앞뒤 공백이 붙은 name 거부
*/
export const toProductSaveRequest = (values: ProductFormValues) => {
return {body: {name: values.name.trim()}};
};대표 함수 하나만 부르는 보조를 대표 파일 아래 비공개 const로 쌓습니다.
page/product-detail/_function/
├── to-product-overview.ts
│ toProductOverview 내보낸 함수
│ toTrendChart toProductOverview가 차트 둘에서 부름
│ toTrendPoints toTrendChart가 두 자리에서 부름
└── to-product-filter-request.ts
자기만 쓰는 보조가 생긴 대표 함수는 자기 이름 폴더를 갖고 보조는 _ 파일입니다.
page/product-detail/_function/
├── to-product-overview/ 자기만 쓰는 보조가 있어 폴더
│ ├── to-product-overview.ts 대표. 폴더와 같은 이름
│ ├── _to-trend-chart.ts toProductOverview만 부름
│ └── _to-trend-points.ts _to-trend-chart만 부름. 폴더 안은 평평
└── to-product-filter-request.ts 보조가 없어 파일 하나
한 대표만 부르는 보조를 _function 바로 아래에 내보내 둡니다.
page/product-detail/_function/
├── to-product-overview.ts
├── to-product-digest.ts
└── to-trend-chart.ts toProductOverview만 부르는데 소유자의 공개 면에 놓임
두 대표가 부르게 된 뒤에 _function 바로 아래로 올리고 _를 뗍니다.
page/product-detail/_function/
├── to-product-overview/
│ └── to-product-overview.ts
├── to-product-digest.ts toTrendChart를 함께 부르기 시작함
└── to-trend-chart.ts 대표 둘이 불러 공개 면으로 올라옴
함께 보기
- 3.3 보조 함수는 재사용되거나 함수 형태가 필수일 때만 이름을 붙입니다
- 3.6 소유자를 지워도 남는 함수만 루트
util로 올립니다 - 3.5 파일 안 선언은 위에서 아래로 읽히게 놓습니다
3.5 파일 안 선언은 위에서 아래로 읽히게 놓습니다
파일을 열면 내보낸 함수가 먼저 보이고 호출부에서 호출 대상으로 이어집니다.
내보낸 계약과 대표 함수를 먼저 보여 주되, 모듈 초기화 시 필요한 선언 순서를 지킵니다. 선언은 파일 위에서 아래로 다음 순서로 놓습니다.
flowchart TD
s1("import") --> s2("내보낸 계약 타입") --> s3("내보낸 대표 함수") --> s4("모듈을 불러올 때<br>계산하는 선언")함수 본문 참조는 호출 시점에 읽으므로 모듈 초기화가 끝난 뒤 부르면 참조 대상이 아래에 있어도 됩니다. 즉시 계산하는 선언은 자기가 부르는 선언 뒤에 둡니다.
내보낸 계약 타입이 함수 아래에 있어 시그니처를 읽으려면 파일을 끝까지 내려가야 합니다.
export const toSummaryRows = (params: ToSummaryRowsParams): SummaryRow[] => {
return params.response.items.map((item) => ({id: item.id, label: item.name.trim() || item.code}));
};
/**
* 요약 표 행을 만들 때 필요한 입력
*/
export interface ToSummaryRowsParams {
/**
* 요약 조회 응답
*/
response: OrderSummaryResponse;
}내보낸 계약 타입이 먼저, 그 계약을 받는 함수가 바로 아래에 옵니다.
/**
* 요약 표 행을 만들 때 필요한 입력
*/
export interface ToSummaryRowsParams {
/**
* 요약 조회 응답
*/
response: OrderSummaryResponse;
}
/**
* 요약 표가 그리는 행 목록. 이름이 비면 코드로 표시
*/
export const toSummaryRows = (params: ToSummaryRowsParams): SummaryRow[] => {
return params.response.items.map((item) => ({id: item.id, label: item.name.trim() || item.code}));
};모듈을 불러올 때 계산되는 선언이 자기가 부르는 선언보다 위에 있습니다.
const selectedLocaleSupported = isSupportedLocale(selectedLocale);
/**
* 짧고 고정된 지원 로케일 목록 기준 판정
*/
export const isSupportedLocale = (locale: string): boolean => {
return locale_supported_values.includes(locale);
};
모듈을 불러올 때 계산되는 선언은 자기가 부르는 선언 뒤에 둡니다.
/**
* 짧고 고정된 지원 로케일 목록 기준 판정
*/
export const isSupportedLocale = (locale: string): boolean => {
return locale_supported_values.includes(locale);
};
const selectedLocaleSupported = isSupportedLocale(selectedLocale);
3.6 소유자를 지워도 남는 함수만 루트 util로 올립니다
소유자 전용 함수를 구분하고 호출부 수가 달라져도 배치 기준을 유지합니다.
승격 판단
루트 util 승격은 호출부 수가 아니라 소유자를 지워도 계산이 남는지로 판단합니다.
flowchart LR
q1{"소유자를 지워도<br>계산이 남는가?"} -- 예 --> q2{"받는 값의 종류로<br>폴더 이름을 지을 수 있는가?"} -- 예 --> r2("루트 util 의 종류 폴더")
q1 -- 아니요 --> r1("소유자 아래 _function")
q2 -- 아니요 --> r1| 소유자를 지운 결과 | 배치 |
|---|---|
| 함수도 사라짐 | 해당 소유자의 _function에 둡니다. toProfileSaveRequest가 그 예입니다 |
| 함수가 남음 | 한 곳에서만 써도 util/<받는 값의 종류>/에 둡니다. toDisplayDate가 그 예입니다 |
종류 폴더
| 폴더 | 기준 |
|---|---|
date, money, string, array, dom, url | 함수가 받는 값의 타입입니다 |
spread 등 도메인 이름 | Spread처럼 실제 입력 타입이면 허용합니다. 화면, 기능 이름은 쓰지 않습니다 |
| 종류 폴더 안 | 함수마다 파일 하나를 두고, 자기만 쓰는 보조는 자기 이름 폴더의 _ 파일로 둡니다 |
소유자 아래 _function | 종류 폴더 없이 함수 파일을 나열합니다 |
두 소유자가 공유할 때
| 두 소유자가 공유하는 것 | 처리 |
|---|---|
| 표시까지 같음 | 프레임워크의 레이어 규칙에 따라 widget이 소유합니다 |
| 계산만 같음 | 각 소유자가 각각 갖습니다 |
| 프로젝트 전반의 계산임 | 루트 util로 올립니다 |
소유자와 함께 사라질 함수를 루트 util로 올립니다.
// profile은 값의 종류가 아니라 화면 이름. 화면이 없어지면 이 요청도 없음
/**
* 서버가 앞뒤 공백이 붙은 displayName 거부
*/
export const toProfileSaveRequest = (values: ProfileFormValues) => {
return {body: {displayName: values.displayName.trim()}};
};소유자와 함께 사라질 함수는 그 소유자의 _function 폴더에 둡니다.
/**
* 서버가 앞뒤 공백이 붙은 displayName 거부
*/
export const toProfileSaveRequest = (values: ProfileFormValues) => {
return {body: {displayName: values.displayName.trim()}};
};소유자를 지워도 남을 함수를 호출부가 하나라고 소유자 아래 둡니다.
// 날짜 표시는 orders 화면을 지워도 남음. 지금 이 화면만 쓴다는 이유로 여기 있음
/**
* 표시 형식 고정. 사용자 로케일을 따르면 목록 정렬 기준과 어긋남
*/
export const toDisplayDate = (value: string): string => {
return dayjs(value).format(date_format);
};소유자를 지워도 남는 함수는 받는 값의 종류 폴더로 올립니다.
/**
* 표시 형식 고정. 사용자 로케일을 따르면 목록 정렬 기준과 어긋남
*/
export const toDisplayDate = (value: string): string => {
return dayjs(value).format(date_format);
};종류 폴더 아래에도 함수마다 파일 하나를 둡니다.
util/
├── date/
│ ├── to-display-date.ts
│ └── to-display-date.test.ts
└── money/
└── to-signed-amount.ts
/**
* 화면 공통 금액 표시. 소수 두 자리와 부호 고정
*/
export const toSignedAmount = (amount: Amount): string => {
return `${amount.value < 0 ? "-" : "+"}$${Math.abs(amount.value).toFixed(2)}`;
};3.7 넓은 스코프에서 let 재할당과 push로 값을 쌓지 않습니다
분기로 공유 지역 변수를 바꾸지 않아 넓은 스코프의 값 조립이 선언형으로 남습니다.
모듈 최상위나 함수 본문 전체에 걸친 let 재할당, push, 조건부 누적으로 값을 조립하지 않습니다.
if나 for 블록 안에서만 쓰는 누적은 대상이 아닙니다.
flowchart LR
q1{"목록에 조건부<br>항목을 넣는가?"} -- 아니요 --> q2{"값 하나가 두 분기로<br>갈리는가?"} -- 예 --> r2("삼항 하나")
q1 -- 예 --> r1("조건부 스프레드나 filter")
q2 -- 아니요 --> r3("분기마다 return 으로<br>끝나는 함수로 추출")추출 여부는 규칙 3.3, 함수 이름은 규칙 3.9을 따릅니다.
넓은 스코프에서 명령형으로 조립을 쌓습니다.
let visibleTabs = ["overview"];
if (canManageItems) {
visibleTabs.push("items");
}
조건부 스프레드로 한 번에 계산합니다.
const visibleTabs = ["overview", ...(canManageItems ? ["items"] : [])];
삼항 안에 삼항을 넣어 값 하나를 고릅니다.
const statusLabel = order.isCancelled ? "취소" : order.isDueSoon ? "임박" : "진행";
분기가 셋이면 return으로 끝나는 함수로 뺍니다.
/**
* 주문 행의 상태 라벨. 취소가 임박보다 우선
*/
export const toStatusLabel = (order: OrderRow): StatusLabel => {
if (order.isCancelled) {
return "취소";
}
if (order.isDueSoon) {
return "임박";
}
return "진행";
};목록 조립에서 조건이 셋이 되자 삼항을 겹칩니다.
const visibleTabs = canManageItems
? canInviteMembers
? ["overview", "items", "members"]
: ["overview", "items"]
: canInviteMembers
? ["overview", "members"]
: ["overview"];
조건이 셋 이상인 목록은 표로 두고 걸러 냅니다.
const visibleTabs = [
{id: "overview", isVisible: true},
{id: "items", isVisible: canManageItems},
{id: "members", isVisible: canInviteMembers},
]
.filter((tab) => tab.isVisible)
.map((tab) => tab.id);
함께 보기
3.8 변수는 재계산을 막거나 복잡한 판정에 이름을 붙일 때만 만듭니다
사용 횟수보다 계산 비용과 판정의 복잡성을 기준으로 변수 선언 여부를 판단합니다.
지역 변수는 재계산을 막거나 여러 항을 합친 판정에 이름을 붙일 때만 만듭니다. 사용처 수만으로는 만들지 않으며, 아래 사유가 없으면 표현식을 쓰는 자리에 둡니다.
| 변수로 받을 사유 | 확인할 것 |
|---|---|
| 콜백, 반복문으로 옮기면 비용이 반복됨 | 코드에 한 번 적혀도 원소마다 실행됩니다. 반복 조회용 Set도 콜백 밖에 둡니다 |
| 시각, 난수처럼 호출마다 값이 달라짐 | 여러 사용처가 같은 값을 보아야 합니다 |
await, yield, 외부 호출 | 순서나 호출 시점을 바꾸지 않습니다. localStorage.getItem()도 해당합니다 |
훅 호출, useState 반환 | 정해진 호출 위치와 횟수를 유지합니다 |
| 여러 항을 합친 판정 | isEditable처럼 이름이 판정의 결론을 설명해야 합니다 |
| 부정이 겹친 판정 | !row.deletedAt && !row.archivedAt은 isVisible처럼 뜻을 드러내는 이름으로 받습니다 |
단일 비교인 row.dueDate < today는 반복해서 써도 그대로 둡니다.
객체 필드의 별칭은 규칙 4.3,
let 재할당과 push는 규칙 3.7가 정합니다.
두 번 쓴다는 이유만으로 변수로 뺍니다.
const toRowClassNames = (row: OrderRow): string[] => {
const isOverdue = row.dueDate < today;
return [
isOverdue ? "ui_row__root--overdue" : "ui_row__root",
isOverdue ? "ui_row__badge--overdue" : "ui_row__badge",
];
};
항이 하나라 두 번 적어도 그 자리에 그대로 씁니다.
const toRowClassNames = (row: OrderRow): string[] => {
return [
row.dueDate < today ? "ui_row__root--overdue" : "ui_row__root",
row.dueDate < today ? "ui_row__badge--overdue" : "ui_row__badge",
];
};
돌려주기만 할 값을 변수로 뺍니다.
const toNextPage = (page: number): number => {
const nextPage = page + 1;
return nextPage;
};
const toRowLabel = (row: Row): string => {
const rowLabel = `${row.name} (${row.id})`;
return rowLabel;
};
이름을 붙이지 않고 그대로 돌려줍니다.
const toNextPage = (page: number): number => {
return page + 1;
};
const toRowLabel = (row: Row): string => {
return `${row.name} (${row.id})`;
};
세 항을 엮은 판정을 쓰는 자리에 그대로 늘어놓습니다.
const toRowAction = (row: ProductRow): RowAction => {
return row.status === product_status.draft && !row.lockedAt && row.ownerId === session.userId
? row_action.edit
: row_action.view;
};
한 번만 써도 합성 판정이라 변수로 뺍니다.
const toRowAction = (row: ProductRow): RowAction => {
const isEditable = row.status === product_status.draft && !row.lockedAt && row.ownerId === session.userId;
return isEditable ? row_action.edit : row_action.view;
};
콜백 안에 두어 행마다 다시 계산합니다.
const toVisibleRows = (rows: Row[], keyword: string): Row[] => {
return rows.filter((row) => row.name.toLowerCase().includes(keyword.trim().toLowerCase()));
};
콜백 밖으로 빼 행마다 다시 계산하지 않습니다.
const toVisibleRows = (rows: Row[], keyword: string): Row[] => {
// 콜백 안으로 옮기면 행마다 다시 계산
const lowerKeyword = keyword.trim().toLowerCase();
return rows.filter((row) => row.name.toLowerCase().includes(lowerKeyword));
};
변수를 없애느라 저장과 캐시 비우기 순서가 뒤집힙니다.
/**
* 초안 저장 후 목록 캐시 비움
*/
const submitDraft = async (draft: Draft) => {
await queryClient.invalidateQueries({queryKey: ["records"]});
return await saveRecord(draft);
};
외부 호출의 실행 순서를 유지하려고 변수로 뺍니다.
/**
* 초안 저장 후 목록 캐시 비움
*/
const submitDraft = async (draft: Draft) => {
const savedRecord = await saveRecord(draft);
await queryClient.invalidateQueries({queryKey: ["records"]});
return savedRecord;
};
함께 보기
3.9 함수 이름은 결과나 효과가 드러나게 짓습니다
함수 이름으로 반환값이나 효과를 파악할 수 있습니다.
역할별 동사
함수 이름은 호출 뒤 얻는 값이나 효과를 구체적으로 드러냅니다. 입력은 시그니처가 설명하므로 이름에 반복하지 않습니다.
| 반환값의 역할 | 동사 | 예 |
|---|---|---|
| 다른 형태로 변환, 분류 | to | toDetailContent |
| 이미 존재하는 값 | get | getSelectedRow |
| 값 하나 또는 없음 | find | findUserByEmail |
| 서로 다른 입력 둘 이상의 우선순위 선택 | choose | chooseBackSource |
| 같은 개념의 허용 범위, 표현 보정 | normalize | normalizePageSize |
문자열, unknown 검증 후 읽기 | parse | parseSearchParams |
| 사람이 읽는 표시 문자열 | format | formatCandidateDayCount |
| 두 값의 정렬 순서 | compare | compareProductsByPrice |
| 비동기 I/O, 여러 요청 조율 | load, fetch | loadProductExport |
| 참, 거짓 판정 | is, has, can, should | shouldShowSummary |
| 함수의 역할 | 동사 | 예 |
|---|---|---|
| 저장, 삭제 | save, remove | saveProduct |
| 조건 위반 시 예외 | assert | assertLoggedIn |
| 검사 결과 또는 오류 | validate | validateProductForm |
| 도메인 동작 | 실제 업무 동사 | submitOrder, cancelBooking |
소유자 경로가 이미 드러내는 도메인은 빼고, 반환 타입 이름보다 호출부가 쓰는 결과 개념을 적습니다.
요청 계약 자체가 출력이면 toUserSaveRequest처럼 짓습니다.
쓰지 않는 동사
build, create, make, process, manage, do, perform, execute, filter, map, update, resolve는
직접 짓는 이름의 첫 동사로 쓰지 않습니다.
| 모호한 이름 | 결과, 효과를 드러낸 이름 |
|---|---|
filterActiveUsers | 남기는 목록이면 toActiveUsers |
mapProductRows | 출력이 행이면 toProductRows |
updateProduct | 저장이면 saveProduct, 계산이면 toUpdatedProduct |
resolveStatusTone | 분류 결과인 toStatusTone |
대상이 아닌 이름
array.map(...) 같은 표준 메서드 호출과 Promise의 resolve, reject는 대상이 아닙니다.
생성기, 프레임워크, 외부 계약이 정한 이름은 바꾸거나 감싸지 않고, handle, use는 프레임워크 규칙을 따릅니다.
입력, 구현 동작, 막연한 접미사를 이름에 씁니다.
export const buildUserPayload = (formValues: UserFormValues) => { /* … */ };export const mapResponseToModel = (response: UserResponse) => { /* … */ };export const processUserRows = (rows: UserRow[]) => { /* … */ };export const resolveStatusTone = (status: string) => { /* … */ };출력 역할이나 효과를 이름에 씁니다.
/**
* 사용자 저장 요청 조립. 서버가 빈 문자열을 거부하므로 비운 칸은 제외
*/
export const toUserSaveRequest = (formValues: UserFormValues) => { /* … */ };/**
* 응답 한 건을 표 행으로 변환
*/
export const toUserRows = (response: UserResponse) => { /* … */ };/**
* 비활성 사용자를 제외한 목록
*/
export const toActiveUsers = (rows: UserRow[]) => { /* … */ };/**
* 상태 문자열을 강조 톤으로 분류
*/
export const toStatusTone = (status: string) => { /* … */ };저장, 검사 함수는 역할과 판정으로 이름을 짓습니다.
/**
* 관리자 권한 판정. 역할 목록이 비면 조회 전 상태로 보고 false
*/
export const isAdminUser = (user: User) => { /* … */ };/**
* 로그인 상태가 아니면 예외. 화면 이동은 호출부가 결정
*/
export const assertLoggedIn = (session: Session): void => {
if (!session.userId) {
throw new NotLoggedInError();
}
};