33 KiB
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):
// @/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):
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<UserList> {
const response = await client.get<UserList>(
`${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):
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 설정, 글로벌 스타일 적용
// src/app/providers/app-provider.tsx
const AppProvider = ({ children }) => {
return (
<QueryClientProvider client={queryClient}> // React Query 설정
<StoreProvider> // Zustand Store 초기화
<PromptProvider>
<ConfirmProvider>
<AuthWrapper> // entities/auth - 인증 처리
<MenuWrapper> // entities/menu - 메뉴 초기화
<DndProvider ...>
{children}
</DndProvider>
</MenuWrapper>
<AlertDialog /> // shared/ui - 전역 다이얼로그
<PromptDialog />
<ConfirmDialog />
<Toaster />
</AuthWrapper>
</ConfirmProvider>
</PromptProvider>
</StoreProvider>
<ReactQueryDevtools />
</QueryClientProvider>
);
};
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만 합니다.
// 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로 추출 |
레이어 역할 준수 |