Navigation

6. JSDoc과 주석 규약

함수 본문 주석에는 의도와 긴 절차의 단계를 적고 코드 내용을 반복하지 않습니다. 선언 위 문서 주석은 어디에 붙일지, 어떤 형식으로 쓸지, 태그를 붙일지를 규칙마다 따로 정합니다. 본문은 목적과 제약만 짧은 한국어 명사형으로 적고, 규칙이 허용한 예외에는 확인할 수 있는 이유를 남깁니다.

6.1 본문 안 설명은 //로 적고 의도와 단계를 남깁니다

코드를 옮겨 적은 주석은 막고 읽는 데 필요한 설명은 남깁니다.

함수 본문에서 코드의 의도나 절차 단계를 설명할 때는 블록 주석 대신 //를 씁니다. 도메인 규칙, 예외 방어, 외부 API 제약, 부수효과 순서, 긴 절차의 단계 구분에 사용합니다.

위치주석 형태
코드 한 줄, 절차 단계//. 긴 흐름을 한 함수에 유지할 때도 단계 구분을 남깁니다
주석의 위//, /*, /** 모두 빈 줄 하나를 둡니다. 코드 줄 바로 아래에 붙이지 않습니다. 블록, 객체, 배열, JSX 요소의 첫 줄과 주석이 이어지는 줄은 예외
규칙 6.2가 정한 선언규칙 6.4에 따른 문서 블록
그 밖의 지역 선언별도 주석을 달지 않습니다. 필요한 줄의 의도만 //로 적습니다
JSX 자식//를 쓸 수 없으므로 프레임워크 규칙을 따릅니다

내용은 규칙 6.3, 허용된 예외의 이유는 규칙 6.5가 정합니다.

지역 선언에 코드를 옮겨 적은 주석을 답니다.

const toMatchedProducts = (products: Product[], keyword: string) => {
	// keyword를 소문자로 변환
	const lowerKeyword = keyword.trim().toLowerCase();

	return products.filter((product) => product.name.toLowerCase().includes(lowerKeyword));
};

선언 이름이 이미 드러내는 내용만 적은 주석은 지웁니다.

const toMatchedProducts = (products: Product[], keyword: string) => {
	const lowerKeyword = keyword.trim().toLowerCase();

	return products.filter((product) => product.name.toLowerCase().includes(lowerKeyword));
};

지켜야 할 순서와 제약을 주석 없이 코드에만 둡니다.

const submitProductDraft = async (draft: ProductDraft) => {
	if (!draft.name.trim()) {
		return;
	}

	const uploadedAttachments = await uploadAttachments(draft.attachments);
	const savedProduct = await saveProduct({name: draft.name, attachments: uploadedAttachments});

	await queryClient.invalidateQueries({queryKey: ["products"]});

	return savedProduct;
};

//로 제약과 단계를 적습니다.

const submitProductDraft = async (draft: ProductDraft) => {
	// SDK가 빈 문자열을 거부. trim 후 값이 없으면 호출 안 함
	if (!draft.name.trim()) {
		return;
	}

	// 1. 첨부 먼저 업로드. 본문 저장에 참조 ID 필요
	const uploadedAttachments = await uploadAttachments(draft.attachments);

	// 2. 본문 저장
	const savedProduct = await saveProduct({name: draft.name, attachments: uploadedAttachments});

	// 3. 목록 캐시 무효화는 저장 완료 후. 순서가 바뀌면 옛 목록이 다시 채워짐
	await queryClient.invalidateQueries({queryKey: ["products"]});

	return savedProduct;
};

함께 보기

6.2 경계가 되는 선언에는 JSDoc 헤더를 붙입니다

구현을 읽기 전에 중요한 경계를 찾고 설명할 수 있습니다.

중요한 선언에는 구현을 읽기 전에 역할과 경계를 알 수 있도록 헤더 문서 주석을 씁니다. 빈 본문이나 영문 라벨만으로는 요구를 충족하지 못하며 실제 한국어 설명이 필요합니다.

