Back to skills
extension
Category: Development & EngineeringNo API key required

react-refactoring

React component code analysis and refactoring. Improve code quality, transform to clean code, perform optimizations. Use when refactoring React components, improving code quality, or cleaning up code, or when user mentions '리팩토링', '코드 개선', '코드 정리', '코드 최적화', 'refactor'. Not for removing unused code (use refactor-clean) or architecture redesign (use component-architecture).

personAuthor: jakexiaohubgithub

React Refactoring Skill

React 컴포넌트의 코드를 분석하고, 개선 가능한 패턴을 찾아 순차적으로 리팩토링하는 skill입니다.

워크플로우

1단계: 대상 파일/범위 확인

사용자에게 리팩토링 대상을 확인하세요:

  • 특정 파일이 지정되었는지
  • 특정 디렉토리 범위인지
  • 프로젝트 전체인지

2단계: 코드 분석

대상 코드를 읽고 다음 리팩토링 패턴들을 체크하세요:

체크리스트

| # | 패턴 | 설명 | 우선순위 | |---|------|------|----------| | 1 | URL 상태 관리 | useState + useEffect로 관리하는 재현 가능 UI 상태를 URL searchParams로 변경 | 높음 | | 2 | URL/Form/Server state 경계 분리 | URL은 확정 조건, form은 draft, query/loader는 서버 캐시로 분리 | 높음 | | 3 | search params 기반 queryKey 구성 | 필터/페이지 변경 시 TanStack Query queryKey도 함께 변경 | 높음 | | 4 | route-level search validation | 컴포넌트별 parsing 대신 route boundary에서 검증 | 높음 | | 5 | setState 내부 side effect 분리 | setState updater 함수 내부의 side effect를 외부로 분리 | 높음 | | 6 | 중복 핸들러 통합 | 동일한 로직의 핸들러 함수들을 하나로 통합 | 높음 | | 7 | 중복 로직 useMemo 추출 | 여러 곳에서 반복되는 계산 로직을 useMemo로 추출 | 높음 | | 8 | canonical URL 정규화 | 내부 fallback과 URL 불일치를 줄이기 위해 표준 URL 표현으로 정규화 | 중간 | | 9 | 타입 가드 함수 적용 | 반복되는 타입 캐스팅(as)을 타입 가드 함수로 개선 | 중간 | | 10 | Zustand 셀렉터 최적화 | 컴포넌트 내에서 액션만 사용 시 직접 export된 actions 사용 | 중간 | | 11 | 유틸리티 함수 추출 | 반복되는 패턴(Toast 표시 등)을 유틸리티 함수로 추출 | 중간 | | 12 | 조건부 비활성화 처리 | 빈 상태, 로딩 상태 등에서 버튼/액션 비활성화 | 중간 | | 13 | 성공 후 정리 로직 추가 | 작업 성공 후 필요한 정리 로직(clearCart 등) 누락 확인 | 중간 | | 14 | 컴포넌트 분리 | 큰 컴포넌트에서 독립적인 UI 단위를 별도 컴포넌트로 분리 | 중간 | | 15 | CSS 브레이크포인트 리팩토링 | 뷰포트 미디어 쿼리 중심 스타일을 clamp(), intrinsic layout, container query로 전환 | 중간 | | 16 | 불필요한 래퍼 함수 제거 | 단순히 다른 함수를 호출하기만 하는 래퍼 함수 제거 | 낮음 | | 17 | 타입 개선 | 암묵적 any 제거, 더 정확한 타입 사용 | 낮음 | | 18 | 불필요한 코멘트 제거 | 코드로 이미 명확한 내용을 설명하는 코멘트 제거 | 낮음 | | 19 | 코드 스페이싱 정리 | 객체 내부나 JSX에서 불필요한 빈 줄 제거 | 낮음 |

병렬 분석 전략 (Sub-agent 활용)

분석 대상 규모에 따라 분석 방식을 선택하세요:

단일/소수 파일 (1~3개):

  • 직접 순차 분석 수행
  • Sub-agent 오버헤드가 더 클 수 있음

다수 파일 (4개 이상) 또는 프로젝트 전체:

  • Task tool로 Explore sub-agent를 병렬 호출하여 분석 시간 단축
  • 각 agent가 독립적인 컨텍스트에서 작업하므로 메인 컨텍스트 보존

