리팩토링 스킬은(는) 무엇을 하나요?
이 스킬은 정립된 패턴에 맞게 기능을 리팩토링하고, 중복을 제거하며, 코드 재사용성을 개선할 때 사용하세요.
UI가 패턴 라이브러리에 속해야 하는지 로컬 구성에 속해야 하는지에 대한 판단은 design-system 스킬을 사용하세요.
리팩토링 전 분석
1. 유사 기능 식별
- 코드베이스에서 비슷한 기능을 가진 유사한 기능을 찾습니다.
- 디렉터리 구조, 컴포넌트 패턴, 유틸리티 사용법을 비교합니다.
- 기능은 비슷하지만 구현이 다른 기능을 찾습니다.
- 시맨틱 검색을 사용하여 관련 기능을 찾습니다: "유사한 테이블/데이터 그리드 패턴을 가진 기능"
2. 중복 기회 찾기
중복 코드 패턴을 검색합니다:
- 컴포넌트: 여러 기능에서 유사한 컴포넌트 파일을 찾습니다 (동일한 이름 또는 유사한 기능).
- 훅: 중복된 훅 로직을 확인합니다 (테이블 훅, 필터 훅, 집계 훅).
- 유틸리티: 중복된 유틸리티 함수를 찾습니다 (집계, 포맷터, 유효성 검사기, 변환기).
- 스타일: 중복된
sxprop 패턴 또는 인라인 스타일을 식별합니다. - 상수: 중복된 상수 또는 설정 객체를 확인합니다.
- 타입: 중복된 타입 정의 또는 인터페이스를 찾습니다.
3. 공유 유틸리티 확인
새로운 유틸리티를 만들기 전에 기존 유틸리티를 확인하세요:
@/util/디렉터리에서 도메인별 유틸리티를 검색합니다.@/util/dates에서 날짜 포맷팅 함수를 확인합니다.@/util/numbers에서 숫자 포맷팅 함수를 확인합니다.@/components/에서 재사용 가능한 컴포넌트를 확인합니다.pattern-library에서 디자인 시스템 컴포넌트를 확인합니다.- 새로 만들기 전에 grep/검색을 사용하여 기존 구현을 찾습니다.
리팩토링 프로세스
1단계: 공유 코드 추출
중복을 발견했을 때:
-
적절한
@/util/디렉터리에 공유 유틸리티를 생성합니다:// 예시: util/[domain]/[category].ts export const sharedFunction = () => { ... }; export const sharedConstant = { ... }; -
모든 기능이 공유 위치에서 가져오도록 업데이트합니다:
// 기능별 파일 - 공유 항목 다시 내보내기 export { sharedFunction, sharedConstant } from "@/util/[domain]/[category]"; -
기능 디렉터리에서 중복 구현을 제거합니다.
-
코드베이스 전체의 모든 import를 업데이트합니다.
2단계: 컴포넌트 통합
중복 컴포넌트를 발견했을 때:
- 가장 완전하고 기능이 풍부한 버전을 식별합니다 (props, 기능, 테스트 확인).
- 공유 위치(
@/components/또는 기능에 종속되지 않은 위치)로 추출합니다. - 모든 사용 사례를 지원하도록 props를 유연하게 만듭니다 (유니온 타입, 선택적 props, 제네릭 사용).
- 해당 컴포넌트를 사용하는 모든 기능의 import를 업데이트합니다.
- 중복 컴포넌트 파일을 삭제합니다.
- 모든 사용법이 여전히 올바르게 작동하는지 확인합니다.
3단계: 로딩 패턴 정렬
로딩 상태의 경우:
loading.tsx파일을 확인합니다 - 단순한 스켈레톤이어야 하며, 전체 페이지 레이아웃이 아니어야 합니다.- 컴포넌트 수준의 스켈레톤 로더를 제거합니다 - 초기 로드는 페이지 수준의
loading.tsx에 의존합니다. - 리페치 중에는: 스켈레톤 대신 기존 데이터를 표시합니다 (UX에 명시적으로 필요한 경우는 제외).
- 코드베이스의 유사한 기능에서 패턴을 매칭합니다.
- Next.js App Router 규칙을 사용합니다 -
loading.tsx는 초기 로드를 처리하고, 컴포넌트는 리페치 상태를 처리합니다.
4단계: 스타일 통합
스타일링의 경우:
- 재사용 가능한 스타일 패턴을 공유 컴포넌트 또는 스타일 유틸리티로 추출합니다.
- 공유 컴포넌트가 있는 경우 인라인
sxprops를 제거합니다. - 커스텀 스타일 대신
pattern-library의 디자인 시스템 컴포넌트를 사용합니다. - 공유 유틸리티나 테마 확장이 될 수 있는 중복된 스타일 객체가 있는지 확인합니다.
- Material-UI 패턴을 따릅니다 - 테마 값을 사용하고, 매직 넘버를 피합니다.
5단계: 패턴 정렬 확인
정립된 기능과의 일관성 보장:
- 디렉터리 구조를 비교합니다 - 유사한 패턴을 따라야 합니다 (
_components/,_hooks/,_utils/). - 훅 패턴을 확인합니다 - 기능 요구 사항이 다르면 더 간단한 훅도 괜찮습니다.
- 데이터 흐름을 확인합니다 - 가능하면 유사한 기능과 일치해야 합니다.
- 컴포넌트 구성을 검토합니다 - 유사한 패턴을 사용해야 합니다 (래퍼, 구성 등).
- 명명 규칙을 확인합니다 - 정립된 패턴을 따릅니다 (컴포넌트는 PascalCase, 훅은 camelCase).
6단계: 유틸리티 함수 통합
유틸리티 함수의 경우:
- 함수가 이미 공유 유틸리티에 존재하는지 확인합니다 (코드베이스 검색).
- 구현을 비교합니다 - 더 완전하거나 견고한 버전을 사용합니다.
- 중복을 통합합니다 - 공유 위치로 이동하고, 모든 import를 업데이트합니다.
- 차이점을 문서화합니다 - 구현이 의도적으로 다른 경우 그 이유를 문서화합니다.
- 기능을 유지합니다 - 통합된 버전이 모든 사용 사례를 처리하는지 확인합니다.
7단계: 주석 정리
리팩토링 후 불필요한 주석을 제거합니다:
- 마이그레이션 노트를 제거합니다 - 코드가 어디서 이동되었는지 설명하는 주석 (예: "isCollecting이 Table/utils.ts에서 내보내집니다").
- 자명한 주석을 제거합니다 - 코드가 하는 일을 다시 설명하는 주석 (예: 코드가 재내보내기를 명확히 보여주는데 "// 공유 집계 재내보내기").
- 설명 주석은 유지합니다 - 왜 어떤 작업을 하는지 설명하는 주석 (예: "// setTimeout을 사용하여 폼 상태가 업데이트되었는지 확인").
- 복잡한 로직 주석은 유지합니다 - 명확하지 않은 비즈니스 로직이나 엣지 케이스를 설명하는 주석.
- 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를 사용하지 마세요. TypographyAPI: 의미를 표현하는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 우선 레이아웃
- 레이아웃 및 스타일링에는 항상
sxprop을 선호합니다. 더 이상 사용되지 않는 MUI 시스템 스타일 props (display,alignItems,justifyContent등)를Box및Stack의 최상위 props로 사용하지 마세요. - 단축 vs 명시적 속성:
sx객체 내에서 MUI 단축 속성을 사용하는 것은 허용되지만, 대부분의 경우 명시적 속성이 더 읽기 쉽습니다. - 스택 방향:
Stack에서directionprop을sx와 함께 사용하는 것은 정립된 패턴이며 유지되어야 합니다.
뷰 모델 vs 오버라이드 Prop
- 파생된 프레젠테이션 상태에는 뷰 모델 패턴을 사용합니다. 뷰 모델은 원시 데이터를 프레젠테이션에 적합한 형태로 변환합니다.
- 오버라이드 props는 신중하게 사용합니다. prop을 오버라이드 가능하게 만드는 것은 컴포넌트 계약을 약화시킵니다. 특정 사용 사례에서만 문서화된 이유로 오버라이드를 허용합니다.
- 타입 안전성 유지: 뷰 모델을 사용할 때 타입 추론이 손실되지 않도록 합니다. 제네릭을 사용하여 유연성과 타입 안전성의 균형을 맞춥니다.