헤더 문서 주석 대상조건
이름 붙인 쿼리, 뮤테이션, 원격 함수, 커스텀 훅, 스토어모두 작성합니다
포매터표시 문자열을 만들 때 작성합니다
핸들러, 이펙트본문에 분기, await, 두 개 이상의 동작 중 하나라도 있으면 작성합니다
보조 함수재사용하거나 내보내면 작성합니다
커스텀 type, interface내보내기 여부와 무관하게 규칙 1.4를 따릅니다

주요 선언에 헤더 설명이 없습니다.

export const toSortedUserIds = (userIds: string[]): string[] => {
	return uniq(userIds).toSorted();
};

const responseProductList = useProductList();

여러 줄 블록에 설명만 적습니다.

/**
 * 요청 순서 고정. 선택 목록의 중복 ID 제거 후 오름차순 정렬
 */
export const toSortedUserIds = (userIds: string[]): string[] => {
	return uniq(userIds).toSorted();
};

/**
 * 상품 목록 조회. 로딩과 오류는 이 응답 객체로만 판단
 */
const responseProductList = useProductList();

함께 보기

6.3 주석에는 목적과 제약만 짧은 한국어 명사형으로 적습니다

몇 해 뒤에 코드를 여는 사람도 주석을 한눈에 읽고 고칠 수 있습니다.

주석에는 이름과 시그니처에 없는 목적, 제약, 부수효과만 스펙 문서의 한국어로 적습니다. 적을 정보가 없으면 주석을 지웁니다.

문장

항목기준
끝맺음한자어 명사로 끝냅니다(조회, 병합, 미포함, 필요, 불가, 고정). 용언을 명사형으로 바꾼 읽음, 둠, 넘김, 합쳐짐은 쓰지 않습니다. 상태를 말하는 없음, 있음, 않음, 열림, 닫힘은 허용합니다. 줄 끝에는 마침표를 찍지 않습니다
한 줄사실 하나만 적습니다. 결론이 먼저이고 조건과 결과는 쉼표 뒤에 둡니다(오류 경계 미포함, 그래프 오류 시 뒤로가기 유지)
조사주제 조사는 항목에 하나까지(패턴 ID는 영역별 URL 직접 조회), 목적격 조사는 생략합니다. 쓸 때는 영어 낱말 뒤에도 붙여 씁니다(MUI가)
접속그래서, 때문에, 위해, ~라서, 덕분을 쓰지 않습니다. 인과는 쉼표 뒤 결과로 적습니다
비유몫, 껍데기, 무너짐, 길이 남음, 갈아 끼움, 흔들림 같은 비유와 의인화를 쓰지 않습니다. 담당, 래퍼, 실패, 복귀 경로, 교체, 변동으로 적습니다
군더더기꾸밈말, 경위를 뺍니다. 예전 동작과 바뀐 과정은 커밋 메시지에 둡니다
나열쉼표로 잇습니다. 가운뎃점, 대시, 화살표는 쓰지 않습니다

낱말

낱말주석 표기
우리말로 통하는 일반 낱말우리말이나 흔한 음차. request는 요청, product는 상품, back 버튼은 뒤로가기 또는 백 버튼, reset은 리셋
우리말이 없거나 어색한 기술 용어한글 음차. provider는 프로바이더, wrapper는 래퍼, render는 렌더
코드 이름, 제품 이름, 약어원문 그대로. QueryClientProvider, MUI, API

자주 바꿔 쓰는 낱말입니다.

용언 명사형, 비유스펙 낱말
읽음, 가져옴조회, 수신
받음, 넘김, 줌수신, 전달, 반환
씀, 둠, 놓음사용, 배치, 보관
그림, 보임, 표시됨렌더, 표시
붙음, 뺌, 비움, 채움추가, 제외, 초기화, 입력
나눔, 묶음, 합쳐짐, 맞춤분리, 병합, 일치
남음, 바뀜, 겹침유지, 변경, 중첩
정함, 고름, 잡음, 좁힘결정, 선택, 계산, 제한
몫, 자리담당, 구간

상자

문서 주석은 제목 한 줄이 기본입니다. 본문이 있으면 빈 줄 뒤에 두 칸 들여쓴 목록 하나로 씁니다. 평문 줄은 쓰지 않습니다.