병렬 분석 예시:

# 3개의 Explore agent를 동시에 호출
Task 1 (Explore): "src/pages/ 디렉토리의 모든 컴포넌트에서 리팩토링 패턴 체크리스트 분석"
Task 2 (Explore): "src/components/ 디렉토리의 모든 컴포넌트에서 리팩토링 패턴 체크리스트 분석"
Task 3 (Explore): "src/hooks/ 디렉토리의 모든 훅에서 리팩토링 패턴 체크리스트 분석"

주의사항:

  • Explore agent는 읽기 전용 (Haiku 모델, 빠르고 저렴)
  • 각 agent에게 체크리스트 패턴을 명시적으로 전달
  • 결과를 통합하여 사용자에게 보고

3단계: 개선점 보고

분석 결과를 사용자에게 보고하세요:

## 분석 결과

### [파일명]

1. **[패턴명]** - [현재 상태 설명]
   - 현재: [현재 코드 요약]
   - 개선: [개선 방향]

2. **[패턴명]** - [현재 상태 설명]
   ...

4단계: 순차적 리팩토링 및 커밋

중요: 여러 개의 리팩토링을 진행할 때, 각 로직별로 커밋을 찍으면서 순차적으로 진행합니다.

각 리팩토링 항목마다:

  1. 해당 패턴의 코드 변경 수행
  2. 변경사항에 대한 커밋 생성
  3. 다음 리팩토링으로 이동

커밋 메시지 형식:

refactor([scope]): [변경 내용]

- 상세 변경 사항 1
- 상세 변경 사항 2

예시:

refactor(MenuPage): URL searchParams로 카테고리 상태 관리 변경

- useState + useEffect 제거
- useSearchParams로 카테고리 상태 관리
- 새로고침/뒤로가기 시에도 상태 유지

5단계: ESLint 실행

모든 리팩토링 완료 후 lint 에러 확인 및 수정:

yarn eslint <변경된 파일들> --fix

리팩토링 패턴 상세

0. CSS 브레이크포인트 리팩토링

반응형 UI 리팩토링은 @media를 모두 제거하는 작업이 아닙니다. 뷰포트 기준 분기를 기본 엔진으로 쓰던 코드를 컴포넌트가 가진 실제 공간 기준으로 바꾸는 작업입니다.

적용 판단 기준

우선 기존 @media 규칙을 아래 네 가지로 분류합니다.

| 분류 | 예시 | 리팩토링 방향 | |------|------|---------------| | 스칼라 변경 | font-size, padding, gap, radius | clamp(), min(), max(), fluid token | | 자연 배치 | card grid, product list, gallery | auto-fit, auto-fill, minmax() | | 구조 변경 | sidebar card vs main card, compact/expanded | container-type, @container | | 환경 대응 | hover, pointer, reduced-motion, color-scheme | @media 유지 |

Before: viewport breakpoint 중심

.product-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

@media (min-width: 768px) {
  .product-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 20px;
  }
}

@media (min-width: 1024px) {
  .product-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 24px;
  }
}

After: intrinsic layout + fluid scalar

.product-grid {
  display: grid;
  gap: clamp(1rem, 2vw, 1.5rem);
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}

컨테이너 기준 컴포넌트 전환

같은 컴포넌트가 사이드바, 모달, 본문에 재사용된다면 뷰포트보다 컨테이너 기준이 적합합니다.

.article-card-shell {
  container-type: inline-size;
}

.article-card {
  padding: clamp(1rem, 5cqi, 2rem);
  border-radius: clamp(0.5rem, 4cqi, 1.5rem);
}

.article-card__body {
  display: grid;
  gap: 1rem;
}

@container (min-width: 36rem) {
  .article-card__body {
    grid-template-columns: 12rem 1fr;
    align-items: center;
  }
}

