SPACE RUMI

Hi, I am rumi. Let's Splattack!

[IT] 프로덕트 개발/React - 리액트

프론트엔드 가이드라인 커서 룰 (FE Guideline Cursor rule)

스페이스RUMI 2025. 4. 22. 17:09

토스 프론트엔드 가이드라인 기반으로 만든 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

반응형