|
| 1 | +# `widgets/` — 위젯 슬라이스 레이어 |
| 2 | + |
| 3 | +> 페이지를 구성하는 "큰 UI 블록" 단위. 한 widget = 자체적으로 의미를 가지는 섹션·영역. |
| 4 | +
|
| 5 | +상위: [`../../CLAUDE.md`](../../CLAUDE.md) |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. widget이 되어야 하는 것 / 아닌 것 |
| 10 | + |
| 11 | +**widget O**: |
| 12 | +- "사이트 네비게이션" — `widgets/site-nav` |
| 13 | +- "사이트 푸터" — `widgets/site-footer` |
| 14 | +- "홈 히어로 섹션" — `widgets/home-hero` |
| 15 | +- "서비스 카드 그리드" — `widgets/home-services` |
| 16 | +- "FAQ 아코디언 섹션" — `widgets/home-faq` |
| 17 | +- "워크스페이스 사이드바" (예정) — `widgets/workspace-sidebar` |
| 18 | +- "면접 진행 컨트롤 바" (예정) — `widgets/interview-control` |
| 19 | + |
| 20 | +**widget X**: |
| 21 | +- "Button 자체" — `shared/ui/Button` |
| 22 | +- "이력서를 업로드한다" — `features/resume` (사용자 액션 흐름) |
| 23 | +- "Session 도메인 모델" — `domain/session` |
| 24 | + |
| 25 | +**판단 기준**: |
| 26 | +- 페이지에 박혔을 때 **자체적으로 의미를 가지는 UI 블록**이면 widget. |
| 27 | +- 작은 범용 컴포넌트라면 `shared/ui`. |
| 28 | +- 사용자 액션(클릭 → API 호출 → 상태 변경)이 본질이라면 `features/`. |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## 2. 슬라이스 구조 |
| 33 | + |
| 34 | +``` |
| 35 | +widgets/{name}/ |
| 36 | +├── ui/ # 컴포넌트 (HomeHero, SiteNav, ...) |
| 37 | +│ └── {Widget}.tsx |
| 38 | +├── model/ # widget 단위 훅·상태 (있다면) |
| 39 | +├── lib/ # widget 내부 유틸 (있다면) |
| 40 | +└── index.ts # public API |
| 41 | +``` |
| 42 | + |
| 43 | +**대부분의 widget은 `ui/` + `index.ts` 두 개로 끝난다.** `model/`·`lib/`는 필요해질 때만 추가. |
| 44 | + |
| 45 | +### Public API (index.ts) |
| 46 | +메인 컴포넌트만 export. 내부 sub-component는 export 하지 않는다. |
| 47 | +```ts |
| 48 | +// widgets/home-hero/index.ts |
| 49 | +export { HomeHero } from './ui/HomeHero' |
| 50 | + |
| 51 | +// ui/Laptop.tsx, ui/ScreenContent.tsx 는 내부 구현 — re-export 안 함 |
| 52 | +``` |
| 53 | + |
| 54 | +슬라이스 폴더는 **kebab-case**, 컴포넌트 export 는 **PascalCase**. |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## 3. 의존성 규칙 |
| 59 | + |
| 60 | +``` |
| 61 | +widgets/{X} → features/*, domain/*, shared/* ✓ |
| 62 | +widgets/{X} → widgets/{Y} ✗ (다른 widget import 금지) |
| 63 | +widgets/{X} → pages/*, app/* ✗ |
| 64 | +``` |
| 65 | + |
| 66 | +위젯끼리 import가 필요하면: |
| 67 | +- 공통 UI 조각을 `shared/ui` 로 추출 |
| 68 | +- 또는 페이지에서 composition 으로 연결 (페이지가 widget 조립 책임) |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## 4. 명명 규칙 |
| 73 | + |
| 74 | +| Prefix | 범위 | 예 | |
| 75 | +|---|---|---| |
| 76 | +| `site-*` | 사이트 전역 (여러 페이지 재사용) | `site-nav`, `site-footer` | |
| 77 | +| `home-*` | 홈페이지 전용 | `home-hero`, `home-services`, `home-faq` | |
| 78 | +| `workspace-*` (예정) | 워크스페이스 페이지 전용 | `workspace-sidebar` | |
| 79 | +| `interview-*` (예정) | 면접 페이지 전용 | `interview-control`, `interview-transcript` | |
| 80 | + |
| 81 | +> prefix 가 페이지 종속을 명시적으로 표현. 한 페이지에서만 쓰이는 widget 도 정상. |
| 82 | +
|
| 83 | +--- |
| 84 | + |
| 85 | +## 5. widget 인벤토리 |
| 86 | + |
| 87 | +| Widget | 책임 | 사용 페이지 | |
| 88 | +|---|---|---| |
| 89 | +| `site-nav` | 상단 네비게이션, 스크롤 시 배경 전환 | 전역 | |
| 90 | +| `site-footer` | 푸터 (브랜드, 링크 컬럼, 카피라이트) | 전역 | |
| 91 | +| `home-hero` | 노트북 목업 + 타이핑 인트로 (`useTypewriter`) | `/` | |
| 92 | +| `home-services` | 서비스 카드 3개 그리드 | `/` | |
| 93 | +| `home-quote` | 다크 그린 quote · 팀 크레딧 | `/` | |
| 94 | +| `home-faq` | FAQ 아코디언 (`<details>` 기반) | `/` | |
| 95 | +| `home-cta` | 풀-블리드 CTA 배너 | `/` | |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## 6. widget vs feature 비교 |
| 100 | + |
| 101 | +| 질문 | widget | feature | |
| 102 | +|---|---|---| |
| 103 | +| 본질 | 페이지 영역을 채우는 **UI 구성** | 사용자가 수행하는 **액션 흐름** | |
| 104 | +| 서버 호출 | 보통 없음 (data 는 props 로 받음) | 있음 (mutation / query) | |
| 105 | +| User Story 매핑 | 없음 (UI 표현 단위) | US-XX 1개 | |
| 106 | +| 상태 | UI 로컬 상태 정도 | 도메인 상태 / 서버 상태 | |
| 107 | +| 예 | `home-hero`, `site-nav` | `auth`, `resume`, `interview` | |
| 108 | + |
| 109 | +같은 영역이라도 **데이터 표시만**이면 widget, **사용자 액션 흐름**이 있으면 feature. widget 이 feature 의 훅을 끌어와 페이지 한 구역에 박는 패턴은 정상 (예: `widgets/interview-control` 이 `features/interview` 의 mutation 훅 사용). |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +## 7. 타입 사용 |
| 114 | + |
| 115 | +widget UI 컴포넌트가 받는 데이터는 [`/docs/frontend-types.md`](../../../docs/frontend-types.md) 의 단방향 흐름을 따른다. |
| 116 | + |
| 117 | +``` |
| 118 | +domain/{slice}/model/ (Entity) → widget 내부에서 XxxModel 로 가공 → 컴포넌트 Props |
| 119 | +``` |
| 120 | + |
| 121 | +`XxxDto` 를 widget 안에서 직접 import 하면 boundary 위반. 매핑은 `features/*/api/` 또는 `domain/*/model/` 에서. |
| 122 | + |
| 123 | +--- |
| 124 | + |
| 125 | +## 8. 신규 widget 추가 절차 |
| 126 | + |
| 127 | +1. `widgets/{name}/ui/{Widget}.tsx` 생성 (PascalCase 파일·컴포넌트명) |
| 128 | +2. `widgets/{name}/index.ts` 에 메인 컴포넌트만 export |
| 129 | +3. 페이지에서 import 해서 조립 (`pages/Xxx/ui/XxxPage.tsx`) |
| 130 | +4. 본 문서 §5 인벤토리 등록 |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## 9. 안티패턴 |
| 135 | + |
| 136 | +- ❌ widget 이 다른 widget 을 직접 import — 페이지에서 composition 으로 |
| 137 | +- ❌ widget 이 라우팅 결정 (`useNavigate` 등) — 페이지 책임 |
| 138 | +- ❌ widget 이 `XxxDto` 직접 사용 — [`/docs/frontend-types.md`](../../../docs/frontend-types.md) 단방향 흐름 위반 |
| 139 | +- ❌ widget 내부 sub-component 를 `index.ts` 에서 re-export — 캡슐화 깨짐 |
| 140 | +- ❌ "재사용성을 위한" widget 미리 만들기 — 한 페이지에서만 써도 widget 이 되는 게 정상. 재사용은 필요해지면. |
| 141 | +- ❌ widget 에서 직접 서버 호출 — `features/*/api/` 또는 `domain/*` 을 경유 |
0 commit comments