리팩토링 체크리스트

  • [ ] @media가 레이아웃 분기인지, 환경 대응인지 먼저 분리합니다.
  • [ ] 폰트/간격/패딩/radius 단계 조정은 clamp(), min(), max()로 바꿉니다.
  • [ ] 고정 열 수 그리드는 repeat(auto-fit|auto-fill, minmax(..., 1fr))로 바꿀 수 있는지 확인합니다.
  • [ ] 재사용 컴포넌트에는 필요한 경우 container-type: inline-size를 추가합니다.
  • [ ] 내부 구조 변경에만 @container를 사용합니다.
  • [ ] @media는 페이지 셸 전환, hover/pointer, reduced-motion, color-scheme, contrast 같은 환경 대응에 남깁니다.
  • [ ] 사이드바, 모달, 본문, 모바일 폭에서 같은 컴포넌트를 확인합니다.

피해야 할 패턴

  • 컴포넌트 내부 레이아웃을 전역 768px, 1024px 기준에 강하게 묶기
  • 단순 spacing 조정마다 breakpoint 추가하기
  • vw로 재사용 컴포넌트 크기를 계산해 사이드바/모달에서 과도하게 커지게 만들기
  • @container로 해결할 문제와 clamp()로 해결할 문제를 구분하지 않기

1. URL 상태 관리

검색어, 필터, 정렬, 페이지네이션처럼 URL로 다시 열었을 때 같은 화면이 재현되어야 하는 상태는 React state보다 URL search params를 원천으로 둡니다.

적용 판단 기준

URL state로 옮기기 좋은 상태:

  • 검색어: keyword
  • 필터: status, category, startDate, endDate
  • 정렬: sort
  • 페이지네이션: page, pageSize
  • 탭 또는 뷰 모드: view

URL state로 옮기지 않는 상태:

  • hover, popover open 여부 같은 일시적 UI 상태
  • submit 전 입력 중인 form draft
  • 서버 응답 데이터 자체
  • URL에 노출하면 안 되는 민감 정보

URL / Form / Server state 분리

| 상태 | 역할 | 저장 위치 | |------|------|----------| | URL state | 현재 결과 화면을 결정하는 확정 조건 | search params | | Form state | 사용자가 입력 중인 draft | React Hook Form 또는 local state | | Server state | URL 조건으로 가져온 서버 응답 | TanStack Query cache 또는 loader data |

submit 기반 검색 화면에서는 form 값으로 직접 refetch하지 말고, submit 시 URL을 바꿔서 queryKey 또는 loaderDeps가 자연스럽게 바뀌게 만듭니다.

Before: local state와 effect로 화면 조건 관리

const [activeCategory, setActiveCategory] = useState('')

useEffect(() => {
  if (categories.length > 0 && !activeCategory) {
    setActiveCategory(categories[0])
  }
}, [categories, activeCategory])

문제점:

  • 새로고침하면 상태가 사라집니다.
  • 뒤로 가기와 공유 링크가 화면 상태를 재현하지 못합니다.
  • 기본값이 URL에 드러나지 않아 실제 URL과 내부 상태가 달라질 수 있습니다.

After: React Router search params 사용

import { useSearchParams } from 'react-router-dom'

const [searchParams, setSearchParams] = useSearchParams()
const categoryParam = searchParams.get('category')
const activeCategory = categoryParam ?? categories[0] ?? ''

const handleCategoryChange = (category: string) => {
  setSearchParams({ category })
}

route-level search validation

URL은 사용자가 직접 수정할 수 있는 문자열 입력입니다. 컴포넌트마다 searchParams.get()과 fallback을 반복하지 말고, route 또는 page boundary에서 검증된 search object로 바꿉니다.

const usersSearchSchema = z.object({
  keyword: z.string().default(''),
  status: z.enum(['all', 'active', 'inactive']).default('all'),
  page: z.coerce.number().int().min(1).default(1),
})

function parseUsersSearch(searchParams: URLSearchParams) {
  return usersSearchSchema.parse({
    keyword: searchParams.get('keyword') ?? undefined,
    status: searchParams.get('status') ?? undefined,
    page: searchParams.get('page') ?? undefined,
  })
}

leaf 컴포넌트는 URLSearchParams를 직접 파싱하지 않고, 검증된 값을 props나 route hook으로 받도록 리팩토링합니다.

TanStack Router 리팩토링 예시

