토스 프론트엔드 가이드라인 기반으로 만든 Cursor rule 입니다
코드블럭 내용은 권장패턴입니다
1. 매직넘버 대신 상수 사용
설명이 없는 숫자(매직 넘버) 대신 명명된 상수를 사용하여 명확성을 높입니다.
- 이해하기 어려운 값에 의미를 부여하여 가독성 향상
- 유지보수성 향상
const ANIMATION_DELAY_MS = 300;
async function onLikeClick() {
await postLike(url);
await delay(ANIMATION_DELAY_MS); // 애니메이션 대기 시간임을 명확히 표시
await refetchPostLike();
}
2. 구현 세부사항 추상화
복잡한 로직이나 상호작용을 전용 컴포넌트/HOC로 추상화합니다.
- 관심사를 분리하여 인지 부하 감소
- 컴포넌트의 가독성, 테스트 용이성, 유지보수성 향상
// 앱 구조
function App() {
return (
<AuthGuard> {/* 래퍼가 인증 확인 처리 */}
<LoginStartPage />
</AuthGuard>
);
}
// AuthGuard 컴포넌트가 확인, 리디렉션 로직을 캡슐화
function AuthGuard({ children }) {
const status = useCheckLoginStatus();
useEffect(() => {
if (status === "LOGGED_IN") {
location.href = "/home";
}
}, [status]);
// 로그인하지 않은 경우에만 자식 렌더링, 그렇지 않으면 null(또는 로딩)
return status !== "LOGGED_IN" ? children : null;
}
// LoginStartPage는 이제 로그인 UI/로직에만 집중
function LoginStartPage() {
// ... 로그인 관련 로직만 포함 ...
return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
}
export function FriendInvitation() {
const { data } = useQuery(/* ... */);
return (
<>
{/* 전용 버튼 컴포넌트 사용 */}
<InviteButton name={data.name} />
{/* ... 기타 UI ... */}
</>
);
}
// InviteButton이 확인 과정을 내부적으로 처리
function InviteButton({ name }) {
const handleClick = async () => {
const canInvite = await overlay.openAsync(({ isOpen, close }) => (
<ConfirmDialog
title={`${name}님과 공유하기`}
// ... 다이얼로그 설정 ...
/>
));
if (canInvite) {
await sendPush();
}
};
return <Button onClick={handleClick}>초대</Button>;
}
3. 조건부 렌더링을 위한 코드경로 분리
조건에 따라 크게 다른 UI/로직을 별도 컴포넌트로 분리합니다.
- 한 컴포넌트 내 복잡한 조건문을 피해 가독성 향상
- 각 특수화된 컴포넌트가 명확한 단일 책임을 갖도록 보장
function SubmitButton() {
const isViewer = useRole() === "viewer";
// 렌더링을 전문화된 컴포넌트에 위임
return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
}
// '뷰어' 역할에 특화된 컴포넌트
function ViewerSubmitButton() {
return <TextButton disabled>제출</TextButton>;
}
// '관리자'(또는 비뷰어) 역할에 특화된 컴포넌트
function AdminSubmitButton() {
useEffect(() => {
showAnimation(); // 애니메이션 로직 격리
}, []);
return <Button type="submit">제출</Button>;
}
4. 복잡한 삼항 연산자 단순화
복잡하거나 중첩된 삼항 연산자를 if/else 또는 IIFE로 대체하여 가독성을 높입니다.
- 조건부 로직을 더 쉽게 이해할 수 있게 함
- 전반적인 코드 유지보수성 향상
const status = (() => {
if (ACondition && BCondition) return "BOTH";
if (ACondition) return "A";
if (BCondition) return "B";
return "NONE";
})();
5. 시선 이동 줄이기 (간단한 로직은 함께 배치)
단순하고 지역화된 로직을 함께 배치하거나 인라인 정의를 사용하여 컨텍스트 전환을 줄입니다.
- 위에서 아래로 읽기와 빠른 이해를 가능하게 함
- 컨텍스트 전환(시선이동)으로 인한 인지부하 감소
function Page() {
const user = useUser();
// 로직이 여기서 바로 보임
switch (user.role) {
case "admin":
return (
<div>
<Button disabled={false}>초대</Button>
<Button disabled={false}>보기</Button>
</div>
);
case "viewer":
return (
<div>
<Button disabled={true}>초대</Button> {/* 뷰어 예시 */}
<Button disabled={false}>보기</Button>
</div>
);
default:
return null;
}
}
function Page() {
const user = useUser();
// 간단한 정책이 여기 정의되어 쉽게 확인 가능
const policy = {
admin: { canInvite: true, canView: true },
viewer: { canInvite: false, canView: true },
}[user.role];
// 역할이 일치하지 않을 경우 속성에 접근하기 전에 정책 존재 여부 확인
if (!policy) return null;
return (
<div>
<Button disabled={!policy.canInvite}>초대</Button>
<Button disabled={!policy.canView}>보기</Button>
</div>
);
}
6. 복잡한 조건 명명하기
복잡한 불리언 조건을 명명된 변수에 할당합니다.
- 조건의 의미를 명시적으로 만듦
- 인지 부하를 줄여 가독성과 자체 문서화 향상
const matchedProducts = products.filter((product) => {
// 제품이 대상 카테고리에 속하는지 확인
const isSameCategory = product.categories.some(
(category) => category.id === targetCategory.id
);
// 제품 가격이 원하는 범위 내에 있는지 확인
const isPriceInRange = product.prices.some(
(price) => price >= minPrice && price <= maxPrice
);
// 전체 조건이 이제 훨씬 명확함
return isSameCategory && isPriceInRange;
});
로직이 복잡하거나, 재사용되거나, 단위 테스트가 필요한 경우 조건에 이름을 붙입니다. 매우 단순하고 일회용인 조건은 이름을 붙이지 않아도 됩니다.
7. 반환 타입 표준화
유사한 함수/훅에 대해 일관된 반환 타입을 사용합니다.
- 코드 예측 가능성 향상; 개발자가 반환값 형태를 예상할 수 있음
- 일관되지 않은 타입으로 인한 혼란과 잠재적 오류 감소
// API훅 (React Query):: 항상 Query 객체 반환
import { useQuery, UseQueryResult } from "@tanstack/react-query";
// fetchUser가 Promise<UserType>을 반환한다고 가정
function useUser(): UseQueryResult<UserType, Error> {
const query = useQuery({ queryKey: ["user"], queryFn: fetchUser });
return query;
}
// fetchServerTime이 Promise<Date>를 반환한다고 가정
function useServerTime(): UseQueryResult<Date, Error> {
const query = useQuery({
queryKey: ["serverTime"],
queryFn: fetchServerTime,
});
return query;
}
// 유효성 검사 함수
type ValidationResult = { ok: true } | { ok: false; reason: string };
function checkIsNameValid(name: string): ValidationResult {
if (name.length === 0) return { ok: false, reason: "이름은 비워둘 수 없습니다." };
if (name.length >= 20)
return { ok: false, reason: "이름은 20자를 초과할 수 없습니다." };
return { ok: true };
}
function checkIsAgeValid(age: number): ValidationResult {
if (!Number.isInteger(age))
return { ok: false, reason: "나이는 정수여야 합니다." };
if (age < 18) return { ok: false, reason: "나이는 18세 이상이어야 합니다." };
if (age > 99) return { ok: false, reason: "나이는 99세 이하여야 합니다." };
return { ok: true };
}
// 사용 시 ok가 false일 때만 'reason'에 안전하게 접근 가능
const nameValidation = checkIsNameValid(name);
if (!nameValidation.ok) {
console.error(nameValidation.reason);
}
8. 숨겨진 로직 드러내기 (단일 책임)
숨겨진 부작용을 피하고, 함수는 서명이 암시하는 작업만 수행해야 합니다(SRP).
- 의도하지 않은 부작용 없이 예측 가능한 동작으로 이어짐
- 관심사 분리(SRP)를 통해 더 강력하고 테스트 가능한 코드 생성
// 함수는 *오직* 잔액만 가져옴
async function fetchBalance(): Promise<number> {
const balance = await http.get<number>("...");
return balance;
}
// 호출자가 필요한 경우 명시적으로 로깅 수행
async function handleUpdateClick() {
const balance = await fetchBalance(); // 가져오기
logging.log("balance_fetched"); // 로깅 (명시적 작업)
await syncBalance(balance); // 다른 작업
}
9. 고유하고 서술적인 이름 사용 (모호함 피하기)
맞춤형 래퍼/함수에 고유하고 서술적인 이름을 사용하여 모호함을 피합니다.
- 모호함을 피하고 예측 가능성 향상
- 개발자가 이름만으로 특정 작업(예: 인증 추가)을 이해할 수 있게 함
// httpService.ts - 더 명확한 모듈 이름
import { http as httpLibrary } from "@some-library/http";
export const httpService = {
// 고유한 모듈 이름
async getWithAuth(url: string) {
// 서술적인 함수 이름
const token = await fetchToken();
return httpLibrary.get(url, {
headers: { Authorization: `Bearer ${token}` },
});
},
};
import { httpService } from "./httpService";
export async function fetchUser() {
// 'getWithAuth' 이름이 동작을 명시적으로 만듦
return await httpService.getWithAuth("...");
}
10. Form 응집도 고려하기
폼 요구사항에 따라 필드 수준, 또는 폼 수준 응집도를 선택합니다.
- 필드 독립성(필드 수준)과 폼 통일성(폼 수준) 간의 균형을 맞춤
- 요구사항에 따라 관련 폼 로직이 적절히 그룹화되도록 보장
// 각 필드가 자체 `validate` 함수 사용. (필드 수준)
import { useForm } from "react-hook-form";
export function Form() {
const {
register,
formState: { errors },
handleSubmit,
} = useForm({
/* defaultValues 등 */
});
const onSubmit = handleSubmit((formData) => {
console.log("폼 제출됨:", formData);
});
return (
<form onSubmit={onSubmit}>
<div>
<input
{...register("name", {
validate: (value) =>
value.trim() === "" ? "이름을 입력해주세요." : true, // 유효성 검사 예시
})}
placeholder="이름"
/>
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<input
{...register("email", {
validate: (value) =>
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)
? true
: "유효하지 않은 이메일 주소입니다.", // 유효성 검사 예시
})}
placeholder="이메일"
/>
{errors.email && <p>{errors.email.message}</p>}
</div>
<button type="submit">제출</button>
</form>
);
}
// 단일 스키마가 전체 폼에 대한 유효성 검사 정의 (폼 수준)
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
name: z.string().min(1, "이름을 입력해주세요."),
email: z.string().min(1, "이메일을 입력해주세요.").email("유효하지 않은 이메일입니다."),
});
export function Form() {
const {
register,
formState: { errors },
handleSubmit,
} = useForm({
resolver: zodResolver(schema),
defaultValues: { name: "", email: "" },
});
const onSubmit = handleSubmit((formData) => {
console.log("폼 제출됨:", formData);
});
return (
<form onSubmit={onSubmit}>
<div>
<input {...register("name")} placeholder="이름" />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<input {...register("email")} placeholder="이메일" />
{errors.email && <p>{errors.email.message}</p>}
</div>
<button type="submit">제출</button>
</form>
);
}
독립적인 유효성 검사, 비동기 검사 또는 재사용 가능한 필드의 경우 필드 수준을 선택하고, 관련 필드, 위저드 폼 또는 상호 의존적인 유효성 검사의 경우 폼 수준을 선택.
11. 기능/도메인별 코드구성
디렉토리를 코드 유형뿐만 아니라 기능/도메인별로 구성합니다.
- 관련 파일을 함께 유지하여 응집도 증가
- 기능 이해, 개발, 유지보수 및 삭제 단순화
src/
├── components/ # 공유/공통 컴포넌트
├── hooks/ # 공유/공통 훅
├── utils/ # 공유/공통 유틸
├── domains/
│ ├── user/
│ │ ├── components/
│ │ │ └── UserProfileCard.tsx
│ │ ├── hooks/
│ │ │ └── useUser.ts
│ │ └── index.ts # 선택적 배럴 파일
│ ├── product/
│ │ ├── components/
│ │ │ └── ProductList.tsx
│ │ ├── hooks/
│ │ │ └── useProducts.ts
│ │ └── ...
│ └── order/
│ ├── components/
│ │ └── OrderSummary.tsx
│ ├── hooks/
│ │ └── useOrder.ts
│ └── ...
└── App.tsx
12. 매직넘버를 로직과 연관짓기
상수를 관련 로직 근처에 정의하거나 이름이 명확하게 연결되도록 합니다.
- 상수를 나타내는 로직과 연결하여 응집도 향상
- 관련 상수를 업데이트하지 않고 로직을 업데이트하여 발생하는 이슈 방지
// 상수가 명확하게 명명되고 잠재적으로 애니메이션 로직 근처에 정의됨
const ANIMATION_DELAY_MS = 300;
async function onLikeClick() {
await postLike(url);
// 딜레이는 상수를 사용하여 애니메이션과의 연결 유지
await delay(ANIMATION_DELAY_MS);
await refetchPostLike();
}
상수가 의존하는 로직과 함께 유지되거나 관계를 보여주기 위해 명확하게 명명되도록 보장.
13. 추상화와 결합도 균형 맞추기 (조기추상화 피하기)
사용 사례가 분기될 수 있는 경우 중복을 조기에 추상화하지 않고 낮은 결합도를 선호합니다.
- 잠재적으로 분기되는 로직을 하나의 추상화로 강제하는 강한 결합 방지
- 미래 요구사항이 불확실할 때 일부 중복을 허용하면 결합도를 낮추고 유지보수성을 향상시킬 수 있음
추상화하기 전에 로직이 정말로 동일한지, 그리고 모든 사용 사례에서 동일하게 유지될 가능성이 있는지 고려하기. 분기가 가능한 경우(예: 다른 페이지가 useOpenMaintenanceBottomSheet와 같은 공유 훅에서 약간 다른 동작이 필요한 경우), 처음에 로직을 분리하는 것(중복 허용)이 더 유지보수 가능하고 결합이 낮은 코드로 이어질 수 있습니다. 팀과 함께 트레이드오프를 논의하세요.
14. 상태 관리 범위 지정 (너무 넓은 훅 피하기)
광범위한 상태 관리를 작고 집중된 훅/컨텍스트로 분리합니다.
- 컴포넌트가 필요한 '상태'에만 의존하도록 보장하여 결합도 감소
- 관련 없는 상태 변경으로 인한 불필요한 리렌더링을 방지하여 성능 향상
// cardId 쿼리 매개변수에 특화된 훅
import { useQueryParam, NumberParam } from "use-query-params";
import { useCallback } from "react";
export function useCardIdQueryParam() {
// 'query'가 원시 매개변수 값을 제공한다고 가정
const [cardIdParam, setCardIdParam] = useQueryParam("cardId", NumberParam);
const setCardId = useCallback(
(newCardId: number | undefined) => {
setCardIdParam(newCardId, "replaceIn"); // 또는 원하는 히스토리 동작에 따라 'push'
},
[setCardIdParam]
);
// 안정적인 반환 튜플 제공
return [cardIdParam ?? undefined, setCardId] as const;
}
// 날짜 범위 등에 대한 별도 훅
// export function useDateRangeQueryParam() { /* ... */ }
15. 컴포지션을 통한 Props 드릴링 제거
Props 드릴링 대신 컴포넌트 구성을 사용합니다.
- 불필요한 중간 종속성을 제거하여 결합도를 크게 감소
- 리팩토링을 쉽게 만들고 더 평평한 컴포넌트 트리에서 데이터 흐름을 명확하게 함
import React, { useState } from "react";
// Modal, Input, Button, ItemEditList 컴포넌트가 존재한다고 가정
function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) {
const [keyword, setKeyword] = useState("");
// 자식을 Modal 내에 직접 렌더링하고 필요한 곳에만 props 전달
return (
<Modal open={open} onClose={onClose}>
{/* Input과 Button 직접 렌더링 */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "1rem",
}}
>
<Input
value={keyword}
onChange={(e) => setKeyword(e.target.value)} // 상태는 여기서 관리
placeholder="항목 검색..."
/>
<Button onClick={onClose}>닫기</Button>
</div>
{/* ItemEditList 직접 렌더링, 필요한 props 받음 */}
<ItemEditList
keyword={keyword} // 직접 전달
items={items} // 직접 전달
recommendedItems={recommendedItems} // 직접 전달
onConfirm={onConfirm} // 직접 전달
/>
</Modal>
);
}
// 중간 ItemEditBody 컴포넌트가 제거되어 결합도 감소.
출처 : https://gist.github.com/toy-crane/dde6258997519d954063a536fc72d055
'[IT] 프로덕트 개발 > React - 리액트' 카테고리의 다른 글
No index signature with a parameter of type 'string' was found on type | 문자열 인덱싱 시그니처 타입에러 (0) | 2023.11.16 |
---|---|
react에서 swiper 사용하기 (feat. reading 'wrapperClass') (0) | 2023.01.10 |
[React Hook] useMemo를 알아보자 (0) | 2023.01.05 |
Typescript IntrinsicAttributes Error (0) | 2023.01.03 |
react에서 html string render / HTML 파싱 (0) | 2022.11.21 |