| 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. |
Good Code
์ข์ ์ฝ๋ = ๋ณ๊ฒฝํ๊ธฐ ์ฌ์ด ์ฝ๋. ์๋ 4๊ฐ์ง ๊ธฐ์ค์ผ๋ก ํ๋จํ๋, ์ํฉ์ ๋ฐ๋ผ ์ ์ฐํ๊ฒ ์ ์ฉํ๋ค.
1. ๊ฐ๋
์ฑ โ ํ ๋ฒ์ ๊ณ ๋ คํ๋ ๋งฅ๋ฝ์ ์ค์ธ๋ค
๊ฐ์ด ์คํ๋์ง ์๋ ์ฝ๋๋ ๋ถ๋ฆฌ
์๋ก ๋ค๋ฅธ ์กฐ๊ฑด์์๋ง ์คํ๋๋ ์ฝ๋๊ฐ ํ ์ปดํฌ๋ํธ์ ์์ด๋ฉด ์ฝ๊ธฐ ์ด๋ ต๋ค.
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))์ ์ด๋ฆ ์์ด๋ ๋ช
ํํ๋ค.
๋งค์ง ๋๋ฒ์ ์ด๋ฆ ๋ถ์ด๊ธฐ
์ซ์๋ง ๋ณด๋ฉด ์๋๋ฅผ ์ ์ ์๋ค. ์ด๋ฆ ์๋ ์์๋ก ๋ชฉ์ ์ ๋๋ฌ๋ธ๋ค.
await delay(300);
const ANIMATION_DELAY_MS = 300;
await delay(ANIMATION_DELAY_MS);
์์ ์ด๋ ์ค์ด๊ธฐ
์กฐ๊ฑด์ ์ดํดํ๋ ค๊ณ ๋ค๋ฅธ ํ์ผ/ํจ์๋ก ์ด๋ํด์ผ ํ๋ค๋ฉด, ํธ์ถ ์์ ์์ ๋ฐ๋ก ์ ์ ์๋๋ก ์ธ๋ผ์ธ์ผ๋ก ์ฎ๊ธด๋ค.
const policy = getPolicyByRole(user.role);
if (!policy.canInvite) return null;
const policy = { admin: { canInvite: true }, viewer: { canInvite: false } }[
user.role
];
์ค์ฒฉ ์ผํญ ๋์ IIFE + if๋ฌธ
์ผํญ์ด ์ค์ฒฉ๋๋ฉด ์ด๋ค ์กฐ๊ฑด์์ ์ด๋ค ๊ฐ์ธ์ง ์ถ์ ํ๊ธฐ ์ด๋ ต๋ค.
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) { ... }
์คํ ๋งฅ๋ฝ๋ณ๋ก ํจ์ ๋ถ๋ฆฌ
ํ๋์ ํ
/ํจ์๊ฐ ์ฌ๋ฌ ์ญํ ์ ํ๊บผ๋ฒ์ ๋ด๋นํ๋ฉด ํ์ํ ๋ถ๋ถ๋ง ์ดํดํ๊ธฐ ์ด๋ ต๋ค.
const { cardId, statementId, dateFrom, ... } = usePageState();
const [cardId, setCardId] = useCardIdQueryParam();
2. ์์ธก ๊ฐ๋ฅ์ฑ โ ์ด๋ฆ๋ง ๋ด๋ ๋์์ ์ ์ ์์ด์ผ ํ๋ค
๊ฐ์ ์ด๋ฆ = ๊ฐ์ ๋์
์ด๋ฆ์ด ๊ฐ์ผ๋ฉด ๋์ผํ๊ฒ ๋์ํด์ผ ํ๋ค. ๋์์ด ๋ค๋ฅด๋ฉด ์ด๋ฆ์ ๋ค๋ฅด๊ฒ ํด์ ์ฐจ์ด๋ฅผ ๋๋ฌ๋ธ๋ค.
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) { ... }
};
๋น์ทํ ์ญํ ์ ํจ์๋ ๋ฐํ ํ์
ํต์ผ
๊ฐ์ ์ข
๋ฅ์ ํจ์๊ฐ ์ ๊ฐ๊ฐ ๋ฐํ ํ์
์ ์ฐ๋ฉด, ์ธ ๋๋ง๋ค ํ์
์ ํ์ธํด์ผ ํ๋ค.
const { data: user } = useUser();
const serverTime = useServerTime();
checkIsNameValid(name);
checkIsAgeValid(age);
type ValidationResult = { ok: true } | { ok: false; reason: string };
์จ์ ์ฌ์ด๋์ดํํธ๋ ํจ์ ์ด๋ฆ์ ๋๋ฌ๋ด๊ฑฐ๋ ํธ์ถ ์์ ์ผ๋ก ์ด๋
ํจ์ ์ด๋ฆ์ด ์ฝ์ํ์ง ์์ ๋์์ ํธ์ถํ๋ ์ชฝ์์ ์์ํ ์ ์๋ค.
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");
3. ์์ง๋ โ ํจ๊ป ๋ฐ๋๋ ์ฝ๋๋ ํจ๊ป ์์ด์ผ ํ๋ค
๋๋ฉ์ธ ๊ธฐ๋ฐ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
ํ์
๋ณ(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๋ง ์์ ํ๋ฉด ๋๋ค.
ํผ ์์ง๋: ํ๋ ๋จ์ vs ํผ ๋จ์
- ํ๋ ๋จ์: ๊ฐ ์
๋ ฅ์ด ๋
๋ฆฝ์ ์ผ๋ก ๊ฒ์ฆ๋ ๋, ํ๋๋ฅผ ์ฌ์ฌ์ฉํ ๋ (
react-hook-form ๊ฐ๋ณ validate)
- ํผ ๋จ์: ํ๋ ๊ฐ ์์กด์ฑ์ด ์์ ๋, ํผ ์ ์ฒด๊ฐ ํ๋์ ๊ธฐ๋ฅ์ผ ๋ (
zod schema)
๋ณ๊ฒฝ ํจํด์ ๋ง์ถฐ ์ ํํ๋ค.
4. ๊ฒฐํฉ๋ โ ์์ ์ ์ํฅ ๋ฒ์๋ฅผ ์๊ฒ ์ ์งํ๋ค
ํ
/ํจ์๋ ํ๋์ ์ฑ
์๋ง
์ฌ๋ฌ ์ญํ ์ ๋ด๋นํ๋ฉด ํ ๊ณณ์ ์์ ํ ๋ ๋ค๋ฅธ ์ญํ ์ ์ํฅ์ด ์๊ธด๋ค.
function useCardIdQueryParam() { ... }
function useStatementIdQueryParam() { ... }
์ํฅ๋ฒ์๊ฐ ๋ค๋ฅธ ์ค๋ณต ์ฝ๋๋ ๋ถ๋ฆฌ ์ ์ง
๊ณตํต์ผ๋ก ์ธ ์ ์์ด๋, ๊ฐ ์ฌ์ฉ์ฒ์ ์๊ตฌ์ฌํญ์ด ๋์ค์ ๋ฌ๋ผ์ง ์ ์๋ค. ์ต์ง๋ก ๋ฌถ์ผ๋ฉด ํ์ชฝ์ ์์ ํ ๋ ๋ค๋ฅธ ์ชฝ๊น์ง ํ
์คํธํด์ผ ํ๋ค.
ํ๋จ ๊ธฐ์ค: ์ง๊ธ๋, ์์ผ๋ก๋ ๋์ผํ๊ฒ ๋์ํ ๊ฒ์ด ํ์คํ ๋๋ง ๊ณตํตํํ๋ค.
Props Drilling ์ ๊ฑฐ
์ค๊ฐ ์ปดํฌ๋ํธ๊ฐ ์ค์ ๋ก ์ฌ์ฉํ์ง ์๋ props๋ฅผ ๋จ์ ์ ๋ฌ๋ง ํ ๋, ์ด๋ฆ์ด ๋ฐ๋๋ฉด ์ฐ์ ์์ ์ด ๋ฐ์ํ๋ค.
- ์กฐํฉ ํจํด (๋จผ์ ์๋):
children์ผ๋ก ๋ด๋ ค๋ณด๋ด ์ค๊ฐ ๊ณ์ธต ์ ๊ฑฐ
- Context API: ์กฐํฉ์ผ๋ก ํด๊ฒฐ ์ ๋ ๋๋ง ์ฌ์ฉ
<ItemEditModal items={items} recommendedItems={...} keyword={...} />
<ItemEditBody items={items} recommendedItems={...} keyword={...} />
<ItemEditList items={items} ... />
<ItemEditModal>
<ItemEditList items={items} />
</ItemEditModal>
์ฒดํฌ๋ฆฌ์คํธ