export const Route = createFileRoute('/admin/users')({
  validateSearch: usersSearchSchema,
  loaderDeps: ({ search }) => ({
    keyword: search.keyword,
    status: search.status,
    page: search.page,
  }),
  loader: ({ deps, context }) => {
    return context.queryClient.fetchQuery(usersQueryOptions(deps))
  },
  component: UsersPage,
})

function UsersPage() {
  const search = Route.useSearch()
  const navigate = Route.useNavigate()
  const form = useForm({ defaultValues: search })

  const onSubmit = form.handleSubmit((values) => {
    navigate({ search: (prev) => ({ ...prev, ...values, page: 1 }) })
  })

  return <UsersSearchForm form={form} onSubmit={onSubmit} />
}

핵심은 validateSearch → loaderDeps → queryKey/server fetch 흐름을 단방향으로 만드는 것입니다.

TanStack Query queryKey 연결

search params가 서버 요청 조건이면 queryKey에도 반드시 포함합니다.

function usersQueryOptions(search: UsersSearch) {
  return queryOptions({
    queryKey: ['users', search],
    queryFn: () => fetchUsers(search),
  })
}

잘못된 예:

const usersQuery = useQuery({
  queryKey: ['users'],
  queryFn: () => fetchUsers({ status, page }),
})

status, page가 바뀌어도 queryKey가 같으면 캐시와 refetch 흐름을 추론하기 어렵습니다.

mutation 이후 재검증

| API | 역할 | |-----|------| | fetchQuery | stale 여부를 고려해 fetch하고 결과를 반환합니다. 최신 fetch가 중요한 loader 흐름에 적합합니다. | | ensureQueryData | 캐시가 있으면 우선 반환하고, 없으면 fetch합니다. stale 데이터를 즉시 반환할 수 있음을 고려합니다. | | invalidateQueries | query를 stale로 표시하고 활성 query를 background refetch하게 합니다. | | router.invalidate / useRevalidator | 라우트 loader 데이터를 다시 검증합니다. |

mutation 후에는 query cache만 invalidate할지, route loader까지 revalidate할지 명확히 정합니다.

canonical URL 정규화

내부 fallback이 URL과 다른 숨은 상태를 만들면 안 됩니다.

/users

내부에서만 page = 1, keyword = ''로 취급하면 실제 URL과 앱 상태가 다릅니다. URL을 SSOT로 삼는 화면에서는 다음 중 하나를 표준으로 정합니다.

  • 기본값을 URL에서 생략하는 것을 표준으로 삼기
  • /users?page=1&keyword=처럼 명시하는 것을 표준으로 삼고 route boundary에서 redirect하기

중요한 것은 같은 화면 상태가 여러 URL로 표현되지 않게 하는 것입니다.

피해야 할 패턴

  • 컴포넌트마다 search params parsing과 defaulting을 반복
  • URL state를 zustand/context/local state에 복사해 또 다른 truth로 관리
  • form watch 값을 submit 전부터 서버 queryKey로 사용
  • search params 변경 없이 수동 refetch로 화면 조건 변경
  • loader에서 URLSearchParams를 임의로 읽고 loaderDeps에는 누락

장점:

  • 새로고침해도 상태 유지
  • 뒤로 가기 동작 지원
  • URL 공유와 QA 재현 가능
  • query cache와 loader dependency를 명확하게 추적 가능

2. 불필요한 래퍼 함수 제거 / 중복 핸들러 통합

2-1. 불필요한 래퍼 함수 제거

Before:

const handleRemove = (id: string) => {
  removeItem(id);
};

// JSX
<Button onClick={() => handleRemove(item.id)} />

After:

// JSX
<Button onClick={() => removeItem(item.id)} />

2-2. 중복 핸들러 통합

동일한 로직을 가진 핸들러 함수들을 하나로 통합합니다.

Before:

const handleGridChange = (optionId: number, label: string) => {
  setSelectedOptions(prev => {
    const next = new Map(prev);
    next.set(optionId, [label]);
    return next;
  });
};

const handleSelectChange = (optionId: number, label: string) => {
  setSelectedOptions(prev => {
    const next = new Map(prev);
    next.set(optionId, [label]);
    return next;
  });
};

After:

