一键导入
good-code
Use when implementing any new feature, writing components, or reviewing code structure. Covers 4 code quality principles from Toss Frontend Fundamentals.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when implementing any new feature, writing components, or reviewing code structure. Covers 4 code quality principles from Toss Frontend Fundamentals.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when writing or modifying UI components, interactive elements, forms, or any HTML structure - before finalizing markup
Use when modifying build config, vite.config, adding new packages, setting up code splitting, or working with module resolution and imports
Use when encountering a bug, unexpected behavior, or error in the frontend - before attempting any fix
| name | good-code |
| description | Use when implementing any new feature, writing components, or reviewing code structure. Covers 4 code quality principles from Toss Frontend Fundamentals. |
좋은 코드 = 변경하기 쉬운 코드. 아래 4가지 기준으로 판단하되, 상황에 따라 유연하게 적용한다.
서로 다른 조건에서만 실행되는 코드가 한 컴포넌트에 섞이면 읽기 어렵다.
// ❌ 같이 있으면 두 경우를 동시에 생각해야 함
function SubmitButton() {
const isViewer = useRole() === "viewer";
useEffect(() => {
if (!isViewer) showButtonAnimation();
}, [isViewer]);
return isViewer ? (
<TextButton disabled>Submit</TextButton>
) : (
<Button type="submit">Submit</Button>
);
}
// ✅ 각자 자신의 역할만 신경 씀
function SubmitButton() {
const isViewer = useRole() === "viewer";
return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
}
복잡한 구현을 숨기고 의미 있는 이름으로 감싸면, 읽는 사람이 한 번에 처리할 맥락이 줄어든다.
// ❌ 인증 확인 로직이 페이지 안에 그대로 노출
function LoginStartPage() {
const { data: user } = useUser();
if (!user) return <Navigate to="/login" />;
return <Main />;
}
// ✅ 의도가 이름으로 드러남
function LoginStartPage() {
return (
<AuthGuard>
<Main />
</AuthGuard>
);
}
조건이 복잡하거나 여러 줄이면 변수에 담아 의도를 드러낸다.
// ❌ 무슨 조건인지 바로 파악 안됨
const result = products.filter((p) =>
p.categories.some(
(c) =>
c.id === targetCategory.id &&
p.prices.some((price) => price >= min && price <= max),
),
);
// ✅ 이름이 의도를 설명
const matchedProducts = products.filter((product) => {
return product.categories.some((category) => {
const isSameCategory = category.id === targetCategory.id;
const isPriceInRange = product.prices.some(
(price) => price >= min && price <= max,
);
return isSameCategory && isPriceInRange;
});
});
단순한 표현식(arr.map(x => x * 2))은 이름 없이도 명확하다.
숫자만 보면 의도를 알 수 없다. 이름 있는 상수로 목적을 드러낸다.
// ❌ 300이 뭔지 모름 — 애니메이션? API 딜레이? 테스트 코드?
await delay(300);
// ✅ 목적이 명확
const ANIMATION_DELAY_MS = 300;
await delay(ANIMATION_DELAY_MS);
조건을 이해하려고 다른 파일/함수로 이동해야 한다면, 호출 시점에서 바로 알 수 있도록 인라인으로 옮긴다.
// ❌ policy가 뭔지 알려면 getPolicyByRole, POLICY_SET까지 따라가야 함
const policy = getPolicyByRole(user.role);
if (!policy.canInvite) return null;
// ✅ 한 눈에 파악 가능
const policy = { admin: { canInvite: true }, viewer: { canInvite: false } }[
user.role
];
삼항이 중첩되면 어떤 조건에서 어떤 값인지 추적하기 어렵다.
// ❌ 읽기 어려움
const status = A && B ? "BOTH" : A || B ? (A ? "A" : "B") : "NONE";
// ✅ 명확
const status = (() => {
if (A && B) return "BOTH";
if (A) return "A";
if (B) return "B";
return "NONE";
})();
b <= a && a <= c 형태가 수학적 부등식과 같아 직관적이다.
// ❌ 중간값을 두 번 처리해야 파악됨
if (score >= 80 && score <= 100) { ... }
// ✅ 수직선에서 범위를 보듯 읽힘
if (80 <= score && score <= 100) { ... }
하나의 훅/함수가 여러 역할을 한꺼번에 담당하면 필요한 부분만 이해하기 어렵다.
// ❌ 모든 파라미터를 한 훅에서 관리 → 카드 ID만 쓰는데 전체 의존
const { cardId, statementId, dateFrom, ... } = usePageState();
// ✅ 역할이 명확한 전용 훅
const [cardId, setCardId] = useCardIdQueryParam();
이름이 같으면 동일하게 동작해야 한다. 동작이 다르면 이름을 다르게 해서 차이를 드러낸다.
// ❌ http.get()인데 인증 헤더를 자동으로 붙임 — 호출하는 쪽이 예상 못함
export const http = {
async get(url: string) {
const token = await fetchToken();
return httpLibrary.get(url, { headers: { Authorization: `Bearer ${token}` } });
}
};
// ✅ 이름에서 차이가 드러남
export const httpService = {
async getWithAuth(url: string) { ... }
};
같은 종류의 함수가 제각각 반환 타입을 쓰면, 쓸 때마다 타입을 확인해야 한다.
// ❌ useUser()는 Query 객체, useServerTime()은 값만 반환
const { data: user } = useUser();
const serverTime = useServerTime(); // 값 바로 반환
// ❌ 검증 함수 반환 타입이 불일치
checkIsNameValid(name); // boolean
checkIsAgeValid(age); // { ok: false, reason: string } — object는 항상 truthy!
// ✅ 타입을 통일하고 Discriminated Union 활용
type ValidationResult = { ok: true } | { ok: false; reason: string };
함수 이름이 약속하지 않은 동작은 호출하는 쪽에서 예상할 수 없다.
// ❌ fetchBalance인데 로깅도 함 — 이름에서 예측 불가
async function fetchBalance(): Promise<number> {
const balance = await http.get<number>("...");
logging.log("balance_fetched"); // 숨은 사이드이펙트
return balance;
}
// ✅ 로깅은 호출하는 쪽에서 명시적으로
async function fetchBalance(): Promise<number> {
return http.get<number>("...");
}
// 호출 시점에서 의도가 보임
const balance = await fetchBalance();
logging.log("balance_fetched");
타입별(components/, hooks/, utils/)로 나누면 하나의 기능을 수정할 때 여러 디렉토리를 오가야 한다. 도메인 기반으로 묶으면 관련 파일이 한 곳에 있다.
// ❌ 타입 기반 — 기능 하나 수정에 3개 디렉토리 이동
src/components/UserCard.tsx
src/hooks/useUser.ts
src/utils/userUtils.ts
// ✅ 도메인 기반 — 관련 파일이 한 곳에, 삭제도 폴더 하나면 끝
src/domains/User/
UserCard.tsx
useUser.ts
userUtils.ts
공통으로 쓰는 것만 최상위 components/, hooks/, utils/에 둔다. 도메인 간 import(../../OtherDomain/hooks)가 보이면 의존 관계를 의심한다.
매직 넘버를 상수로 만들면 값의 의미와 위치가 함께 관리된다. 애니메이션 딜레이가 바뀌면 ANIMATION_DELAY_MS만 수정하면 된다.
react-hook-form 개별 validate)zod schema)변경 패턴에 맞춰 선택한다.
여러 역할을 담당하면 한 곳을 수정할 때 다른 역할에 영향이 생긴다.
// ❌ usePageState가 모든 쿼리 파라미터를 한 번에 관리
// → cardId만 바꿔도 statementId 사용 컴포넌트도 리렌더링
// ✅ 각 파라미터를 전담하는 훅으로 분리
function useCardIdQueryParam() { ... }
function useStatementIdQueryParam() { ... }
// 수정 영향 범위가 해당 훅 사용처로만 한정됨
공통으로 쓸 수 있어도, 각 사용처의 요구사항이 나중에 달라질 수 있다. 억지로 묶으면 한쪽을 수정할 때 다른 쪽까지 테스트해야 한다.
// 두 페이지에서 거의 같은 바텀시트 로직 사용 → 당장은 같아 보여도
// 페이지 A는 로깅 값이 다르고, 페이지 B는 닫기 동작이 다를 수 있음
// → 나중에 달라질 것 같으면 처음부터 분리하는 게 낫다
판단 기준: 지금도, 앞으로도 동일하게 동작할 것이 확실할 때만 공통화한다.
중간 컴포넌트가 실제로 사용하지 않는 props를 단순 전달만 할 때, 이름이 바뀌면 연쇄 수정이 발생한다.
children으로 내려보내 중간 계층 제거// ❌ 중간 컴포넌트들이 items, recommendedItems, keyword를 그냥 통과시킴
<ItemEditModal items={items} recommendedItems={...} keyword={...} />
<ItemEditBody items={items} recommendedItems={...} keyword={...} />
<ItemEditList items={items} ... />
// ✅ 조합 패턴
<ItemEditModal>
<ItemEditList items={items} />
</ItemEditModal>