우리가 흔히 알고 사용해왔던 구조는 레이어드 아키텍처(layered architecture) 이다.
📂 src/
├── 📁 components/ → UI 컴포넌트 (재사용 가능한 단위)
├── 📁 pages/ → 라우트 단위 페이지
├── 📁 hooks/ → 재사용 가능한 훅
├── 📁 lib/ → 유틸리티, API 호출 등 공통 라이브러리
├── 📁 store/ → 전역 상태 관리 (Redux, Zustand 등)
도메인 독립적 구조라고도 불리며 이 방식은 기능 중심이 아닌 역할 중심으로 코드를 분리하는 방법이다.
create-react-app 에서 익히 봤던 구조다.
기능 분할 설계(feature-sliced-design 이하 FSD) 는 느슨한 결합, 높은 응집력을 실현하는 데 최적화된 아키텍처라고 한다.
느슨한 결합 이란 코드, 프론트엔드 관점에서 컴포넌트 간에 의존성을 최소화하고 독립적으로 동작할 수 있는 형태를 말한다.
높은 응집력 은 관심사가 비슷한 코드들이 같은 위치에 모여 있는 것을 의미한다.
📂 src/
├── 📁 app/ → 글로벌 설정, 라우팅, 페이지 전역 데이터 관리
├── 📁 shared/ → 재사용 가능한 공통 모듈 (UI, hooks, utils 등)
├── 📁 entities/ → 핵심 도메인 (User, Product 등)
├── 📁 features/ → 독립적인 기능 모듈 (로그인, 장바구니 등)
├── 📁 widgets/ → 여러 기능을 조합한 복합 UI 요소 (Navbar, Sidebar 등)
├── 📁 pages/ → 라우트 단위 페이지 (Home, Profile 등)
│ ├── 📁 api/ → API 라우트 (Next.js API routes)
│ ├── 📄 index.tsx → Home 페이지
│ ├── 📄 cart.tsx → Cart 페이지
app 폴더는 애플리케이션 전반에 필요한 설정 파일과 전역 상태 관리 전역 스타일이 존재한다. 라우터 설정이나 공통 레이아웃이 존재할 수 있다.
📁 app/ → 글로벌 설정 및 전역 관리
├── 📄 layout.tsx → 페이지 레이아웃 설정 (공통 UI)
├── 📄 router.tsx → 글로벌 라우팅 설정
└── 📄 theme.ts → 전역 테마 및 스타일 설정
공통적으로 사용되는 UI 컴포넌트나 유틸 함수, 커스텀 훅이 존재하는 곳이다. 재사용이 가능한 UI 와 기능이 존재한다고 보면됨.
📁 shared/ → 재사용 가능한 공통 모듈
├── 📁 components/ → UI 컴포넌트 (Button, Input 등)
├── 📁 hooks/ → 공통 훅 (useLocalStorage, useFetch 등)
├── 📁 utils/ → 유틸리티 함수 (formatDate, apiCall 등)
└── 📁 types/ → 공통 타입 정의
user나 product 등의 핵심 도메인 로직이 존재하는 곳이다.
📁 entities/ → 핵심 도메인 모델 및 로직
├ 📁 product/ → Product 관련 기능
│ ├── 📁 model/ → 데이터 모델 (Product 데이터 처리)
│ ├── 📁 ui/ → Product 관련 UI 컴포넌트
│ └── 📁 api/ → API 호출 함수
├ 📁 user/ → User 관련 기능
│ ├── 📁 model/
│ ├── 📁 ui/
│ └── 📁 api/
/login /mywish /detail /mypage 등의 독립적인 기능에 대한 로직이 존재하는 곳이다.
📁 features/ → 독립적인 기능 모듈
├ 📁 cart/ → 장바구니 관련 기능
│ ├── 📁 model/ → 상태 관리 및 로직
│ ├── 📁 ui/ → Cart UI
│ └── 📁 hooks/ → 장바구니 관련 훅
├ 📁 auth/ → 인증 관련 기능
│ ├── 📁 model/
│ ├── 📁 ui/
│ └── 📁 hooks/
여러 기능을 조합한 UI 요소가 존재하는 곳이다.
shared 와 같이 공통적으로 사용되는 UI 컴포넌트를 관리하는 곳이지만 shared 에 존재하는 아토믹한 UI를 조합하거나 구조가 복잡한 UI가 존재한다.
Header.tsx 가 그 예시이다. 공통적으로 사용되지만 로직은 복잡한
📁 widgets/ → 복합 UI 요소
├ 📁 navbar/ → 네비게이션 바 관련 컴포넌트
├ 📁 sidebar/ → 사이드바 관련 컴포넌트
├ 📁 header/ → 헤더 관련 컴포넌트
├ 📁 searchbar/ → 검색창 관련 컴포넌트
└ 📁 footer/ → 푸터 관련 컴포넌트
라우트에 따라 페이지별로 렌더링될 페이지 컴포넌트가 존재하는 곳
📁 pages/ → 페이지 라우트 (Next.js 페이지 파일)
├ 📁 api/ → API 라우트 (Next.js API Routes)
│ └── 📄 product.ts → 제품 관련 API
├ 📄 index.tsx → 홈 페이지
├ 📄 cart.tsx → 장바구니 페이지
└ 📄 login.tsx → 로그인 페이지
FSD는 느슨한 결합과 높은 응집력 에 최적화된 아키텍처라고 소개하고 있다.
결합도와 응집력 두가지 키워드를 가지고 FSD 와 레이어드 아키텍처를 비교해보자.
레이어드 아키텍처
우선 레이어드 아키텍처의 경우 기능이 아닌 역할로 구분된 구조다.
레이어드 아키텍처에서 Cart.tsx
페이지를 구성한다고 가정해보자면
/src/components/CartItem.tsx
/src/hooks/useCart.ts
/src/store/cartStore.ts
components hooks store 등 이곳 저곳을 참조하여 import 해야한다.
FSD
반면에 FSD 의 경우
src/features/cart/ui/Cart.tsx
src/features/cart/ui/CartItem.tsx
src/features/cart/modal/useCart.ts
src/features/cart/modal/cartStore.ts
위와 같이 cart 라는 기능의 폴더 내부에서 참조된다.
여기서 말하는 결합도란 여러 폴더를 참조해야 하는 정도를 의미하고 FSD 가 레이어드 아키텍처에 비해 결합도가 낮다.
응집력은 말 그대로 관심사가 비슷한 코드가 모여있는 정도를 의미한다.
위 예시를 대입해보면 레이어드 아키텍처는 Cart 라는 동일한 관심사를 가진 것에 비해 코드가 역할 별로 흩어져 있다.
반대로 FSD 는 Cart 라는 폴더를 기준으로 ui와 기능이 모두 같은 곳에 있다.
새로운 지식과 인사이트를 갈구해야된다는 사실을 여기서도 느꼈다.
사실 기존의 레이어드 아키텍처 기반의 프로젝트를 진행해오면서 불편하다라고 생각하진 않았다. 그러나 어떤 계기로 FSD 기반의 프로젝트를 진행해보니 레이어드 아키텍처의 단점이 드러났고 역체감을 느낄 수 있었다.
기존 방식에 매번 불편함을 찾아 느끼고 새로운 방식을 찾는다면 좋겠지만, 익숙함에 불편함에 가려지거나 기존이 최선이겠거니 생각한다면 레거시한 사람이 될 수 있겠구나 라는 생각이 들었다.