// 동일한 로직이므로 하나로 통합, 의미를 명확히 하는 네이밍 사용
const handleSingleOptionChange = (optionId: number, label: string) => {
  setSelectedOptions(prev => {
    const next = new Map(prev);
    next.set(optionId, [label]);
    return next;
  });
};

장점:

  • 코드 중복 제거
  • 유지보수 용이성 향상
  • 로직 변경 시 한 곳만 수정

3. setState 내부 side effect 분리

React의 setState updater 함수는 순수해야 합니다. side effect(Toast, API 호출 등)는 외부로 분리합니다.

문제점:

  • React가 StrictMode나 Concurrent 기능에서 updater를 여러 번 호출할 수 있음
  • 토스트가 여러 번 표시되는 등의 버그 발생 가능

Before:

const handleListToggle = (optionId: number, label: string, maxCount: number) => {
  setSelectedOptions(prev => {
    const current = prev.get(optionId) || [];

    if (current.includes(label)) {
      // 제거 로직...
    } else {
      if (current.length >= maxCount) {
        // ⚠️ setState 내부에서 side effect
        overlay.open(({ isOpen, close }) => (
          <Toast isOpen={isOpen} close={close} type="warn" message="최대 선택 갯수입니다" />
        ));
        return prev;
      }
      // 추가 로직...
    }
    return next;
  });
};

After:

const handleListToggle = (optionId: number, label: string, maxCount: number) => {
  const current = selectedOptions.get(optionId) || [];
  const isSelected = current.includes(label);

  // 1. 먼저 조건 체크 및 side effect 처리 (외부에서)
  if (!isSelected && current.length >= maxCount) {
    overlay.open(({ isOpen, close }) => (
      <Toast isOpen={isOpen} close={close} type="warn" message="최대 선택 갯수입니다" />
    ));
    return;
  }

  // 2. 순수한 상태 업데이트만 수행
  setSelectedOptions(prev => {
    const next = new Map(prev);
    const prevCurrent = prev.get(optionId) || [];

    if (prevCurrent.includes(label)) {
      next.set(optionId, prevCurrent.filter(l => l !== label));
    } else if (prevCurrent.length < maxCount) {
      next.set(optionId, [...prevCurrent, label]);
    }
    return next;
  });
};

장점:

  • React 권장 패턴 준수 (updater 순수성)
  • StrictMode/Concurrent 환경에서 안전
  • side effect 중복 실행 방지

4. 중복 로직 useMemo 추출

여러 곳에서 동일한 계산 로직이 반복되면 useMemo로 추출하여 재사용합니다.

Before:

// totalPrice 계산에서
const totalPrice = useMemo(() => {
  const orderOptions = Array.from(selectedOptions.entries())
    .filter(([, labels]) => labels.length > 0)
    .map(([optionId, labels]) => ({ optionId, labels }));

  return calculateTotalPrice(orderOptions);
}, [selectedOptions]);

// handleAddToCart에서 동일 로직 반복
const handleAddToCart = () => {
  const orderOptions = Array.from(selectedOptions.entries())
    .filter(([, labels]) => labels.length > 0)
    .map(([optionId, labels]) => ({ optionId, labels }));

  addItem({ options: orderOptions, ... });
};

After:

// 별도 useMemo로 추출
const orderOptions = useMemo(
  () =>
    Array.from(selectedOptions.entries())
      .filter(([, labels]) => labels.length > 0)
      .map(([optionId, labels]) => ({ optionId, labels })),
  [selectedOptions]
);

// 재사용
const totalPrice = useMemo(() => {
  return calculateTotalPrice(orderOptions);
}, [orderOptions]);

const handleAddToCart = () => {
  addItem({ options: orderOptions, ... });
};

장점:

  • 코드 중복 제거
  • 계산 결과 캐싱으로 성능 최적화
  • 로직 변경 시 한 곳만 수정

5. 타입 가드 함수 적용

반복되는 타입 캐스팅(as)을 타입 가드 함수로 개선합니다.

Before:

// 여러 곳에서 반복되는 타입 캐스팅
if (option.type === 'list') {
  const listOpt = option as ListOption;
  if (selected.length < listOpt.minCount) { ... }
}

// JSX에서도 반복
{option.type === 'list' && (
  <ListOptionRenderer
    option={option as ListOption}
    onToggle={() => handleToggle((option as ListOption).maxCount)}
  />
)}

