-
Notifications
You must be signed in to change notification settings - Fork 4
Code Convention
JongJin Kim edited this page Apr 24, 2026
·
1 revision
| 종류 | 규칙 | 예시 |
|---|---|---|
| React 컴포넌트 | PascalCase |
UserCard.tsx, LoginForm.tsx
|
| Hooks | camelCase + use prefix |
useAuth.ts, useToggle.ts
|
| utils / helpers | camelCase |
formatDate.ts, validateEmail.ts
|
| constants | UPPER_SNAKE_CASE |
MAX_RETRY_COUNT, API_BASE_URL
|
| Zustand store | camelCase + .store.ts
|
user.store.ts, auth.store.ts
|
| 도메인 타입 | camelCase + .types.ts
|
goal.types.ts, goalList.types.ts
|
| Zod schema | camelCase + Schema suffix |
emailSchema, createTodoSchema
|
| Test 파일 | 원본 파일 옆에 위치 |
UserCard.test.tsx, useAuth.test.ts
|
// PascalCase 사용
type User = { ... }
interface TodoItem { ... }// {ComponentName}Props
type UserCardProps = {
name: string;
age: number;
};// {Entity}Response / {Entity}ListResponse
type UserResponse = {
id: number;
name: string;
};
type UserListResponse = {
users: User[];
nextCursor?: string;
};// Create{Entity}Request / Update{Entity}Request
type CreateTodoRequest = {
title: string;
dueDate?: string;
};
type UpdateTodoRequest = {
title?: string;
completed?: boolean;
};// 1. imports
// 2. type 정의
// 3. 컴포넌트 함수
// 4. export default
type UserCardProps = {
name: string;
age: number;
};
export default function UserCard({ name, age }: UserCardProps) {
return <div className="...">...</div>;
}✅ 추가해야 하는 경우:
- onClick, onChange 등 이벤트 핸들러 사용
- useState, useEffect, useRef 등 React 훅 사용
- 브라우저 API (window, document, localStorage) 접근
❌ 추가하면 안 되는 경우:
- 데이터만 fetch해서 렌더링하는 서버 컴포넌트
- props를 받아 정적으로 렌더링하는 순수 컴포넌트
각 slice/segment는 index.ts로 public interface를 명시한다.
// features/create-todo/index.ts
export { CreateTodoForm } from "./ui/CreateTodoForm";
export { useCreateTodo } from "./hooks/useCreateTodo";외부에서는 내부 경로 직접 import 금지:
// ❌
import { CreateTodoForm } from "@/features/create-todo/ui/CreateTodoForm";
// ✅
import { CreateTodoForm } from "@/features/create-todo";src/app/globals.css의 @theme 토큰을 항상 사용한다.
// ❌ arbitrary value 금지
<p className="text-[14px] text-[#111827]">
// ✅ 정의된 토큰 사용
<p className="typography-label-1 text-label-normal">typography-{scale} utility class 사용:
<h1 className="typography-title-2">제목</h1>
<p className="typography-body-2">본문</p>
<span className="typography-caption-1">캡션</span>| 클래스 | 크기 | 용도 |
|---|---|---|
typography-display-1 |
56px | 최대 타이틀 |
typography-title-2 |
28px | 섹션 타이틀 |
typography-heading-2 |
20px | 카드 헤딩 |
typography-body-1 |
18px | 주요 본문 |
typography-body-2 |
16px | 일반 본문 |
typography-label-1 |
14px | 라벨, 버튼 |
typography-caption-1 |
12px | 부가 정보 |
// 텍스트
text - label - normal; // 기본 텍스트 (#111827)
text - label - alternative; // 보조 텍스트 (40% opacity)
text - inverse - normal; // 반전 텍스트 (#ffffff)
// 배경
bg - background - normal;
bg - background - normal - alternative;
bg - background - elevated - normal;
// 브랜드
bg - blue - 800; // Primary (#6c63ff)
bg - green - 800; // Secondary (#2ec4b6)<div className="custom-scroll overflow-y-auto">...</div>// camelCase + Schema suffix
const emailSchema = z.string().email();
const createTodoSchema = z.object({
title: z.string().min(1).max(100),
dueDate: z.string().optional(),
});
// 타입 추출
type CreateTodoInput = z.infer<typeof createTodoSchema>;// features/auth/store/auth.store.ts
import { create } from "zustand";
import { persist, immer } from "...";
type AuthStore = {
user: User | null;
setUser: (user: User) => void;
clearUser: () => void;
};
export const useAuthStore = create<AuthStore>()(
persist(
immer((set) => ({
user: null,
setUser: (user) =>
set((state) => {
state.user = user;
}),
clearUser: () =>
set((state) => {
state.user = null;
}),
})),
{ name: "taskmate-auth" },
),
);// entities/todo/query/todo.queryOptions.ts
export const todoQueryOptions = {
list: (params: TodoListParams) =>
queryOptions({
queryKey: ["todo", "list", params],
queryFn: () => getTodos(params),
staleTime: 60_000,
}),
};
// 사용 (features or widgets)
const { data } = useSuspenseQuery(todoQueryOptions.list(params));Mutation은 features/{domain}/mutation/use{Action}Mutation.ts 에 작성:
// features/goal/mutation/useCreatePersonalGoalMutation.ts
type UseCreatePersonalGoalMutationOptions = {
onSuccess?: () => void;
};
export function useCreatePersonalGoalMutation({
onSuccess,
}: UseCreatePersonalGoalMutationOptions = {}) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ name, dueDate }: { name: string; dueDate: string }) =>
goalApi.createGoal({ name, dueDate, type: "PERSONAL" }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["personal", "goals"] });
onSuccess?.();
},
});
}
// 사용 (widgets)
const { mutate: createGoal } = useCreatePersonalGoalMutation({
onSuccess: () => router.back(),
});규칙:
- navigation, modal 닫기 등 UI side effect는
onSuccess콜백으로 위임 — 훅 내부에서 처리 금지 -
queryClient.invalidateQueries는 훅 내부onSuccess에서 처리
// UserCard.test.tsx — 컴포넌트 옆에 위치
import { render, screen } from "@testing-library/react";
import UserCard from "./UserCard";
describe("UserCard", () => {
it("이름을 렌더링한다", () => {
render(
<UserCard
name="홍길동"
age={30}
/>,
);
expect(screen.getByText("홍길동")).toBeInTheDocument();
});
});