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 태그 | 필요할 때만 씁니다 |
영문이거나 선언 이름을 옮겨 적기만 합니다.
/**
* This function sorts product refs and returns the result.
*/
export const toSortedProductRefs = (refs: ProductRef[]): ProductRef[] => {
return sortBy(uniq(refs), [(ref) => ref.id]);
};/**
* 상품을 정렬하는 함수
*/
export const toProductsNewestFirst = (products: Product[]): Product[] => {
return orderBy(products, ["updatedAt", "id"], ["desc", "asc"]);
};/**
* route-local product tree props
*/
export interface PgProductTreeSectionProps {
categoryNodes: ProductCategoryNode[];
}이름에 없는 정보를 스펙 낱말로 더합니다.
/**
* 검토 목록 순서 고정. 중복 참조 제거 후 식별자순 정렬
*/
export const toSortedProductRefs = (refs: ProductRef[]): ProductRef[] => {
return sortBy(uniq(refs), [(ref) => ref.id]);
};/**
* 저장 응답 순서 대신 최신순 재정렬
*
* - 제약: 동일 updatedAt 항목 순서는 서버 미보장
* - 응답 순서 사용 시 새로고침마다 목록 순서 변동
*/
export const toProductsNewestFirst = (products: Product[]): Product[] => {
return orderBy(products, ["updatedAt", "id"], ["desc", "asc"]);
};/**
* 상품 트리 입력 계약
*/
export interface PgProductTreeSectionProps {
/**
* 사이드바 표시용 분류 노드 목록
*/
categoryNodes: ProductCategoryNode[];
}긴 동사형 문장에 경위, 가운뎃점, 대시, 영어 낱말을 섞습니다.
/**
* Host 앱/단독 실행/테스트 gallery 가 모두 같은 query client 를 쓰도록 여기 한 곳에서 만들어 둔다 — 예전에는 화면마다 따로 만들어서 retry 횟수가 서로 달랐다.
*/
export const queryClient = new QueryClient({defaultOptions: {queries: {retry: query_retry_count}}});결론을 제목에 쓰고 조건과 결과는 목록 항목으로 둡니다.
/**
* 호스트 앱, 단독 실행, 테스트 공용 쿼리 클라이언트
*
* - 재시도 횟수는 이 한 곳에서만 설정
*/
export const queryClient = new QueryClient({defaultOptions: {queries: {retry: query_retry_count}}});제약 셋을 한 문장에 몰아 씁니다.
/**
* 상품 저장 요청. 서버가 빈 문자열과 앞뒤 공백을 거부하고, 가격은 원 단위 정수만 받으며, 태그는 한도를 넘기면 저장 자체가 실패하기 때문에 여기서 미리 정리해서 보낸다.
*/
export const toProductSaveRequest = (draft: ProductDraft): ProductSaveRequest => {
return {
name: draft.name.trim(),
price: Math.round(draft.price),
tags: draft.tags.slice(0, product_tag_limit),
};
};요약 한 줄 뒤에 제약을 항목 하나씩 들여쓴 목록으로 나눕니다.
/**
* 상품 저장 요청. 서버 검증 기준 사전 정리
*
* - 제약: 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");
};
바뀐 경위와 날짜만 있는 괄호를 적습니다.
/**
* 주문 저장 요청
*
* 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),
};
};지금 참인 사실에 출처를 붙이고 없어질 사실은 임시:로 표시합니다.
/**
* 주문 저장 요청. 서버 검증 기준 사전 정리
*
* - 제약: 수량은 정수만 허용 (백엔드 답변 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가 없습니다.
/**
* 상품들을 표에 그릴 형태로
*/
const responseProductListSuspense = useGetProductListSuspense(
{ query: { page: urlParams.page } },
{ query: { select: (response: ProductListResponseDTO) => toProductRows(response) } },
);무엇을 조회해 무엇으로 가공하는지와 엔드포인트를 적습니다.
/**
* 상품 목록 조회, 표 행 뷰모델 가공
* {@link /api/product}
*/
const responseProductListSuspense = useGetProductListSuspense(
{page: urlParams.page},
{ query: { select: (response: ProductListResponseDTO) => toProductRows(response) } },
);6.4 JSDoc은 한 줄로 쓰지 않고 여러 줄 블록으로 씁니다
선언 위 주석 형태가 파일마다 같아 주석을 검색하고 훑어보기 쉬워집니다.
문서 주석은 /**, *, */를 각각 다른 줄에 둔 여러 줄 블록으로 씁니다.
| 형태, 판단 | 기준 |
|---|---|
/** 한 줄 */ | 쓰지 않습니다 |
선언 설명을 //로 작성 | 쓰지 않습니다. 선언 위 //는 규칙 6.5의 예외 이유에 씁니다 |
| 문서화할 선언 선택 | 규칙 6.2를 따릅니다 |
| 태그 선택 | 규칙 6.3를 따릅니다 |
한 줄 블록과 //로 선언을 설명합니다.
/** 상품 목록. 조회 실패는 호출부에서 처리 */
export const fetchProductList = async (): Promise<Product[]> => {
return await client.get("/products");
};// 상품 저장 요청. 응답 본문이 없어 성공은 상태 코드로만 확인
export const saveProduct = async (product: Product): Promise<void> => {
await client.post("/products", product);
};같은 내용을 여러 줄 블록으로 고정합니다.
/**
* 상품 목록. 조회 실패는 호출부에서 처리
*/
export const fetchProductList = async (): Promise<Product[]> => {
return await client.get("/products");
};/**
* 상품 저장 요청. 응답 본문이 없어 성공은 상태 코드로만 확인
*/
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]);
함께 보기