Communitygithub.com

caseycme/리팩토링 스킬

정립된 패턴에 맞게 기능을 리팩토링하고, 중복을 제거하며, 코드 재사용성을 개선하는 방법을 안내합니다. DataGrid/패턴 라이브러리, 타입, fetch 페이지네이션, view-model과 override-prop 지침, MUI sx 우선 레이아웃(더 이상 사용되지 않는 Box 스타일 단축 props 사용 금지)을 포함합니다. 기능 리팩토링, 중복 코드 통합, 공유 유틸리티 추출, 유사 기능 간 패턴 정렬 시 사용하세요.

리팩토링 스킬란 무엇인가요?

이 스킬은 코드베이스에서 정립된 패턴에 맞춰 기능을 리팩토링하고 중복을 제거하며 코드 재사용성을 높이기 위한 체계적인 가이드를 제공합니다. 개발자가 유사한 기능을 식별하고, 중복 코드를 찾아내며, 공유 컴포넌트와 유틸리티를 추출하는 사전 분석 단계를 거치도록 안내합니다. 주요 리팩토링 단계로는 공유 코드 추출, 컴포넌트 통합, 로딩 패턴 정렬, 스타일 통합, 패턴 정렬 확인, 유틸리티 함수 통합 등이 포함됩니다. 이 스킬은 Claude Code, Cursor, Codex 등 다양한 AI 에이전트 플랫폼에서 사용할 수 있으며, 주로 TypeScript와 React 기반의 MUI(데이터 그리드, 패턴 라이브러리)를 사용하는 프론트엔드 프로젝트에 적합합니다. 또한 코드 리뷰에서 반복적으로 지적되는 세부 사항(DataGrid 토글 패널, 타입 안전성, Typography 사용, 제어 흐름 최적화, API 페치 및 페이지네이션 패턴)을 적절히 처리하여 코드 품질을 일관되게 유지하도록 돕습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/caseycme/skills/tree/HEAD/skills/engineering/refactoring

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

리팩토링 스킬은(는) 무엇을 하나요?

이 스킬은 정립된 패턴에 맞게 기능을 리팩토링하고, 중복을 제거하며, 코드 재사용성을 개선할 때 사용하세요.

UI가 패턴 라이브러리에 속해야 하는지 로컬 구성에 속해야 하는지에 대한 판단은 design-system 스킬을 사용하세요.

리팩토링 전 분석

1. 유사 기능 식별

  • 코드베이스에서 비슷한 기능을 가진 유사한 기능을 찾습니다.
  • 디렉터리 구조, 컴포넌트 패턴, 유틸리티 사용법을 비교합니다.
  • 기능은 비슷하지만 구현이 다른 기능을 찾습니다.
  • 시맨틱 검색을 사용하여 관련 기능을 찾습니다: "유사한 테이블/데이터 그리드 패턴을 가진 기능"

2. 중복 기회 찾기

중복 코드 패턴을 검색합니다:

  • 컴포넌트: 여러 기능에서 유사한 컴포넌트 파일을 찾습니다 (동일한 이름 또는 유사한 기능).
  • 훅: 중복된 훅 로직을 확인합니다 (테이블 훅, 필터 훅, 집계 훅).
  • 유틸리티: 중복된 유틸리티 함수를 찾습니다 (집계, 포맷터, 유효성 검사기, 변환기).
  • 스타일: 중복된 sx prop 패턴 또는 인라인 스타일을 식별합니다.
  • 상수: 중복된 상수 또는 설정 객체를 확인합니다.
  • 타입: 중복된 타입 정의 또는 인터페이스를 찾습니다.

3. 공유 유틸리티 확인

새로운 유틸리티를 만들기 전에 기존 유틸리티를 확인하세요:

  • @/util/ 디렉터리에서 도메인별 유틸리티를 검색합니다.
  • @/util/dates에서 날짜 포맷팅 함수를 확인합니다.
  • @/util/numbers에서 숫자 포맷팅 함수를 확인합니다.
  • @/components/에서 재사용 가능한 컴포넌트를 확인합니다.
  • pattern-library에서 디자인 시스템 컴포넌트를 확인합니다.
  • 새로 만들기 전에 grep/검색을 사용하여 기존 구현을 찾습니다.

리팩토링 프로세스

1단계: 공유 코드 추출

