# 10. FSD 구조하에 CS 프로그램 > **FSD(Feature-Sliced Design)**: 레이어(Layer) 간 단방향 의존성을 강제하는 프론트엔드 아키텍처 방법론. > `shared → entities → widgets → pages → app` 순서로 **하위 레이어만 참조 가능**. --- ## 1. FSD란 무엇인가 FSD(Feature-Sliced Design)는 기능 중심으로 코드를 조직화하는 프론트엔드 아키텍처입니다. 가장 중요한 원칙은 **"상위 레이어가 하위 레이어를 참조하고, 하위 레이어는 절대 상위 레이어를 참조할 수 없다"** 입니다. ``` ┌─────────────────────────────────────────────────────────┐ │ app (애플리케이션 진입점) │ ← 최상위: 모든 레이어 조합 ├─────────────────────────────────────────────────────────┤ │ pages (Next.js 라우트 페이지) │ ← 라우팅 연결 + 페이지 컴포넌트 ├─────────────────────────────────────────────────────────┤ │ widgets (독립 UI 블록) │ ← 여러 entities 조합한 복합 UI ├─────────────────────────────────────────────────────────┤ │ entities (도메인 엔티티) │ ← 비즈니스 도메인 단위 캡슐화 ├─────────────────────────────────────────────────────────┤ │ shared (공통 기반 인프라) │ ← 최하위: 재사용 가능한 순수 기능 └─────────────────────────────────────────────────────────┘ ↑ 상위 레이어만 아래를 참조 (단방향) ``` ### 레이어 내부 구조: 슬라이스(Slice) + 세그먼트(Segment) 각 레이어는 `슬라이스(도메인/기능 단위)` → `세그먼트(역할 단위)`로 세분화됩니다. ``` 레이어/슬라이스/세그먼트 예시: entities/user/api/ entities/user/model/ entities/user/ui/ entities/user/index.ts ← 공개 API (Public API) ``` --- ## 2. CS 앱 실제 디렉터리 구조 ### 전체 구조 ``` apps/cs/ ├── app/ # Next.js App Router (라우팅 레이어) │ ├── (auth)/ # 인증 없는 공개 라우트 │ │ └── login/ │ ├── (main)/ # 인증이 필요한 메인 라우트 │ │ ├── layout.tsx # 메인 레이아웃 래퍼 │ │ ├── (with-base-layout)/ # 헤더/LNB 포함 레이아웃 │ │ │ ├── layout.tsx # BaseLayout + PageLayout 조합 │ │ │ ├── users/ # 사용자 관리 라우트 │ │ │ │ ├── page.tsx # → UsersPage 컴포넌트 연결 │ │ │ │ ├── register/ │ │ │ │ └── [userUnqId]/ │ │ │ ├── bbs/ # 게시판 관리 라우트 │ │ │ ├── dashboard/ │ │ │ ├── menu/ │ │ │ └── ... │ │ └── (without-base-layout)/ # LNB 없는 전체화면 │ └── api/ # Next.js API Routes │ ├── src/ # FSD 소스 레이어 │ ├── app/ # App 레이어 (글로벌 설정) │ │ ├── index.tsx # App 루트 컴포넌트 │ │ ├── providers/ │ │ │ └── app-provider.tsx # 전역 Provider 조합 │ │ └── styles/ # 글로벌 CSS │ │ │ ├── pages/ # Pages 레이어 (페이지 컴포넌트) │ │ ├── users/ │ │ │ ├── model/ # 테이블 컬럼 정의 등 │ │ │ ├── ui/ # 페이지 컴포넌트 │ │ │ └── index.ts # Public API │ │ ├── bbs/ │ │ ├── dashboard/ │ │ ├── menu/ │ │ ├── role/ │ │ ├── site/ │ │ └── ... │ │ │ ├── widgets/ # Widgets 레이어 (복합 UI 블록) │ │ ├── dnd-tree/ # 드래그앤드롭 트리 위젯 │ │ ├── form-wrapper/ # 폼 래퍼 위젯 │ │ └── post/ # 게시물 관련 복합 위젯 │ │ │ ├── entities/ # Entities 레이어 (도메인 엔티티) │ │ ├── auth/ # 인증 도메인 │ │ ├── user/ # 사용자 도메인 │ │ ├── board/ # 게시판 도메인 │ │ ├── code/ # 공통코드 도메인 │ │ ├── menu/ # 메뉴 도메인 │ │ ├── role/ # 권한 도메인 │ │ ├── site/ # 사이트 도메인 │ │ ├── group/ # 그룹 도메인 │ │ ├── organization/ # 조직 도메인 │ │ ├── attachment/ # 파일첨부 도메인 │ │ └── ... │ │ │ └── shared/ # Shared 레이어 (공통 기반) │ ├── api/ # API 클라이언트 │ ├── config/ # 환경변수, 엔드포인트 상수 │ ├── lib/ # 공통 커스텀 훅 │ ├── model/ # 공통 타입 (PageableList 등) │ ├── providers/ # 공통 Context Provider │ ├── store/ # Zustand 스토어 정의 │ └── ui/ # CS 앱 전용 공통 UI 컴포넌트 │ ├── packages/common/ # 모노레포 공통 패키지 │ ├── components/ # @common/components - UI 컴포넌트 라이브러리 │ ├── business_components/ # 업무용 공통 컴포넌트 │ ├── auth/ # @common/auth - 인증 유틸리티 │ ├── network/ # @common/network - 네트워크 유틸리티 │ └── config/ # 공통 설정 │ └── packages/rsone/ # 추가 공통 컴포넌트 패키지 └── components/ ``` --- ## 3. 레이어별 상세 역할 ### 🔵 `shared` - 공통 기반 레이어 **위치**: `src/shared/` **의존성**: 다른 어떤 레이어도 참조하지 않음 (최하위) **역할**: 앱 전체에서 재사용 가능한 순수 유틸리티, 인프라, 타입 정의 | 세그먼트 | 경로 | 내용 | |--------|------|------| | `api` | `src/shared/api/` | Axios 인스턴스 (`client.ts`), 공통 request 설정 | | `config` | `src/shared/config/` | 환경변수(`env.ts`), API 엔드포인트 상수(`api.ts`), 인증 설정 | | `lib` | `src/shared/lib/` | 커스텀 훅 (`useDataTable`, `useUserStore`, `useMutationWrapper` 등) | | `model` | `src/shared/model/` | 공통 인터페이스 (`PageableList`, `PageableParams`) | | `store` | `src/shared/store/` | Zustand 스토어 정의 (`userStore`, `menuStore`, `alertStore`) | | `providers` | `src/shared/providers/` | Context Provider (`StoreProvider`, `PromptProvider`) | | `ui` | `src/shared/ui/` | 앱 전용 공통 UI (`AlertDialog`, `ConfirmDialog`, `PageLayout`, `GridViewDataTable` 등) | **코드 예시 (`src/shared/api/client.ts`)**: ```typescript // @/shared/api 로 임포트 import { client } from "@/shared/api"; ``` --- ### 🟢 `entities` - 비즈니스 도메인 레이어 **위치**: `src/entities/` **의존성**: `shared`만 참조 가능 **역할**: 하나의 비즈니스 도메인(user, board, auth 등)을 API 서비스와 타입으로 캡슐화 각 엔티티는 다음 세그먼트로 구성됩니다: ``` entities/user/ ├── api/ │ └── user-service.ts # API 서비스 클래스 (CRUD 메서드) ├── model/ │ └── user-interface.ts # TypeScript 타입, Zod 스키마, 상수 ├── ui/ │ └── password-reset-dialog.tsx # 해당 도메인 전용 UI (선택적) └── index.ts # Public API - 외부에서 사용할 것만 export ``` **코드 예시 (`src/entities/user/api/user-service.ts`)**: ```typescript import { client } from "@/shared/api"; // shared 참조 OK import { ENDPOINT } from "@/shared/config"; // shared 참조 OK import type { UserService, UserSearchParams } from "../model/user-interface"; export class UserApiService implements UserService { public async findPageUser(params: UserSearchParams): Promise { const response = await client.get( `${ENDPOINT.USER_SERVICE.USERS}`, { params }, ); return response.data; } } ``` **엔티티 간 참조 (`@x` 패턴)**: 동일 레이어(entities) 내 다른 슬라이스를 참조할 때는 `@x` 폴더를 통해 명시적으로 허용합니다. ``` entities/board/@x/ └── post.ts # board 엔티티가 post 엔티티에서 필요한 것만 re-export ``` --- ### 🟡 `widgets` - 독립 UI 블록 레이어 **위치**: `src/widgets/` **의존성**: `shared`, `entities` 참조 가능 **역할**: 여러 entities를 조합하여 독립적으로 동작하는 복합 UI 블록 | 슬라이스 | 내용 | |--------|------| | `dnd-tree` | 드래그앤드롭 트리 컴포넌트 (메뉴 관리 등에 활용) | | `form-wrapper` | 폼 공통 래퍼 (저장/취소 버튼 패턴 표준화) | | `post` | 게시물 관련 복합 UI 블록 | --- ### 🟠 `pages` - 페이지 컴포넌트 레이어 **위치**: `src/pages/` **의존성**: `shared`, `entities`, `widgets` 참조 가능 **역할**: 실제 화면 UI 구현. Next.js App Router의 `page.tsx`에서 이 컴포넌트를 import하여 연결 각 페이지 슬라이스 구조: ``` pages/users/ ├── model/ │ └── table-columns.tsx # TanStack Table 컬럼 정의 ├── ui/ │ ├── users-page.tsx # 목록 페이지 메인 컴포넌트 │ ├── create-page.tsx # 등록 페이지 │ ├── modify-page.tsx # 수정 페이지 │ ├── user-detail.tsx # 상세 컴포넌트 │ └── users-form.tsx # 등록/수정 공통 폼 └── index.ts # Public API ``` **코드 예시 (`src/pages/users/ui/users-page.tsx`)**: ```typescript import { UserApi } from "@/entities/user"; // entities 참조 OK import { CodeApi } from "@/entities/code"; // entities 참조 OK import { ENDPOINT } from "@/shared/config"; // shared 참조 OK import { useDataTable } from "@/shared/lib"; // shared 참조 OK import { Button } from "@common/components"; // 공통 패키지 참조 OK // ❌ import { something } from "@/app/..."; // 상위 레이어 참조 불가 ``` --- ### 🔴 `app` - 애플리케이션 레이어 **위치**: `src/app/` **의존성**: 모든 하위 레이어 참조 가능 **역할**: 앱 초기화, 전역 Provider 설정, 글로벌 스타일 적용 ```typescript // src/app/providers/app-provider.tsx const AppProvider = ({ children }) => { return ( // React Query 설정 // Zustand Store 초기화 // entities/auth - 인증 처리 // entities/menu - 메뉴 초기화 {children} // shared/ui - 전역 다이얼로그 ); }; ``` --- ## 4. Next.js App Router와 FSD 통합 방식 Next.js App Router의 라우팅 파일(`app/` 디렉토리)과 FSD의 `pages` 레이어는 **별도로 분리**됩니다. ``` ┌─────────────────────────────────────────────────────────┐ │ Next.js App Router (app/) │ │ │ │ app/(main)/(with-base-layout)/users/page.tsx │ │ ┌─────────────────────────────────────────────────┐ │ │ │ import { UsersPage } from "@/pages/users"; │ │ │ │ export default UsersPage; │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ │ (딱 1줄 연결) │ └────────────────────┼────────────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────┐ │ FSD pages 레이어 (src/pages/) │ │ │ │ src/pages/users/ui/users-page.tsx │ │ → 실제 화면 로직과 UI 구현 │ └─────────────────────────────────────────────────────────┘ ``` **핵심 패턴**: `app/` 폴더의 `page.tsx`는 단순히 FSD `pages` 레이어 컴포넌트를 **re-export**만 합니다. ```typescript // app/(main)/(with-base-layout)/users/page.tsx import { UsersPage } from "@/pages/users"; export default UsersPage; // 딱 1줄! ``` ### 레이아웃 계층 구조 ``` app/(main)/layout.tsx └── App (src/app/index.tsx) └── AppProvider (QueryClient, Auth, Menu, Store 등) app/(main)/(with-base-layout)/layout.tsx └── BaseLayout (@common/components) ← 헤더 + LNB └── PageLayout (src/shared/ui) ← 콘텐츠 영역 └── {children} ← 각 page.tsx 컴포넌트 ``` --- ## 5. 모노레포 패키지와의 관계 CS 앱은 `packages/` 아래의 공통 패키지를 `shared` 레이어의 하위 개념으로 활용합니다. ``` ┌─────────────────────────────────────────────────────────┐ │ apps/cs │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ src/shared (앱 전용 공통) │ │ │ │ - @/shared/api → Axios 클라이언트 │ │ │ │ - @/shared/config → 엔드포인트 상수 │ │ │ │ - @/shared/lib → 커스텀 훅 │ │ │ │ - @/shared/ui → 앱 전용 공통 UI │ │ │ └──────────────────────────────────────────────────┘ │ │ ↑ 참조 │ │ ┌──────────────────────────────────────────────────┐ │ │ │ packages/common (모노레포 공통 패키지) │ │ │ │ - @common/components → 기반 UI 컴포넌트 라이브러리 │ │ │ │ - @common/auth → JWT, 쿠키 유틸리티 │ │ │ │ - @common/network → 에러 인터셉터, 리다이렉트 │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` | 패키지 | Import 경로 | 역할 | |--------|------------|------| | `packages/common/components` | `@common/components` | Button, Input, Table, Dialog 등 기반 UI | | `packages/common/auth` | `@common/auth` | JWT 파싱, getRemainingMs, 쿠키 관리 | | `packages/common/network` | `@common/network` | 401 에러 인터셉터, isAtLoginRedirectUrl | | `packages/rsone/components` | `@rsone/components` | 추가 UI 컴포넌트 | --- ## 6. 미들웨어 기반 API 프록시 아키텍처 CS 앱은 클라이언트가 백엔드를 직접 호출하지 않고 **Next.js Middleware를 통해 프록시**합니다. ``` Browser (React 컴포넌트) │ │ axios.get('/server/user-service/users') ▼ Next.js Middleware (middleware.ts) │ - 쿠키에서 JWT 토큰 추출 → 요청 헤더에 주입 │ - URL 패턴 매칭 → 실제 백엔드 URL로 rewrite │ - CSP 보안 헤더 추가 ▼ 실제 백엔드 API 서버 (Spring Boot) │ └── 응답 반환 ``` --- ## 7. 의존성 규칙 요약 ``` ❌ 금지: 하위 레이어 → 상위 레이어 참조 ✅ 허용: 상위 레이어 → 하위 레이어 참조 shared ─────────────────────────── (다른 레이어 참조 불가) entities → shared widgets → shared, entities pages → shared, entities, widgets app → shared, entities, widgets, pages ``` **동일 레이어 내 참조 규칙**: - `entities` 내 다른 슬라이스 참조 시: `@x` 폴더를 통해 명시적으로 허용 - `pages` 내 다른 슬라이스 참조: 원칙적으로 금지 (widgets로 추출해야 함) --- ## 8. UI 추가 시 프로그램 UI 및 액션 흐름 ### 8-1. 신규 도메인 UI 추가 순서 ``` Step 1: entities 레이어에 도메인 추가 src/entities/{도메인}/ ├── api/{도메인}-service.ts ← API CRUD 클래스 작성 ├── model/{도메인}-interface.ts ← TypeScript 타입/인터페이스/Zod 스키마 정의 ├── ui/ (선택) ← 도메인 전용 UI (예: 검색 다이얼로그) └── index.ts ← Public API export Step 2: pages 레이어에 페이지 컴포넌트 추가 src/pages/{도메인}/ ├── model/table-columns.tsx ← 테이블 컬럼 정의 (TanStack Table) ├── ui/{도메인}-page.tsx ← 목록 페이지 ├── ui/create-page.tsx ← 등록 페이지 ├── ui/modify-page.tsx ← 수정 페이지 └── index.ts ← Public API export Step 3: Next.js 라우트 연결 app/(main)/(with-base-layout)/{도메인}/ ├── page.tsx ← import { XxxPage } from "@/pages/{도메인}"; export default XxxPage; ├── register/page.tsx └── [id]/page.tsx ``` ### 8-2. 목록 페이지 UI 액션 흐름 사용자 관리(`/users`) 페이지를 예시로 전체 액션 흐름을 설명합니다. ``` ┌─────────────────────────────────────────────────────────────────┐ │ UsersPage (src/pages/users/ui/users-page.tsx) │ │ │ │ [초기 렌더링] │ │ useQuery({ queryKey: [ENDPOINT.USER_SERVICE.USERS, ...] }) │ │ └── UserApi.findPageUser(params) → 사용자 목록 API 호출 │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 검색 폼 영역 │ │ │ │ - 기간 선택: InputDateRangePicker (shared/ui) │ │ │ │ - 상태 선택: Select + CodeApi.getCodeList() (entities) │ │ │ │ - 키워드: Input (공통 컴포넌트) │ │ │ │ - [검색] 버튼 → handleSubmit() → setSearchParams() │ │ │ │ - [초기화] 버튼 → handleReset() → 상태 초기화 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 통계 현황 영역 │ │ │ │ - useQuery(userSttsStats) → UserApi.selectUserSttsStats() │ │ │ │ - 상태별 카운트 표시 (대기/정상/정지/탈퇴/삭제) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 데이터 테이블 영역 (useDataTable hook - shared/lib) │ │ │ │ - DataTable (@common/components) 렌더링 │ │ │ │ - 컬럼 정의: table-columns.tsx (pages/users/model/) │ │ │ │ - 행 클릭 → router.push('/users/{userUnqId}') │ │ │ │ - 페이지네이션 → setPagination() → 재쿼리 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ [우상단 버튼] │ │ - [등록] 버튼 → router.push('/users/register') │ └─────────────────────────────────────────────────────────────────┘ ``` **액션 흐름 상세**: ``` [검색 버튼 클릭] │ ▼ handleSubmit(e) e.preventDefault() setPagination({ pageIndex: 0 }) ← 1페이지로 리셋 setSearchParams({ ← 검색 파라미터 업데이트 keywordType, keyword, userStateCode, ... }) │ ▼ (React 상태 변경 → 리렌더링) useQuery 재실행 (queryKey에 searchParams 포함) │ ▼ UserApi.findPageUser(params) │ ▼ axios.get('/server/user-service/users?page=0&size=10&...') │ ▼ middleware.ts → JWT 헤더 주입 → 백엔드로 rewrite │ ▼ Spring Boot 백엔드 API │ ▼ 응답 DataTable 리렌더링 ``` ### 8-3. 등록/수정 페이지 UI 액션 흐름 ``` ┌──────────────────────────────────────────────────────────────┐ │ CreatePage / ModifyPage │ │ │ │ [폼 초기화] │ │ - react-hook-form useForm() 초기화 │ │ - Zod resolver로 유효성 검사 스키마 연결 │ │ - (수정 시) useQuery로 상세 데이터 로드 → form.reset() │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 입력 폼 (FormWrapper 위젯 or 직접 구성) │ │ │ │ - FormField + Input, Select, DatePicker 등 │ │ │ │ - 유효성 에러 메시지 자동 표시 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ [저장 버튼 클릭] │ │ form.handleSubmit(onSubmit) │ │ └── Zod 유효성 검사 통과 여부 확인 │ │ │ │ │ ├── 실패 → 에러 메시지 표시 │ │ └── 성공 → useMutationWrapper 실행 │ │ → UserApi.saveUser(data) 호출 │ │ → 성공 시 목록 페이지 이동 │ │ → 실패 시 AlertDialog 표시 │ └──────────────────────────────────────────────────────────────┘ ``` **액션 흐름 상세**: ``` [저장 버튼 클릭] │ ▼ form.handleSubmit(onSubmit) │ ├── Zod 유효성 검사 실패 │ └── 필드별 에러 메시지 표시 (react-hook-form) │ └── Zod 유효성 검사 성공 │ ▼ useMutationWrapper / useMutation │ ▼ UserApi.saveUser(data) [entities/user/api] │ ▼ axios.post('/server/user-service/users', data) │ ▼ middleware.ts → JWT 주입 → 백엔드로 rewrite │ ├── 성공 응답 │ ├── useAlertStore.showAlert('저장되었습니다') │ ├── queryClient.invalidateQueries(...) ← 캐시 무효화 │ └── router.push('/users') ← 목록으로 이동 │ └── 실패 응답 └── useAlertStore.showAlert(에러 메시지) ``` ### 8-4. 전체 데이터 흐름 다이어그램 ``` 사용자 인터랙션 (브라우저 UI) │ ▼ app/(main)/.../(with-base-layout)/users/page.tsx [Next.js App Router - 라우팅 진입점] │ (re-export) ▼ src/pages/users/ui/users-page.tsx [FSD pages 레이어 - 화면 컴포넌트] - useState: 검색 파라미터, 페이지네이션 상태 - useQuery: 데이터 조회 (React Query) │ ├──(UI 조합)──────────────────────────────┐ │ │ ▼ ▼ @common/components src/shared/ui/ Button, Input, Select, DataTable GridViewDataTable, InputDateRangePicker │ │ └──────────────┬──────────────────────────┘ │ ▼ src/entities/user/api/user-service.ts src/entities/code/api/code-service.ts [FSD entities 레이어 - API 서비스] │ ▼ src/shared/api/client.ts [Axios 인스턴스] │ ▼ middleware.ts [Next.js Middleware - 프록시 + JWT 주입] │ ▼ Spring Boot 백엔드 API │ ▼ 응답 반환 React Query 캐시 업데이트 │ ▼ UI 리렌더링 ``` ### 8-5. 인증 및 메뉴 초기화 흐름 ``` 앱 최초 로드 │ ▼ src/app/providers/app-provider.tsx (AppProvider) │ ▼ entities/auth/lib/auth-wrapper.tsx (AuthWrapper) │ ├── 쿠키에서 JWT 토큰 확인 │ ├── 토큰 없음 → /login 리다이렉트 │ └── 토큰 있음 │ ├── getRemainingMs() → 남은 시간 계산 │ └── setupRefreshTimer() → 자동 갱신 타이머 설정 │ ├── /users/info/me API 호출 → 로그인 사용자 정보 조회 │ └── useUserStore.setUserInfo() → Zustand 전역 상태 저장 │ └── entities/menu/lib/menu-wrapper.tsx (MenuWrapper) │ ├── 메뉴 목록 API 호출 └── useMenuStore → Zustand에 메뉴 데이터 저장 │ ▼ BaseLayout에 메뉴 렌더링 (LNB) │ ▼ 메뉴 클릭 → router.push(item.urlPath) → 해당 page.tsx로 이동 ``` --- ## 9. 공통 패키지 (`packages/common`) 역할 ### `packages/common/components` → `@common/components` 모든 앱에서 공유하는 기반 UI 컴포넌트 라이브러리: ``` components/src/ui/ ├── button/ # Button 컴포넌트 (variant: default, outline, ghost 등) ├── input/ # Input 텍스트 입력 ├── select/ # Select 드롭다운 ├── data-table/ # DataTable (TanStack Table 기반 페이지네이션 테이블) ├── dialog/ # Dialog 모달 ├── date-picker/ # DatePicker 날짜 선택 ├── sidebar/ # Sidebar (LNB) ├── header/ # Header 헤더 ├── lnb/ # LNB 좌측 네비게이션 ├── layout/ # BaseLayout (Header + LNB + Content 조합) ├── tree/ # Tree 트리 컴포넌트 ├── pagination/ # Pagination 페이지네이션 ├── tabs/ # Tabs 탭 ├── badge/ # Badge 배지 └── ... ``` ### `packages/common/business_components` (업무용 공통 컴포넌트) ``` business_components/src/ui/ ├── form/ # Form 공통 래퍼 ├── table/ # 업무용 테이블 ├── search_form/ # 검색 폼 레이아웃 ├── date_picker/ # 업무용 날짜 선택 ├── excel/ # 엑셀 다운로드 버튼 ├── file-upload/ # 파일 업로드 ├── map/ # 지도 컴포넌트 └── ... ``` --- ## 10. FSD 준수 체크리스트 새로운 기능을 추가할 때 다음 사항을 확인합니다: | 체크 항목 | 기준 | |----------|------| | ✅ `shared`에 비즈니스 로직 없음 | shared는 순수 유틸/인프라만 포함 | | ✅ `entities`가 `pages`를 import 안 함 | 단방향 의존성 준수 | | ✅ `entities` 간 참조는 `@x` 폴더 사용 | 명시적 교차 참조 | | ✅ `app/page.tsx`는 re-export만 함 | 1줄 연결 패턴 | | ✅ 각 슬라이스에 `index.ts` 존재 | Public API 캡슐화 | | ✅ 타입은 `model/` 세그먼트에 위치 | 타입 중앙화 | | ✅ API 호출은 `entities/api/` 세그먼트에 위치 | 서비스 분리 | | ✅ 전역 상태는 `shared/store/`에 정의 | 스토어 중앙화 | | ✅ 복수 entities 조합 UI는 `widgets`로 추출 | 레이어 역할 준수 |