After:

// 타입 가드 함수 정의
const isListOption = (option: MenuOption): option is ListOption => option.type === 'list';
const isGridOption = (option: MenuOption): option is GridOption => option.type === 'grid';
const isSelectOption = (option: MenuOption): option is SelectOption => option.type === 'select';

// 사용 - TypeScript가 자동으로 타입 추론
if (isListOption(option)) {
  if (selected.length < option.minCount) { ... }  // option이 ListOption으로 추론됨
}

// JSX에서도 깔끔하게
{isListOption(option) && (
  <ListOptionRenderer
    option={option}  // 이미 ListOption 타입
    onToggle={() => handleToggle(option.maxCount)}
  />
)}

장점:

  • 명시적 타입 캐스팅(as) 제거로 타입 안전성 향상
  • TypeScript 자동 타입 추론 활용
  • 코드 가독성 향상
  • 타입 체크 로직 재사용

6. Zustand 셀렉터 최적화

Before:

const addItem = useCartStore(state => state.addItem);
const removeItem = useCartStore(state => state.removeItem);

After (store 파일):

// 액션 직접 export (구독 불필요)
export const cartActions = {
  addItem: (item: Omit<CartItem, 'id'>) => useCartStore.getState().addItem(item),
  removeItem: (id: string) => useCartStore.getState().removeItem(id),
};

After (컴포넌트):

import { cartActions } from '../stores/useCartStore';

// 직접 사용
cartActions.removeItem(item.id);

장점:

  • 액션은 상태가 아니므로 불필요한 리렌더링 방지
  • 컴포넌트 외부에서도 사용 가능

7. 유틸리티 함수 추출

Before:

// 여러 파일에서 반복
overlay.open(({ isOpen, close }) => (
  <Toast isOpen={isOpen} close={close} type="warn" message={error} delay={1500} />
));

After (utils/toast.tsx):

import { overlay } from 'overlay-kit';
import { Toast } from 'tosslib';

type ToastType = 'success' | 'warn' | 'info';

interface ShowToastOptions {
  type?: ToastType;
  delay?: number;
}

export function showToast(message: string, options: ShowToastOptions = {}) {
  const { type = 'warn', delay = 1500 } = options;
  return overlay.open(({ isOpen, close }) => (
    <Toast isOpen={isOpen} close={close} type={type} message={message} delay={delay} />
  ));
}

사용:

import { showToast } from '../utils/toast';

showToast('오류가 발생했습니다');
showToast('성공!', { type: 'success' });

8. 조건부 비활성화 처리

Before:

<FixedBottomCTA onClick={handleCheckout} disabled={isPending}>
  결제하기
</FixedBottomCTA>

After:

<FixedBottomCTA onClick={handleCheckout} disabled={isPending || items.length === 0}>
  결제하기
</FixedBottomCTA>

9. 성공 후 정리 로직

Before:

createOrder(orderRequest, {
  onSuccess: data => {
    navigate(`/order-complete/${data.orderId}`);
  },
});

After:

createOrder(orderRequest, {
  onSuccess: data => {
    cartActions.clearCart(); // 장바구니 비우기
    navigate(`/order-complete/${data.orderId}`);
  },
});

10. 컴포넌트 분리

Before:

function CartPage() {
  // ... 많은 로직

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          <ListRow contents={...} />
          <NumericSpinner ... />
        </div>
      ))}
    </div>
  );
}

After:

function CartPage() {
  // ... 페이지 로직만

  return (
    <div>
      {items.map(item => (
        <CartItemRow key={item.id} item={item} />
      ))}
    </div>
  );
}

interface CartItemRowProps {
  item: CartItem;
}

function CartItemRow({ item }: CartItemRowProps) {
  return (
    <div>
      <ListRow contents={...} />
      <NumericSpinner ... />
    </div>
  );
}

11. 타입 개선

Before:

function handleChange(value: any) {
  setValue(value);
}

After:

function handleChange(value: string) {
  setValue(value);
}

12. 불필요한 코멘트 제거

코드 자체로 의도가 명확한 경우 코멘트는 오히려 노이즈가 됩니다.