중복을 발견했을 때:

  1. 적절한 @/util/ 디렉터리에 공유 유틸리티를 생성합니다:

    // 예시: util/[domain]/[category].ts
    export const sharedFunction = () => { ... };
    export const sharedConstant = { ... };
    
  2. 모든 기능이 공유 위치에서 가져오도록 업데이트합니다:

    // 기능별 파일 - 공유 항목 다시 내보내기
    export { sharedFunction, sharedConstant } from "@/util/[domain]/[category]";
    
  3. 기능 디렉터리에서 중복 구현을 제거합니다.

  4. 코드베이스 전체의 모든 import를 업데이트합니다.

2단계: 컴포넌트 통합

중복 컴포넌트를 발견했을 때:

  1. 가장 완전하고 기능이 풍부한 버전을 식별합니다 (props, 기능, 테스트 확인).
  2. 공유 위치(@/components/ 또는 기능에 종속되지 않은 위치)로 추출합니다.
  3. 모든 사용 사례를 지원하도록 props를 유연하게 만듭니다 (유니온 타입, 선택적 props, 제네릭 사용).
  4. 해당 컴포넌트를 사용하는 모든 기능의 import를 업데이트합니다.
  5. 중복 컴포넌트 파일을 삭제합니다.
  6. 모든 사용법이 여전히 올바르게 작동하는지 확인합니다.

3단계: 로딩 패턴 정렬

로딩 상태의 경우:

  1. loading.tsx 파일을 확인합니다 - 단순한 스켈레톤이어야 하며, 전체 페이지 레이아웃이 아니어야 합니다.
  2. 컴포넌트 수준의 스켈레톤 로더를 제거합니다 - 초기 로드는 페이지 수준의 loading.tsx에 의존합니다.
  3. 리페치 중에는: 스켈레톤 대신 기존 데이터를 표시합니다 (UX에 명시적으로 필요한 경우는 제외).
  4. 코드베이스의 유사한 기능에서 패턴을 매칭합니다.
  5. Next.js App Router 규칙을 사용합니다 - loading.tsx는 초기 로드를 처리하고, 컴포넌트는 리페치 상태를 처리합니다.

4단계: 스타일 통합

스타일링의 경우:

  1. 재사용 가능한 스타일 패턴을 공유 컴포넌트 또는 스타일 유틸리티로 추출합니다.
  2. 공유 컴포넌트가 있는 경우 인라인 sx props를 제거합니다.
  3. 커스텀 스타일 대신 pattern-library의 디자인 시스템 컴포넌트를 사용합니다.
  4. 공유 유틸리티나 테마 확장이 될 수 있는 중복된 스타일 객체가 있는지 확인합니다.
  5. Material-UI 패턴을 따릅니다 - 테마 값을 사용하고, 매직 넘버를 피합니다.

5단계: 패턴 정렬 확인

정립된 기능과의 일관성 보장:

  1. 디렉터리 구조를 비교합니다 - 유사한 패턴을 따라야 합니다 (_components/, _hooks/, _utils/).
  2. 훅 패턴을 확인합니다 - 기능 요구 사항이 다르면 더 간단한 훅도 괜찮습니다.
  3. 데이터 흐름을 확인합니다 - 가능하면 유사한 기능과 일치해야 합니다.
  4. 컴포넌트 구성을 검토합니다 - 유사한 패턴을 사용해야 합니다 (래퍼, 구성 등).
  5. 명명 규칙을 확인합니다 - 정립된 패턴을 따릅니다 (컴포넌트는 PascalCase, 훅은 camelCase).

6단계: 유틸리티 함수 통합

유틸리티 함수의 경우:

  1. 함수가 이미 공유 유틸리티에 존재하는지 확인합니다 (코드베이스 검색).
  2. 구현을 비교합니다 - 더 완전하거나 견고한 버전을 사용합니다.
  3. 중복을 통합합니다 - 공유 위치로 이동하고, 모든 import를 업데이트합니다.
  4. 차이점을 문서화합니다 - 구현이 의도적으로 다른 경우 그 이유를 문서화합니다.
  5. 기능을 유지합니다 - 통합된 버전이 모든 사용 사례를 처리하는지 확인합니다.

7단계: 주석 정리