자리기준
제목선언이 무엇인지 명사구 한 줄. 짧은 보충 하나는 마침표 뒤 같은 줄
본문빈 줄 하나 뒤, 줄마다 -로 시작하는 목록. 사실 하나면 항목 하나
항목사실 하나. 결론이 먼저이고 이유는 마침표 뒤
{@link}마지막 줄, 목록 밖
조회, 뮤테이션 바인딩제목은 <응답> 조회, <결과> 가공 또는 <동작> 요청, <성공 뒤 처리>. 마지막 줄에 부르는 엔드포인트를 {@link /api/...}로 적습니다. 여러 조회를 합치면 엔드포인트마다 한 줄
//사실 하나. 둘이면 줄을 나누거나 헤더 블록의 목록으로 올립니다

항목 앞에는 종류를 알리는 태그 셋만 씁니다. 그 밖의 사실은 태그 없이 적습니다.

태그뜻
제약:바깥(API, 라이브러리, 명세, 기획)이 정해 코드가 따르는 조건
예외:규칙을 비껴간 자리와 확인할 수 있는 근거. 규칙 6.5의 자리
임시:없어질 사실과 지울 조건. 예) 서버 작업이 끝나면 삭제

괄호 꼬리는 출처 전용입니다. 누가 정했는지와 그 날짜를 적습니다((백엔드 답변 10-14), (기획 10-07), (피그마 10-01), (사용자 10-13)). 날짜만 있는 (10-14)는 출처가 아닙니다.

내용과 태그

내용, 태그판정
선언 이름만 번역하거나 코드를 한 줄씩 옮긴 설명쓰지 않습니다
본문 전체가 영어인 주석쓰지 않습니다. 헤더가 영어뿐이면 필드 주석이 한국어여도 요구를 충족하지 못합니다
설명 없이 @param, @returns만 나열쓰지 않습니다
@api, @helper, @field이름과 문법이 드러내는 역할을 태그로 반복하지 않습니다
@schema 같은 비표준 태그새로 만들지 않습니다
@summary헤더 첫 줄과 겹치므로 쓰지 않습니다
@deprecated, @example, @param, @returns 등 TSDoc 태그필요할 때만 씁니다

영문이거나 선언 이름을 옮겨 적기만 합니다.

util/array/to-sorted-product-refs.tsts
/**
 * This function sorts product refs and returns the result.
 */
export const toSortedProductRefs = (refs: ProductRef[]): ProductRef[] => {
	return sortBy(uniq(refs), [(ref) => ref.id]);
};
util/array/to-products-newest-first.tsts
/**
 * 상품을 정렬하는 함수
 */
export const toProductsNewestFirst = (products: Product[]): Product[] => {
	return orderBy(products, ["updatedAt", "id"], ["desc", "asc"]);
};
page/products/_pg-product-tree-section.tsxts
/**
 * route-local product tree props
 */
export interface PgProductTreeSectionProps {
	categoryNodes: ProductCategoryNode[];
}

이름에 없는 정보를 스펙 낱말로 더합니다.

util/array/to-sorted-product-refs.tsts
/**
 * 검토 목록 순서 고정. 중복 참조 제거 후 식별자순 정렬
 */
export const toSortedProductRefs = (refs: ProductRef[]): ProductRef[] => {
	return sortBy(uniq(refs), [(ref) => ref.id]);
};
util/array/to-products-newest-first.tsts
/**
 * 저장 응답 순서 대신 최신순 재정렬
 *
 *   - 제약: 동일 updatedAt 항목 순서는 서버 미보장
 *   - 응답 순서 사용 시 새로고침마다 목록 순서 변동
 */
export const toProductsNewestFirst = (products: Product[]): Product[] => {
	return orderBy(products, ["updatedAt", "id"], ["desc", "asc"]);
};
page/products/_pg-product-tree-section.tsxts
/**
 * 상품 트리 입력 계약
 */
export interface PgProductTreeSectionProps {
	/**
	 * 사이드바 표시용 분류 노드 목록
	 */
	categoryNodes: ProductCategoryNode[];
}

긴 동사형 문장에 경위, 가운뎃점, 대시, 영어 낱말을 섞습니다.

app/query-client.tsts
/**
 * Host 앱/단독 실행/테스트 gallery 가 모두 같은 query client 를 쓰도록 여기 한 곳에서 만들어 둔다 — 예전에는 화면마다 따로 만들어서 retry 횟수가 서로 달랐다.
 */
