Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
7.9 KiB
FE 기능 모듈 구조 컨벤션 + Subscription 분해 리팩토링 — 설계
- 작성일: 2026-07-09
- 역할/저장소: FE (
web-ui) - 범위: FE 대형 컴포넌트 책임 분해 리팩토링의 1번째 서브프로젝트. (전체 분해: Subscription→RealEstate→PortfolioTab→InstaCards→MusicStudio 순차, 각 별도 spec/plan.)
- 성격: behavior-preserving(동작 보존) 순수 추출. 로직 재작성 금지, UI/동작 변경 없음.
1. 배경 & 목표
기능이 늘며 일부 페이지 컴포넌트가 단일 파일에 과도한 책임을 담게 됨(예: Subscription.jsx 1647줄에 4탭+카드+상세+캘린더+헬퍼 전부). 하네스 엔지니어링 원칙(단일 책임·명확한 경계·CLAUDE.md 계층 문서화)에 맞춰 기능 모듈 구조로 분해하고, 그 컨벤션을 하네스(web-ui/CLAUDE.md)와 README에 기록한다.
이 스펙은 두 부분:
- Part A (공통 컨벤션 + 문서화): "기능 모듈 구조" 표준 정의 → 이후 모든 서브프로젝트가 참조. web-ui/CLAUDE.md + README 기록.
- Part B (Subscription 분해): 컨벤션을
Subscription.jsx에 첫 적용.
비목표: 동작/스타일 변경, 신규 기능, api.js 중복(local apiPatch) 정리 외 무관 리팩토링.
2. Part A — 기능 모듈 구조 컨벤션 (하네스 기록)
표준 디렉토리 형태 (레퍼런스: src/pages/listings/):
src/pages/<feature>/
├── <Feature>.jsx # 페이지 shell — 라우팅 진입, 탭바/헤더/레이아웃, 하위 조합만
├── <Feature>.css # 스타일 (shell에서 1회 import → 전역 클래스 하위 공유)
├── components/ # 표현 단위 (카드/탭/상세/리스트…) 각 단일 책임
├── hooks/ # 상태·API·부수효과 훅 (선택)
└── <feature>Utils.js # 순수 헬퍼(포맷/매핑/계산) + <feature>Utils.test.js
규칙:
- 파일당 단일 책임, 권장 상한 ~300줄(초과 시 분할 신호).
- 순수 로직(포맷/매핑/계산)은
*Utils.js로 뽑고 유닛 테스트 필수. - 상태+API가 얽힌 로직은
hooks/또는 자립형 탭 컴포넌트로. - shell은 "조합"만: 데이터 페칭·비즈니스 로직을 직접 갖지 않음.
- 컴포넌트는 서로 잘 정의된 props/훅 인터페이스로만 통신.
문서화 대상:
web-ui/CLAUDE.md: "기능 모듈 구조" 섹션 신설(위 형태·규칙), 페이지 구조 표에 리팩토링 반영.README.md: 프로젝트 통계/구조 설명에 모듈 구조 컨벤션 1문단 추가.
3. Part B — Subscription.jsx 분해 (file map)
현 src/pages/subscription/Subscription.jsx(1647줄)를 아래로 분해. 코드 이동만(내용 동일), import 배선만 추가.
src/pages/subscription/
├── Subscription.jsx # shell: PullToRefresh + 헤더 + 탭바 + FAB + activeTab/refreshKey. 4탭 조합. (~70줄)
├── Subscription.css # (변경 없음, shell에서 import 유지)
├── subscriptionUtils.js # 상수·순수헬퍼 이동
├── subscriptionUtils.test.js # 순수헬퍼 유닛 테스트 (신규)
└── components/
├── StatusBadge.jsx # (기존 컴포넌트 이동)
├── DashboardTab.jsx # 자립형(self-fetch)
├── AnnouncementCard.jsx
├── AnnouncementDetail.jsx # detailTab 상태 + 정보/일정/주택형/매칭분석
├── CalendarView.jsx
├── AnnouncementsTab.jsx # AnnouncementCard/AnnouncementDetail/CalendarView 조합
├── MatchesTab.jsx
├── ProfileTab.jsx # DistrictTierEditor/NotificationSettings(기존) 사용
├── DistrictTierEditor.jsx # (기존 유지)
└── NotificationSettings.jsx # (기존 유지)
subscriptionUtils.js로 이동할 항목:
- 상수:
STATUS_CONFIG,HOUSE_TYPE_LABELS,TABS,STATUS_FILTERS,DEFAULT_PROFILE - 순수 헬퍼:
extractTier,fmt,fmtFull,_diffDays,getDDays,getDDayColor,fmtDateTime,fmtPrice apiPatch(현재 Subscription 로컬 정의) — 이동. (⚠️src/api.js에 동일 기능apiPatch이미 존재 — 이번엔 로컬 것을 utils로 그대로 이동(동작 보존), api.js와의 dedupe는 후속 follow-up으로 기록.)
의존 관계(이동 시 import 배선):
DashboardTab→ apiGet/apiPost, STATUS_CONFIG, fmt류, getDDays/getDDayColor, fmtDateTime, fmtPrice, StatusBadgeAnnouncementCard/AnnouncementDetail→ STATUS_CONFIG, HOUSE_TYPE_LABELS, extractTier, fmt/fmtFull, getDDays/getDDayColor, fmtPrice, StatusBadgeAnnouncementsTab→ apiGet/apiDelete/apiPatch, STATUS_FILTERS, AnnouncementCard/AnnouncementDetail/CalendarViewMatchesTab→ apiGet/apiPost/apiPatch, extractTier, fmt/getDDays/getDDayColor, StatusBadgeProfileTab→ apiGet/apiPut, DEFAULT_PROFILE, DistrictTierEditor/NotificationSettingsSubscription(shell) → TABS, 4탭 컴포넌트, PullToRefresh/FAB/Link
주의: /realestate/listings 교차링크(현 shell의 Link)는 그대로 유지.
4. 테스트 (안전망 — 현재 Subscription은 테스트 0)
behavior-preserving라 기존 동작 유지가 핵심. 안전망:
subscriptionUtils.test.js(신규, 필수) — 순수 헬퍼 유닛:extractTier: "자치구 S티어: 강남구 (+25)" → "S"; 미매치 → null.getDDays/getDDayColor: D-day 경계(오늘=D-Day, 미래 D-N, 과거 D+N; 색 임계 0/3/7).fmtPrice(현 구현 동작 고정): 10000→"1억", 15000→"1.5억", 9999→"9,999만", null→null._diffDays: 로컬 타임존 파싱 경계.
StatusBadge.test.jsx(신규, 스모크) — status→색/라벨, 미정의 status 폴백.- 각 탭 컴포넌트는 자립형(api 페치) → 이번 범위는 스모크 최소화(추출 검증은 build+lint+수동). 탭 스모크(vi.mock api)는 권장이나 필수 아님(후속).
검증 게이트(품질 게이트): npm run test:run(신규 유닛 포함 전체 green) + npm run lint(신규 파일 0 에러) + npm run build(성공) + 개발서버 /realestate 4탭 수동 확인(공고 목록/상세/캘린더/매칭/프로필 저장 동작 동일).
5. 컴포넌트 구조 (분해 후 검증 관점)
각 파일이 "무엇을 하는가 / 어떻게 쓰는가 / 무엇에 의존하는가"로 독립 이해 가능해야 함:
AnnouncementDetail(현 272줄) → 여전히 큰 편이나 단일 책임(공고 상세). 내부 detailTab 3섹션은 한 컴포넌트 응집. 300줄 근처 → 필요 시 후속 분할 여지 기록.ProfileTab(현 390줄) → 폼이 커서 300줄 초과. 이번엔 파일 추출까지만, 폼 섹션(기본/자격/선호) 하위 분할은 후속 follow-up으로 기록(과분해 회피, YAGNI).
6. 완료 기준 (Acceptance)
- Part A: web-ui/CLAUDE.md "기능 모듈 구조" 섹션 + README 컨벤션 문단 기록.
- Part B: Subscription.jsx가 shell(~70줄) + components/8 + subscriptionUtils(+test)로 분해, 동작/스타일 불변.
subscriptionUtils.test.js·StatusBadge.test.jsx통과, 전체npm run test:rungreen.npm run lint·npm run build통과./realestate4탭 수동 검증 동일 동작.
7. 리스크 / 오픈 이슈
- 테스트 부재 컴포넌트의 리팩토링: 순수 추출로 위험 최소화 + build/lint/유닛테스트/수동검증. 로직 한 줄도 바꾸지 않음.
- 거대 import 배선 실수 가능: 추출 시 각 파일의 사용 심볼을 정확히 import해야 함(누락 시 build 실패로 즉시 검출).
- 후속 follow-up(문서화, 이번 미적용): ProfileTab 폼 섹션 분할; local apiPatch ↔ api.js apiPatch dedupe; AnnouncementDetail 추가 분할.
- 다음 서브프로젝트: RealEstate(909)→PortfolioTab(671)→InstaCards(1013)→MusicStudio(1936), 각 동일 컨벤션 별도 spec/plan.