리팩토링 후 불필요한 주석을 제거합니다:

  1. 마이그레이션 노트를 제거합니다 - 코드가 어디서 이동되었는지 설명하는 주석 (예: "isCollecting이 Table/utils.ts에서 내보내집니다").
  2. 자명한 주석을 제거합니다 - 코드가 하는 일을 다시 설명하는 주석 (예: 코드가 재내보내기를 명확히 보여주는데 "// 공유 집계 재내보내기").
  3. 설명 주석은 유지합니다 - 왜 어떤 작업을 하는지 설명하는 주석 (예: "// setTimeout을 사용하여 폼 상태가 업데이트되었는지 확인").
  4. 복잡한 로직 주석은 유지합니다 - 명확하지 않은 비즈니스 로직이나 엣지 케이스를 설명하는 주석.
  5. TODO/FIXME 주석은 제거합니다 - 정당한 향후 작업 항목이 아닌 경우.

불필요한 주석 예시:

// ❌ 불필요 - 코드에서 명확함
// util에서 공유 집계 재내보내기
export { latest, totalRange } from "@/util/keywords/gridAggregations";

// ❌ 불필요 - 마이그레이션 노트, 더 이상 필요 없음
// isCollecting은 이제 Table/utils.ts에서 중복을 피하기 위해 내보내집니다
// 거기서 가져오세요: import { isCollecting } from "../KeywordListDetail/Table/utils";

좋은 주석 예시:

// ✅ 좋은 주석 - WHY를 설명
// CSV 처리 후 폼 상태가 업데이트되었는지 확인하기 위해 setTimeout 사용
setTimeout(() => {
  void handleSubmit(onSubmit)();
}, 100);

// ✅ 좋은 주석 - 복잡한 로직 설명
// 키워드는 다음과 같은 경우 업데이트됩니다:
// 1. lastUpdated 값이 변경된 경우 (가장 신뢰할 수 있는 지표), 또는
// 2. 더 이상 수집 중 상태가 아니고 충분히 폴링한 경우 (대체 방법)

8단계: 코드 품질 개선

리팩토링된 코드 전체에 일관된 코드 품질 패턴을 적용합니다:

Null/Undefined 확인

느슨한 동등 연산자를 사용하여 null 및 undefined 확인을 간소화합니다:

// ❌ 장황함
if (value !== undefined && value !== null) { ... }
if (value !== undefined && value !== null && value !== "") { ... }

// ✅ 간소화됨
if (value != null) { ... }
if (value != null && value !== "") { ... }

참고: != null (느슨한 동등)을 사용하여 null과 undefined를 한 번에 확인합니다. 이는 관용적인 TypeScript/JavaScript이며 더 간결합니다.

타입 전용 가져오기

타입 전용 가져오기에는 import type을 사용하여 빌드 성능을 개선하고 의도를 명확히 합니다:

// ❌ 타입 전용 사용에 런타임 import 사용
import { SomeType } from "app-api/types/domain";

// ✅ 타입 전용 import
import type { SomeType } from "app-api/types/domain";

적용 대상:

  • app-api의 타입 정의
  • 인터페이스 import
  • 타입 전용 유틸리티 타입
  • 타입 어노테이션에만 사용되는 모든 import

불필요한 타입 단언 제거

TypeScript가 타입을 올바르게 추론할 수 있는 경우 타입 단언을 제거합니다:

// ❌ 불필요한 단언
const items = data.map((item) => item as SomeType);

// ✅ TypeScript가 추론하도록 함
const items = data.map((item) => item);

단언을 유지해야 하는 경우: TypeScript가 타입을 정말로 추론할 수 없거나, TypeScript가 확인할 수 없는 방식으로 타입을 좁히는 경우에만 유지합니다.

불필요한 Nullish 병합 제거

값이 이미 nullable이거나 TypeScript가 올바른 타입을 추론하는 경우 ?? null을 제거합니다:

// ❌ 불필요 - valueGetter가 이미 nullable 반환
valueGetter: (value, row) => {
  return row.someField ?? null;
};

// ✅ 간소화됨
valueGetter: (value, row) => {
  return row.someField;
};

유지해야 하는 경우: API 호환성을 위해 undefined를 null로 명시적으로 변환하거나, 타입 시스템이 요구하는 경우에 유지합니다.

사용되지 않는 Props 제거

컴포넌트 인터페이스와 구현에서 사용되지 않는 props를 제거합니다:

// ❌ 사용되지 않는 prop
interface ComponentProps {
  item: Item;
  unusedField: string; // 컴포넌트에서 사용되지 않음
}

// ✅ 깔끔한 인터페이스
interface ComponentProps {
  item: Item;
}

콜백에 명시적 타입 추가

TypeScript가 타입을 추론할 수 없는 경우 콜백 매개변수에 명시적 타입 어노테이션을 추가합니다:

// ❌ 암시적 any
const items = ids.map((id) => { ... });

// ✅ 명시적 타입
const items = ids.map((id: string | number) => { ... });

적용 대상: 배열 메서드 (map, filter, forEach), 이벤트 핸들러 및 컨텍스트에서 타입이 명확하지 않은 기타 콜백.

반복되는 JSX 패턴 추출

반복되는 JSX 패턴을 재사용 가능한 컴포넌트로 추출합니다:

// ❌ 반복되는 패턴
{
  isAggregation ? (
    <Typography
      variant="body2"
      fontWeight="medium"
      fontStyle="italic"
      color="text.secondary"
    >
      {formattedValue}
    </Typography>
  ) : (
    <Typography variant="body2" fontWeight="medium">
      {formattedValue}
    </Typography>
  );
}

// ✅ 추출된 컴포넌트
<AggregationTypography
  formattedValue={formattedValue}
  isAggregation={isAggregation}
/>;

다양한 구현을 위한 팩토리 함수 사용

로직이 유사하지만 다른 데이터 구조를 처리해야 하는 경우 팩토리 함수를 사용합니다:

// ✅ 팩토리 함수 패턴
export const createIntentOperators = <T extends { intent?: Intent | Intent[] }>(
  normalizeToArray: (value: T) => Intent[]
) => {
  return {
    // 정규화된 배열로 작업하는 연산자
  };
};

// 다른 기능에서 사용
const operators1 = createIntentOperators<Feature1Type>((item) => [item.intent]);
const operators2 = createIntentOperators<Feature2Type>(
  (item) => item.intents || []
);

이점: 다양한 데이터 구조를 적절히 처리하면서 핵심 로직을 재사용합니다.

코드 리뷰 강화 (반복 피드백)

리팩토링이 그리드, 대시보드, 페치 또는 공유된 "행 + 메트릭" 형태를 건드릴 때 적용합니다 (반복되는 Moz Pro UI PR 피드백과 일치).

DataGrid: 상세 패널 토글

  • 확장/축소를 위한 detail-panel 열에는 pattern-library의 DetailPanelToggle을 선호합니다. 커스텀 셀에서 임시방편 IconButton + @mui/icons를 사용하지 마세요.
  • 접근성 연결: 셀이 탭으로 포커스 가능해야 하는 경우 buttonTabIndex={0}을 전달합니다 (app/(default)/competitive-research/top-competing-content/_module/components/Table/columns.tsx, SerpFeaturesCell 참조).
  • 레이아웃/중앙 정렬: Box/Stack에는 **sx**를 사용합니다 (또는 정립된 패턴인 경우 Stack의 direction/spacing). Box에서 레거시 MUI 시스템 스타일 props (예: display, alignItems를 최상위 props로)로 sx를 대체하지 마세요 — MUI는 해당 접근 방식을 **sx**로 대체하는 방향으로 더 이상 사용하지 않습니다(deprecating).

타입 지정: object / 익명 행 형태 피하기

  • 구조적 타입이 존재하거나 한 번 정의할 수 있는 경우 readonly object[]를 brand_definitions, 행 조각 또는 API 인접 목록에 사용하지 마세요.
  • 공유 "유사" 인터페이스를 정의합니다 (예: 기능 utils 모듈의 BrandDefinitionLike). 내보내고 셀 내부에서 getBrandName을 캐스트와 함께 복제하는 대신 단일 표시줄 도우미 (예: getBrandDefinitionDisplayLine)를 사용합니다.
  • 매개변수에 타입을 지정한 후 중복 캐스트 제거합니다 (as BrandDefinitionLike[]).
  • 형태가 실제 BrandDefinition인 경우 *_module/utils/dataHelpers의 도메인 도우미를 재사용합니다. 프롬프트 행 페이로드가 다른 경우에만 느슨한 도우미를 사용합니다.