export const queryClient = new QueryClient({defaultOptions: {queries: {retry: query_retry_count}}});

결론을 제목에 쓰고 조건과 결과는 목록 항목으로 둡니다.

app/query-client.tsts
/**
 * 호스트 앱, 단독 실행, 테스트 공용 쿼리 클라이언트
 *
 *   - 재시도 횟수는 이 한 곳에서만 설정
 */
export const queryClient = new QueryClient({defaultOptions: {queries: {retry: query_retry_count}}});

제약 셋을 한 문장에 몰아 씁니다.

service/product/to-product-save-request.tsts
/**
 * 상품 저장 요청. 서버가 빈 문자열과 앞뒤 공백을 거부하고, 가격은 원 단위 정수만 받으며, 태그는 한도를 넘기면 저장 자체가 실패하기 때문에 여기서 미리 정리해서 보낸다.
 */
export const toProductSaveRequest = (draft: ProductDraft): ProductSaveRequest => {
	return {
		name: draft.name.trim(),
		price: Math.round(draft.price),
		tags: draft.tags.slice(0, product_tag_limit),
	};
};

요약 한 줄 뒤에 제약을 항목 하나씩 들여쓴 목록으로 나눕니다.

service/product/to-product-save-request.tsts
/**
 * 상품 저장 요청. 서버 검증 기준 사전 정리
 *
 *   - 제약: name 앞뒤 공백 불가
 *   - 제약: price 원 단위 정수만 허용
 *   - 제약: tags 한도 초과 시 저장 실패
 */
export const toProductSaveRequest = (draft: ProductDraft): ProductSaveRequest => {
	return {
		name: draft.name.trim(),
		price: Math.round(draft.price),
		tags: draft.tags.slice(0, product_tag_limit),
	};
};

역할 태그로 선언의 성격을 다시 적습니다.

/**
 * @api 상품 목록. 조회 실패는 호출부에서 처리
 */
export const fetchProductList = async (): Promise<Product[]> => {
	return await client.get("/products");
};

태그를 지우고 헤더 첫 줄에 선언이 하는 일을 적습니다.

/**
 * 상품 목록. 조회 실패 처리는 호출부 담당
 */
export const fetchProductList = async (): Promise<Product[]> => {
	return await client.get("/products");
};

바뀐 경위와 날짜만 있는 괄호를 적습니다.

service/order/to-order-save-request.tsts
/**
 * 주문 저장 요청
 *
 * 10-07 서버가 수량을 정수로만 받게 되어 반올림 추가
 * 배송 메모는 서버가 200자에서 자름 (10-14)
 */
export const toOrderSaveRequest = (draft: OrderDraft): OrderSaveRequest => {
	return {
		quantity: Math.round(draft.quantity),
		memo: draft.memo.slice(0, order_memo_limit),
	};
};

지금 참인 사실에 출처를 붙이고 없어질 사실은 임시:로 표시합니다.

service/order/to-order-save-request.tsts
/**
 * 주문 저장 요청. 서버 검증 기준 사전 정리
 *
 *   - 제약: 수량은 정수만 허용 (백엔드 답변 10-07)
 *   - 임시: 배송 메모 200자 절단, 서버 한도 확장 시 삭제 (기획 10-14)
 */
export const toOrderSaveRequest = (draft: OrderDraft): OrderSaveRequest => {
	return {
		quantity: Math.round(draft.quantity),
		memo: draft.memo.slice(0, order_memo_limit),
	};
};

조회 바인딩 설명이 토막 문장이고 부르는 API가 없습니다.

page/product/pg-product-table.tsxts
/**
 * 상품들을 표에 그릴 형태로
 */
const responseProductListSuspense = useGetProductListSuspense(
	{ query: { page: urlParams.page } },
	{ query: { select: (response: ProductListResponseDTO) => toProductRows(response) } },
);

무엇을 조회해 무엇으로 가공하는지와 엔드포인트를 적습니다.

page/product/pg-product-table.tsxts
/**
 * 상품 목록 조회, 표 행 뷰모델 가공
 * {@link /api/product}
 */