제거 대상 코멘트

1. 함수명/변수명으로 이미 충분한 경우:

// Before
// 역 선택 핸들러
const handleStationSelect = (station: Station) => { ... };

// 뒤로가기 핸들러
const handleBack = () => { ... };

// After
const handleStationSelect = (station: Station) => { ... };
const handleBack = () => { ... };

2. 코드 구조로 명확한 경우:

// Before
interface SearchState {
  // State
  departureStation: string | null;
  arrivalStation: string | null;

  // Actions Namespace
  actions: { ... };
}

// After
interface SearchState {
  departureStation: string | null;
  arrivalStation: string | null;

  actions: { ... };
}

3. 섹션 레이블:

// Before
// Types
export const TRIP_TYPE = { ... };

// Type Guard
export function isTripType(value: string): value is TripType { ... }

// 직접 export된 Actions (hook 없이 사용 가능)
export const searchActions = { ... };

// After
export const TRIP_TYPE = { ... };

export function isTripType(value: string): value is TripType { ... }

export const searchActions = { ... };

4. 제거된 코드에 대한 설명:

// Before
// Note: useSearchActions는 제거됨.
// 액션 사용은 stores에서 직접 export된 searchActions를 사용하세요.

// After
// (코멘트 자체를 삭제)

유지해야 할 코멘트

1. 타입 힌트:

const stationType = searchParams.get('type'); // 'departure' | 'arrival'

2. 긴 JSX 섹션 구분 (선택적):

{/* 출발역 선택 */}
<ListRow ... />

{/* 가는 날 선택 */}
<ListRow ... />

3. 비즈니스 로직의 "왜":

// 편도로 전환 시 오는 날 초기화 (사용자가 왕복 → 편도로 변경하면 오는 날 선택이 의미 없음)
returnDate: tripType === TRIP_TYPE.ONE_WAY ? null : state.returnDate,

4. TODO, FIXME:

// TODO: API 응답 형식 변경 후 수정 필요
// FIXME: 엣지 케이스 처리 필요

코멘트 필요성 판단 기준

| 질문 | Yes → 유지 | No → 제거 | |-----|-----------|----------| | 코드만 읽고 의도를 파악하기 어려운가? | ✅ | ❌ | | 비즈니스 규칙이나 "왜"를 설명하는가? | ✅ | ❌ | | 외부 시스템/API와의 관계를 설명하는가? | ✅ | ❌ | | 함수명/변수명이 이미 동일한 내용을 전달하는가? | ❌ | ✅ | | 코드 구조로 이미 명확한가? | ❌ | ✅ |


14. 코드 스페이싱 정리

JS 코드에서 불필요한 줄 바꿈을 정리합니다.

허용되는 빈 줄: 용도별 그룹화 (훅 → 파생 상태 → 핸들러)

제거해야 할 빈 줄:

  • 객체 리터럴 내부 프로퍼티 사이
  • JSX return문 내부 요소 사이

Before:

export const useStore = create(set => ({
  ...initialState,

  actions: {
    doSomething: () => set({ ... }),

    doAnotherThing: () => set({ ... }),
  },
}));

// JSX도 마찬가지
return (
  <>
    <Header />

    <Content />

    <Footer />
  </>
);

After:

export const useStore = create(set => ({
  ...initialState,
  actions: {
    doSomething: () => set({ ... }),
    doAnotherThing: () => set({ ... }),
  },
}));

// JSX도 연속으로
return (
  <>
    <Header />
    <Content />
    <Footer />
  </>
);

장점:

  • 불필요한 시각적 노이즈 제거
  • 객체/JSX는 이미 구조적으로 구분되어 있음
  • 코드가 더 compact하고 읽기 쉬워짐

주의사항

  1. 점진적 변경: 한 번에 모든 것을 바꾸지 말고, 각 패턴별로 순차적으로 진행
  2. 커밋 단위: 각 리팩토링 패턴마다 별도 커밋 생성
  3. 테스트 확인: 변경 후 기능이 정상 동작하는지 확인
  4. 기존 패턴 존중: 프로젝트의 기존 코드 스타일과 패턴을 따름
  5. 과도한 추상화 금지: 한 번만 사용되는 코드는 추출하지 않음