Typography vs Box (패턴 라이브러리)

  • 텍스트(빈 상태, 인라인 "—", 강조된 범위)의 경우, 적절할 때 component="span"과 함께 pattern-library의 Typography를 선호합니다 — 복사본으로 가장하는 Box를 사용하지 마세요.
  • Typography API: 의미를 표현하는 variant, color, fontWeight, **component**를 사용합니다 (이것들은 더 이상 사용되지 않습니다).
  • Box / 레이아웃 래퍼: 간격, 플렉스 레이아웃, 크기 조정 및 테마 값에는 **sx**를 선호합니다. 스타일을 **sx**에서 Box "스타일" 단축 props로 이동하는 우발적 리팩토링을 피하세요. 이는 현재 MUI 지침과 충돌합니다.

컴포넌트 경계: props vs 내부 상태

  • 부모가 레이아웃이나 URL 상태에 필요하지 않은 경우 컴포넌트 내부의 data에서 불리언을 파생합니다 (예: 기본 브랜드의 brand_terms에서 "브랜드 용어 있음").
  • UI 전용 토글은 내부적으로 유지합니다 (예: 하나의 카드에서 두 차트 모드 간 전환). 다른 형제 또는 경로가 동일한 상태를 구동해야 하는 경우에만 리프팅이 필요한 이유를 문서화합니다.

제어 흐름 및 JSX

  • 로딩/빈 분기에 대해 큰 삼항 연산자를 단일 return 직전의 변수에 할당하는 대신, 조기 return을 선호합니다. 메모이제이션이 진정으로 하나의 표현식을 필요로 하는 경우는 예외입니다.
  • 정렬 비교기: 가정된 도메인 최대값에 연결된 매직 센티널(예: 5)을 하드코딩하지 마세요. null/undefined에 대해 **Number.POSITIVE_INFINITY / 명시적 "마지막 정렬"**을 사용하여 한도가 변경되어도 동작이 올바르게 유지되도록 합니다.

페치 및 페이지네이션

  • 백엔드가 페이지로 나뉘어 있고 프로젝트가 한 페이지를 초과할 수 있는 경우 대시보드 규모 목록에 대해 고정된 작은 limit (예: 100)을 피하세요.
  • 안정적인 페이지 크기로 반복하여 페이지가 한도보다 적은 행 또는 0행을 반환할 때까지 진행하거나, 액션의 문서화된 계약을 따릅니다 (moz-pro-service 액션 문서 / Bruno에서 최대 limit 참조).
  • 호출자가 개수가 필요한 경우 total 또는 파생된 합계를 일관되게 표시합니다.

프레젠테이션 래퍼: 자명한 것을 반복하지 마세요

  • 부모 Card가 이미 표시 제목을 설정한 경우 중복된 대체 상태 필드를 제거합니다 (예: 카드 제목과 일치하는 cardContentTitle 복제 방지).
  • Null 검사: prop이 string으로 타입 지정된 경우, 수신 측에서 null 검사를 중복으로 수행하지 마세요. 타입 시스템이 이미 보장을 제공합니다.

MUI sx 우선 레이아웃

  • 레이아웃 및 스타일링에는 항상 sx prop을 선호합니다. 더 이상 사용되지 않는 MUI 시스템 스타일 props (display, alignItems, justifyContent 등)를 Box 및 Stack의 최상위 props로 사용하지 마세요.
  • 단축 vs 명시적 속성: sx 객체 내에서 MUI 단축 속성을 사용하는 것은 허용되지만, 대부분의 경우 명시적 속성이 더 읽기 쉽습니다.
  • 스택 방향: Stack에서 direction prop을 sx와 함께 사용하는 것은 정립된 패턴이며 유지되어야 합니다.

뷰 모델 vs 오버라이드 Prop

  • 파생된 프레젠테이션 상태에는 뷰 모델 패턴을 사용합니다. 뷰 모델은 원시 데이터를 프레젠테이션에 적합한 형태로 변환합니다.
  • 오버라이드 props는 신중하게 사용합니다. prop을 오버라이드 가능하게 만드는 것은 컴포넌트 계약을 약화시킵니다. 특정 사용 사례에서만 문서화된 이유로 오버라이드를 허용합니다.
  • 타입 안전성 유지: 뷰 모델을 사용할 때 타입 추론이 손실되지 않도록 합니다. 제네릭을 사용하여 유연성과 타입 안전성의 균형을 맞춥니다.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

관련 스킬