const responseProductListSuspense = useGetProductListSuspense(
	{page: urlParams.page},
	{ query: { select: (response: ProductListResponseDTO) => toProductRows(response) } },
);

6.4 JSDoc은 한 줄로 쓰지 않고 여러 줄 블록으로 씁니다

선언 위 주석 형태가 파일마다 같아 주석을 검색하고 훑어보기 쉬워집니다.

문서 주석은 /**, *, */를 각각 다른 줄에 둔 여러 줄 블록으로 씁니다.

형태, 판단기준
/** 한 줄 */쓰지 않습니다
선언 설명을 //로 작성쓰지 않습니다. 선언 위 //는 규칙 6.5의 예외 이유에 씁니다
문서화할 선언 선택규칙 6.2를 따릅니다
태그 선택규칙 6.3를 따릅니다

한 줄 블록과 //로 선언을 설명합니다.

service/product/fetch-product-list.tsts
/** 상품 목록. 조회 실패는 호출부에서 처리 */
export const fetchProductList = async (): Promise<Product[]> => {
	return await client.get("/products");
};
service/product/save-product.tsts
// 상품 저장 요청. 응답 본문이 없어 성공은 상태 코드로만 확인
export const saveProduct = async (product: Product): Promise<void> => {
	await client.post("/products", product);
};

같은 내용을 여러 줄 블록으로 고정합니다.

service/product/fetch-product-list.tsts
/**
 * 상품 목록. 조회 실패는 호출부에서 처리
 */
export const fetchProductList = async (): Promise<Product[]> => {
	return await client.get("/products");
};
service/product/save-product.tsts
/**
 * 상품 저장 요청. 응답 본문이 없어 성공은 상태 코드로만 확인
 */
export const saveProduct = async (product: Product): Promise<void> => {
	await client.post("/products", product);
};

함께 보기

6.5 컨벤션 예외에는 확인할 수 있는 이유를 적습니다

예외가 취향인지 근거가 있는지 코드에서 바로 드러납니다.

확인할 수 있는 근거

규칙이 허용한 예외에는 다른 사람이 확인할 수 있는 근거를 주석으로 남깁니다. “성능을 위해”, “안전하게”, “필요해서”처럼 확인할 수 없는 말은 예외의 근거가 되지 않습니다.

근거적을 내용
외부 패키지, API 제약어떤 API가 무엇을 요구하는지
측정 결과측정 대상과 수치
제품 명세, 티켓결정이 기록된 위치
상수constant 폴더에 선언된 이름

주석 자리

flowchart LR
	q1{"헤더 문서 주석이<br>있는 선언인가?"} -- 아니요 --> q2{"JSX 자식인가?"} -- 아니요 --> r3("해당 줄 바로 위에 //")
	q1 -- 예 --> r1("헤더 블록의 예외: 항목")
	q2 -- 예 --> r2("프레임워크 규칙이<br>정한 형태")

헤더 블록에서는 예외: 태그를 단 목록 항목으로 적습니다. 어투와 상자 꼴은 규칙 6.3를 따릅니다.

확인할 수 없는 말로 예외를 정당화합니다.

// 성능을 위해 메모이제이션
const columns = useMemo(() => {
	return toTableColumns(responseTableColumnsSuspense.data.columns);
}, [responseTableColumnsSuspense.data.columns]);

외부 패키지의 제약을 가리킵니다.

// columns 참조 유지. MUI Data Grid는 참조가 바뀌면 열 너비, 순서를 잃을 수 있음
const columns = useMemo(() => {
	return toTableColumns(responseTableColumnsSuspense.data.columns);
}, [responseTableColumnsSuspense.data.columns]);

막연한 말이라 무엇을 재서 넣었는지 알 수 없습니다.

// 안전하게 다시 계산하지 않도록
const filteredRows = useMemo(() => {
	return rows.filter((row) => matchRow(row, deferredKeyword));
}, [deferredKeyword, rows]);

측정 결과를 가리킵니다.

// 행 5,000개에서 매 렌더 필터링 120ms 측정. 지연한 검색어에만 재계산
const filteredRows = useMemo(() => {
	return rows.filter((row) => matchRow(row, deferredKeyword));
}, [deferredKeyword, rows]);

함께 보기