FSD 아키텍처 소개

들어가며

우리 팀은 최근 FSD 아키텍처를 도입하려고 하고 있습니다. 이유는 여러가지가 있습니다. 우리 팀원들이 목적 조직에서 기능 조직으로 변화하면서 풀 기반으로 작업하는 것이 필요했고, 몇개월째 시도하려고 노력중이지만, 잘 되지 않는 원인 중 하나가 모노레포 구조의 서비스마다 폴더 구조가 너무 다르다는 것입니다.

그래서 공통된 ‘컨벤션’ 정의가 필요한 상황입니다.

그래서 컨벤션 정의를 다루기 위해 FSD를 도입하려고 하고 있습니다. 그러면 다음과 같은 질문이 나올 수 있습니다. 왜 ‘FSD’인가? 그에 대한 이야기를 하려고 합니다.

우리가 폴더구조를 바꾸는 진짜 이유

프론트엔드 개발에서 폴더 구조는 단순한 파일 정리가 아닙니다.

실제로는 팀의 생산성과 코드 품질을 결정하는 핵심 아키텍처입니다.

프론트엔드에서 폴더 구조의 진화 과정

실무에서 폴더 구조가 어떻게 진화하는지 우리 loans 프로젝트를 예시로 살펴보겠습니다.

1단계: 역할 기반 구조 (프로젝트 초기)

프로젝트를 처음 시작할 때는 보통 이런 식으로 구조를 잡습니다:

text
📁 src/
  📁 pages/              # 페이지 컴포넌트들
    📄 RefinancingListPage.tsx
    📄 RefinancingDetailPage.tsx
    📄 RefinancingComparePage.tsx
  📁 components/         # 재사용 컴포넌트들
    📄 BankLogo.tsx
    📄 Calculator.tsx
    📄 ItemList.tsx
    📄 Filter.tsx
  📁 hooks/              # 커스텀 훅들
    📄 useFetchMyLoans.ts
    📄 useCalculator.ts
    📄 useLoanFilter.ts
  📁 utils/              # 유틸리티 함수들
    📄 calculate.ts
    📄 format.ts

이 방식의 장점:

  • 기술적 역할이 명확해서 개발자가 이해하기 쉬움
  • 초기 설정이 간단하고 빠르게 시작 가능
  • 작은 프로젝트에서는 충분히 효과적

2단계: 기능 기반 구조 (features 중심)

프로젝트가 커지면서 기능별로 폴더를 나누게 됩니다:

text
📁 src/
  📁 features/          # 기능 중심으로 구성
    📁 loan-filtering/  # 대출 필터링 기능
      📁 components/
        📄 Filter.tsx
        📄 FilterButton.tsx
      📁 hooks/
        📄 useLoanFilter.ts
      📁 utils/
        📄 filterUtils.ts
    📁 loan-comparison/ # 대출 비교 기능
      📁 components/
        📄 CompareTable.tsx
        📄 CompareCard.tsx
      📁 hooks/
        📄 useComparison.ts
    📁 bank-selection/  # 은행 선택 기능
      📁 components/
        📄 BankList.tsx
        📄 BankLogo.tsx
      📁 hooks/
        📄 useBankData.ts
  📁 shared/            # 공통 컴포넌트
    📁 components/
      📄 Button.tsx
      📄 Input.tsx
    📁 utils/
      📄 calculate.ts
      📄 format.ts

기능 기반 구조의 장점:

  • 각 기능이 독립적으로 개발 가능
  • 기능별로 모든 관련 코드가 한 곳에 모임
  • 팀이 기능별로 작업할 때 효율적

하지만 이 구조의 한계:

Features 폴더가 커지면서 여러 도메인이 뭉치게 되어, 프로젝트가 더 커질 때 수정 범위를 예측하기 어려워집니다. 예를 들어:

  • loan-filtering이 신용대출, 대환대출, 주택담보대출 모두에 관련
  • bank-selection이 여러 금융상품에서 공통 사용
  • 특정 도메인(예: 대환대출) 변경 시 여러 features에 영향
text
📁 features/
  📁 loan-filtering/    # 🚨 여러 도메인이 뭉쳐 있음
    # 신용대출, 대환대출, 주택담보대출 모두 포함
  📁 bank-selection/    # 🚨 모든 금융상품에서 사용
  📁 rate-calculation/  # 🚨 다양한 상품 타입별 계산 로직 혼재

3단계: 도메인 기반 구조로의 발전

Features 중심 구조의 한계를 깨뜨리고, 도메인을 먼저 나누고 그 안에서 기능을 구성하는 방식으로 발전합니다:

text
📁 src/
  📁 domains/           # 도메인별로 먼저 분리
    📁 refinancing/     # 대환대출 도메인
      📁 features/      # 도메인 내 기능들
        📁 filtering/
          📁 components/
            📄 Filter.tsx
            📄 FilterButton.tsx
          📁 hooks/
            📄 useRefinancingFilter.ts
        📁 comparison/
          📁 components/
            📄 CompareTable.tsx
          📁 hooks/
            📄 useRefinancingComparison.ts
        📁 my-loans/
          📁 components/
            📄 MyLoanList.tsx
          📁 hooks/
            📄 useFetchMyLoans.ts
      📁 components/    # 도메인 공통 컴포넌트
        📄 RefinancingBankLogo.tsx
        📄 RefinancingCalculator.tsx
      📁 utils/         # 도메인별 유틸리티
        📄 refinancingCalculate.ts

    📁 loan-result/     # 신용 대출 조회 결과 도메인
      📁 features/
        📁 filtering/
          📁 components/
            📄 ResultFilter.tsx
          📁 hooks/
            📄 useResultFilter.ts
        📁 sorting/
          📁 components/
            📄 SortOptions.tsx
          📁 hooks/
            📄 useResultSort.ts
      📁 components/    # 도메인 공통 컴포넌트
        📄 LoanCard.tsx
        📄 ProductCard.tsx
      📁 utils/
        📄 loanResultUtils.ts

  📁 shared/            # 전체 앱 공통
    📁 components/
      📄 Button.tsx
      📄 Input.tsx
    📁 utils/
      📄 commonUtils.ts

도메인 기반 구조의 장점:

  • 도메인별로 명확하게 분리되어 책임이 명확함
  • 각 도메인 내에서 기능별로 다시 구성되어 응집도가 높음
  • 특정 도메인 변경 시 다른 도메인에 영향을 주지 않음
  • 대환대출, 신용대출 작업자가 각각 독립적으로 작업 가능

현재 우리 프로젝트의 실제 구조

위의 이상적인 구조를 바탕으로, 현재 우리 loans 프로젝트의 실제 src/ui/refinancing/ 구조:

text
📁 src/ui/refinancing/   # 대환대출 도메인
  📁 assets/            # 아이콘, 이미지
    📄 AllBankLogo.tsx
    📄 ArrowDown.tsx
    📄 Expand.tsx
  📁 components/        # 재사용 컴포넌트
    📁 common/
      📄 BankLogo.tsx
      📄 Toast.tsx
    📁 list/             # 목록 관련 기능
      📄 ItemList.tsx
      📄 Filter.tsx
    📁 detail/           # 상세 관련 기능
      📄 Calculator.tsx
      📄 Footer.tsx
  📁 hooks/             # 커스텀 훅
    📄 useFetchMyLoans.ts
    📄 useCalculator.ts
    📄 useLoanFilter.ts
  📁 compare/           # 비교 기능
    📁 components/
    📁 hooks/
  📁 my-loans/          # 내 대출 관리
    📁 hooks/

현재 구조의 개선된 점:

  • 대환대출 관련 모든 코드가 refinancing/ 폴더에 집중
  • 비즈니스 도메인이 명확하게 분리됨
  • 다른 도메인(신용대출, 주택담보대출)과 독립적

현재 구조의 한계점

하지만 실제 개발하다 보면 여전히 문제가 발생합니다:

문제 1: 기능 변경 시 여러 폴더 수정

대환대출 목록의 필터링 로직을 수정한다면:

text
📁 components/list/Filter.tsx        # UI 수정
📁 hooks/useLoanFilter.ts            # 로직 수정
📁 components/list/ItemList.tsx      # 목록 표시 수정
📁 utils.ts                         # 유틸리티 수정

문제 2: 역할별 폴더 내 복잡성 증가

components/ 폴더를 보면:

  • common/, list/, detail/, bridge/, choice/, rejected/로 다시 세분화
  • 어떤 컴포넌트가 어디에 있는지 찾기 어려움
  • 컴포넌트 간 의존성을 파악하기 어려움

문제 3: 크로스 도메인 의존성

typescript
// components/list/ItemList.tsx에서import { useFetchMyLoans } from '../../hooks/useFetchMyLoans';
import { BankLogo } from '../common/BankLogo';
import { calculateLoan } from '../../utils';
import { myLoansApi } from '../../my-loans/api';// 🚨 다른 기능 영역 침범

문제 4: 테스트와 유지보수의 어려움

실제 개발하면서 겪는 구체적인 문제들:

typescript
// 대환대출 필터 기능을 테스트하려면describe('Refinancing Filter', () => {
// 1. 컴포넌트 로직 (components/list/Filter.tsx)// 2. 비즈니스 로직 (hooks/useLoanFilter.ts)// 3. 유틸리티 로직 (utils.ts)// 4. API 로직 (여러 곳에 흩어져 있음)// → 모든 의존성을 추적하고 모킹해야 함
});

문제 5: 신규 개발자 온보딩 어려움

  • “대환대출 목록 필터링 버그를 고쳐주세요” → 어디서부터 봐야 할지 모름
  • components/list/, hooks/, utils/, compare/ 등을 모두 파악해야 함
  • 각 폴더의 역할과 책임이 명확하지 않음

다음 단계: FSD 구조

이런 문제들을 해결하기 위해 FSD(Feature-Sliced Design) 구조를 적용하면 더 체계적이고 확장 가능한 아키텍처를 구축할 수 있습니다.

FSD는 비즈니스 로직 중심의 계층적 아키텍처로, 위에서 겪었던 모든 문제점들을 체계적으로 해결하는 방법론입니다.

프론트엔드에서 폴더 구조 종류

실무에서 폴더 구조가 어떻게 진화하는지 우리 loans 프로젝트의 src/ui/refinancing/ 폴더를 예시로 살펴보겠습니다.

1단계: 역할 기반 구조 (프로젝트 초기)

프로젝트를 처음 시작할 때는 보통 이런 식으로 구조를 잡습니다:

text
📁 src/
  📁 pages/              # 페이지 컴포넌트들
    📄 RefinancingListPage.tsx
    📄 RefinancingDetailPage.tsx
    📄 RefinancingComparePage.tsx
  📁 components/         # 재사용 컴포넌트들
    📄 BankLogo.tsx
    📄 Calculator.tsx
    📄 ItemList.tsx
    📄 Filter.tsx
  📁 hooks/              # 커스텀 훅들
    📄 useFetchMyLoans.ts
    📄 useCalculator.ts
    📄 useLoanFilter.ts
  📁 utils/              # 유틸리티 함수들
    📄 calculate.ts
    📄 format.ts

이 방식의 장점:

  • 기술적 역할이 명확해서 개발자가 이해하기 쉬움
  • 초기 설정이 간단하고 빠르게 시작 가능
  • 작은 프로젝트에서는 충분히 효과적

2단계: 도메인 기반 구조 (현재 우리 프로젝트)

프로젝트가 커지면서 기능별로 폴더를 나누게 됩니다. 현재 우리의 src/ui/refinancing/ 구조가 바로 이것입니다:

text
📁 src/ui/refinancing/   # 대환대출 도메인
  📁 assets/            # 아이콘, 이미지
    📄 AllBankLogo.tsx
    📄 ArrowDown.tsx
    📄 Expand.tsx
  📁 components/        # 재사용 컴포넌트
    📁 common/
      📄 BankLogo.tsx
      📄 Toast.tsx
    📁 list/
      📄 ItemList.tsx
      📄 Filter.tsx
    📁 detail/
      📄 Calculator.tsx
      📄 Footer.tsx
  📁 hooks/             # 커스텀 훅
    📄 useFetchMyLoans.ts
    📄 useCalculator.ts
    📄 useLoanFilter.ts
  📁 compare/           # 비교 기능
    📁 components/
    📁 hooks/
  📁 my-loans/          # 내 대출 관리
    📁 hooks/

개선된 점:

  • 대환대출 관련 모든 코드가 refinancing/ 폴더에 집중
  • 비즈니스 도메인이 명확하게 분리됨
  • 다른 도메인(신용대출, 주택담보대출)과 독립적

현재 구조의 한계점

하지만 실제 개발하다 보면 여전히 문제가 발생합니다:

문제 1: 기능 변경 시 여러 폴더 수정 대환대출 목록의 필터링 로직을 수정한다면:

text
📁 components/list/Filter.tsx        # UI 수정
📁 hooks/useLoanFilter.ts            # 로직 수정
📁 components/list/ItemList.tsx      # 목록 표시 수정
📁 utils.ts                         # 유틸리티 수정

문제 2: 역할별 폴더 내 복잡성 증가 components/ 폴더를 보면:

  • common/, list/, detail/, bridge/, choice/, rejected/로 다시 세분화
  • 어떤 컴포넌트가 어디에 있는지 찾기 어려움
  • 컴포넌트 간 의존성을 파악하기 어려움

문제 3: 크로스 도메인 의존성

typescript
// components/list/ItemList.tsx에서
import { useFetchMyLoans } from '../../hooks/useFetchMyLoans';
import { BankLogo } from '../common/BankLogo';
import { calculateLoan } from '../../utils';
import { myLoansApi } from '../../my-loans/api';// 🚨 다른 기능 영역 침범

문제 4: 테스트와 유지보수의 어려움 실제 개발하면서 겪는 구체적인 문제들:

typescript
// 대환대출 필터 기능을 테스트하려면
describe('Refinancing Filter', () => {
// 1. 컴포넌트 로직 (components/list/Filter.tsx)
// 2. 비즈니스 로직 (hooks/useLoanFilter.ts)
// 3. 유틸리티 로직 (utils.ts)
// 4. API 로직 (여러 곳에 흩어져 있음)
// → 모든 의존성을 추적하고 모킹해야 함
});

문제 5: 신규 개발자 온보딩 어려움

  • “대환대출 목록 필터링 버그를 고쳐주세요” → 어디서부터 봐야 할지 모름
  • components/list/, hooks/, utils/, compare/ 등을 모두 파악해야 함
  • 각 폴더의 역할과 책임이 명확하지 않음

3단계: FSD 구조로의 해결

이런 문제들을 해결하기 위해 FSD 구조를 적용하면:

text
📁 src/v2/
  📁 entities/
    📁 loan/              # 대출 도메인 엔티티
      📁 model/
        📄 types.ts       # 대출 타입 정의
        📄 store.ts       # 대출 상태 관리
      📁 api/
        📄 loanApi.ts     # 대출 API 호출
      📄 index.ts         # Public API

    📁 bank/              # 은행 도메인 엔티티
      📁 model/
        📄 types.ts
      📁 ui/
        📄 BankLogo.tsx   # 은행 로고 컴포넌트
      📄 index.ts

  📁 features/
    📁 loan-filtering/    # 대출 필터링 기능
      📁 ui/
        📄 Filter.tsx
        📄 FilterButton.tsx
      📁 model/
        📄 filterStore.ts
      📁 lib/
        📄 filterUtils.ts
      📄 index.ts         # Public API: export { Filter, useFilter }

    📁 loan-comparison/   # 대출 비교 기능
      📁 ui/
        📄 CompareTable.tsx
      📁 model/
        📄 compareLogic.ts
      📄 index.ts

  📁 widgets/
    📁 loan-list/         # 대출 목록 위젯
      📁 ui/
        📄 LoanList.tsx
      📄 index.ts

  📁 pages/
    📁 refinancing-list/  # 대환대출 목록 페이지
      📁 ui/
        📄 RefinancingListPage.tsx
      📄 index.ts

FSD 구조의 장점:

  1. 높은 응집도: 필터링 기능 수정 시 features/loan-filtering/ 하나만 수정
  2. 명확한 의존성: 각 레이어의 Public API를 통한 명확한 인터페이스
  3. 테스트 용이성: 각 feature는 독립적으로 테스트 가능
  4. 팀 협업: “필터링 버그” → features/loan-filtering/ 바로 이동 가능

기존 폴더 구조

아래 폴더 구조는 일반적인 작은 규모의 프론트엔드 프로젝트에서 사용되는 폴더 구조입니다.

많은 프로젝트들이 역할 기반으로 이러한 구조를 사용하고 있습니다:

text
📁 src
  📁 actions          # Redux actions
    📁 product
    📁 order
  📁 api             # API 호출 함수들
  📁 components      # 재사용 가능한 컴포넌트들
  📁 containers      # 컨테이너 컴포넌트들
  📁 constants       # 상수 정의
  📁 i18n           # 국제화 파일들
  📁 modules        # 비즈니스 로직 모듈들
  📁 helpers        # 헬퍼 함수들
  📁 routes         # 라우팅 관련
    📁 products.jsx
    📄 products.[id].jsx
  📁 utils          # 유틸리티 함수들
  📁 reducers       # Redux reducers
  📁 selectors      # Redux selectors
  📁 styles         # 스타일 파일들
  📄 App.jsx
  📄 index.js

이러한 구조는 역할(Role) 기반 분류로, 기술적 역할에 따라 파일들을 분리합니다.

기존 폴더 구조의 문제점

FSD 문서에서 지적하는 분산된 모듈 구조의 문제점:

역할 기반 구조는 다음과 같은 근본적인 문제를 가지고 있습니다:

  • 변화가 있을 때, 변경이 폴더 여러군데서 발생함: 하나의 기능을 수정하려면 components/, hooks/, helpers/, constants/ 등 여러 폴더를 동시에 수정해야 합니다
  • 응집도가 낮음: 관련된 코드들이 물리적으로 분산되어 있어 기능의 전체적인 모습을 파악하기 어렵습니다
  • 도메인 로직의 파편화: 특정 도메인(예: credit, credit-refinancing, house-refinancing)과 관련된 코드가 여러 디렉토리에 흩어져 있음

예시: Delivery 기능을 수정할 때

text
📁 components/loanCard     # UI 컴포넌트
📁 hooks/loan.js         # Redux actions
📁 helpers/loan.js         # 헬퍼 함수들
📁 constants/loan.js       # 상수들
📁 entities/loan/          # 비즈니스 로직

이렇게 최소 5개의 다른 폴더에서 파일을 수정해야 하며, 이는 낮은 응집도(Low Cohesion)와 높은 결합도(High Coupling)를 의미합니다.

좋은 설계를 하기 위한 조건

소프트웨어 아키텍처에서 좋은 설계의 핵심 원칙은 다음과 같습니다:

  • 높은 응집도(High Cohesion): 관련된 기능들은 가까이 모여 있어야 합니다
  • 낮은 결합도(Low Coupling): 서로 다른 모듈 간의 의존성은 최소화되어야 합니다

FSD에서 제시하는 분리된(Segregated) 모듈 구조는 이 문제를 해결합니다:

text
📁 entities/
  📁 loan/
    📁 ui/              # ~ components/
      📄 card.js
      📄 choice.js
    📁 model/           # 비즈니스 로직
      📄 actions.js
      📄 constants.js
      📄 selectors.js
    📁 lib/             # ~ helpers
      📄 utils.js
  📁 user/

이렇게 하면 대출 관련 모든 코드가 entities/loan/ 하나의 디렉토리에 집중되어 높은 응집도를 달성할 수 있습니다.

이는 단일 책임 원칙(Single Responsibility Principle)과도 연결되며, 하나의 변경 사유는 하나의 모듈에만 영향을 주어야 한다는 원칙입니다.


FSD 아키텍처란?

  • Feature-Sliced Design(FSD)는 프론트엔드 애플리케이션을 위한 비즈니스 로직 중심의 계층적 아키텍처 방법론입니다.

FSD의 핵심 설계 철학

FSD는 단순한 “기능 중심” 아키텍처가 아닌, 복합적 접근 방식을 채택합니다:

비즈니스 로직 중심 설계: FSD의 근본 원칙은 “Splitting application by business logic” (비즈니스 로직에 따른 애플리케이션 분할)입니다. 이는 기술적 구현보다 비즈니스 가치와 도메인 로직을 우선시하는 접근법입니다.

하이브리드 아키텍처 접근법

FSD는 다양한 아키텍처 패턴의 장점을 결합한 하이브리드 방법론입니다:

1. Domain-Driven Design (DDD) 영향

  • entities 레이어: 비즈니스 도메인의 핵심 개념을 완전히 캡슐화
  • 도메인 중심의 코드 구성으로 비즈니스 복잡성 관리

2. Clean Architecture 원칙

  • 계층적 의존성 규칙: 상위 레이어는 하위 레이어만 의존
  • Public API를 통한 명확한 경계 분리

3. Feature-Driven Development

  • features 레이어: 사용자 인터랙션과 완전한 비즈니스 기능 구현
  • 기능별 독립적 개발과 배포 가능

4. Component-Based Architecture

  • widgets: 재사용 가능한 UI 블록으로 컴포넌트 기반 설계

3차원 구조 체계

FSD는 3차원의 구조적 조직화를 통해 복잡성을 관리합니다:

text
📁 src/
  📁 app/           # Layer (계층)
  📁 pages/         # Layer (계층)
    📁 product/     # Slice (도메인/기능별 분할)
      📁 ui/        # Segment (기술적 목적별 분할)
        📄 ProductPage.tsx
      📁 api/       # Segment (기술적 목적별 분할)
        📄 getProduct.ts
      📁 model/     # Segment (기술적 목적별 분할)
        📄 types.ts
  📁 features/      # Layer (계층)
    📁 auth/        # Slice (도메인/기능별 분할)
      📁 ui/        # Segment (기술적 목적별 분할)
      📁 model/     # Segment (기술적 목적별 분할)

1. Layers (논리적 계층구조)

각 레이어는 서로 다른 분할 기준을 사용합니다:

  • entities: 도메인 중심 (User, Product, Order)
  • features: 기능 중심 (로그인, 댓글 작성, 장바구니)
  • pages: 라우트 중심 (URL과 대응되는 페이지)
  • widgets: UI 블록 중심 (Header, Footer, ProductGrid)

2. Slice (도메인/기능별 분할)

각 레이어 내에서 비즈니스 도메인이나 사용자 기능별로 분할

3. Segment (기술적 목적별 분할)

각 슬라이스 내에서 기술적 관심사별로 분할:

  • ui: 사용자 인터페이스와 표시 로직
  • model: 비즈니스 로직, 상태 관리, 데이터 조작
  • api: 백엔드 상호작용, 요청 함수, 데이터 매핑
  • lib: 재사용 가능한 라이브러리 코드
  • config: 설정 파일, 기능 플래그

왜 이런 복합적 접근이 필요한가?

단일 기준의 한계: 순수한 기능 중심이나 도메인 중심 구조만으로는 프론트엔드의 복잡성을 완전히 해결할 수 없습니다. FSD는 각 레이어의 특성에 맞는 최적의 분할 기준을 적용하여:

  1. entities: 도메인 무결성 보장
  2. features: 사용자 기능의 독립성 확보
  3. pages: 라우팅과의 자연스러운 연결
  4. widgets: UI 재사용성 최대화

이렇게 비즈니스 로직을 중심으로 하되, 상황에 맞는 최적의 분할 전략을 사용하는 것이 FSD의 핵심 설계 철학입니다.

FSD가 필요한 진짜 이유

실무에서 겪는 고통 포인트들

1. 코드 리뷰의 악몽

typescript
// PR: "대환대출 목록 필터 개선"// 변경된 파일:
- components/list/Filter.tsx
- components/list/ItemList.tsx
- hooks/useLoanFilter.ts
- hooks/useFetchMyLoans.ts
- utils.ts
- compare/hooks/useChoiceBankList.ts// 🤔 왜 여기도?

리뷰어 입장에서 전체 맥락을 파악하기 어려움

2. 버그 추적의 미로

text
"필터 적용 시 은행 로고가 깨져요"
→ components/list/Filter.tsx?
→ components/common/BankLogo.tsx?
→ hooks/useLoanFilter.ts?

→ 결국 compare/components/BankItem.tsx에서 발견 😱

3. 기능 확장 시 영향 범위 예측 불가

typescript
// "내 대출에 새 은행 추가" 요청
// 수정해야 할 곳들을 찾기 위해 전체 코드베이스 검색 필요
grep -r "BANK_CODE" src/ui/refinancing/
// 결과: 27개 파일에서 발견 😨

FSD로 해결되는 구체적 문제들

1. 변경 범위의 명확성

typescript
// AS-IS: "필터 개선" → 6개 폴더, 12개 파일 수정
// TO-BE: "필터 개선" → features/loan-filtering/ 하나만 수정

// FSD에서는
// features/loan-filtering/index.ts
export { Filter, FilterButton } from './ui';
export { useFilter, filterStore } from './model';

2. 의존성 투명성

typescript
// pages/refinancing-list/ui/RefinancingListPage.tsx
import { LoanList } from 'widgets/loan-list';
import { Filter } from 'features/loan-filtering';
import { Bank } from 'entities/bank';

// 한눈에 봐도 의존성이 명확함

3. 팀 협업 효율성

  • 백엔드 개발자: “대출 API 스펙 변경” → entities/loan/api/ 수정
  • 디자이너: “필터 UI 개선 요청” → features/loan-filtering/ui/ 확인
  • QA: “비교 기능 버그” → features/loan-comparison/ 테스트

FSD 규칙들

FSD는 아키텍처 무결성을 보장하기 위해 엄격한 규칙을 제시합니다. 이 규칙들은 공식 문서에서 명시된 핵심 원칙입니다:

1. 레이어 간 Import 규칙 (Import Rule on Layers)

핵심 규칙: 슬라이스 내의 모듈은 엄격히 하위에 위치한 레이어의 슬라이스만 import할 수 있습니다.

typescript
// ❌ 금지: features/auth에서 features/product 의존 (같은 레이어)import { ProductCard } from "features/product"

// ❌ 금지: entities에서 features 의존 (상위 레이어)import { AuthForm } from "features/auth"

// ✅ 허용: features에서 entities 의존 (하위 레이어)import { User } from "entities/user"

// ✅ 허용: 하위 layer에서 가져오기import { Button } from "shared/ui"

목적: 아키텍처 무결성 강화 및 순환 의존성 방지

2. Public API 규칙 (Public API Rule on Slices)

핵심 규칙: 모든 슬라이스(및 슬라이스가 없는 레이어의 세그먼트)는 반드시 Public API 정의를 포함해야 합니다.

typescript
// entities/user/index.ts (Public API)export { User } from "./ui/User"
export { userModel } from "./model/userModel"
export type { UserType } from "./model/types"

// ❌ 금지: 내부 구조 직접 접근import { UserType } from "entities/user/model/types"

// ✅ 허용: Public API 사용import { UserType } from "entities/user"

Public API가 필요한 이유:

  • 캡슐화: 슬라이스 구현 세부사항을 숨기고 안정적인 인터페이스 제공
  • 리팩토링 안전성: 내부 구조 변경 시 외부에 영향을 주지 않음
  • 명확한 의존성 관리: 모듈 간 의존성을 명시적으로 관리

3. 엔티티 간 Cross-Import (@x 표기법)

엔티티 레이어에서는 상호 의존성이 흔하기 때문에 @x 표기법을 사용한 특별한 Public API를 제공합니다:

typescript
// entities/song/@x/artist.ts (Cross-import API)
export type { Song } from "../model/song.ts"

// entities/artist/model/artist.ts
import type { Song } from "entities/song/@x/artist"

export interface Artist {
  name: string;
  songs: Array<Song>;
}

@x 표기법의 의미: “A crossed with B” - A와 B의 교차점

배럴 파일의 성능 이슈와 해결책

FSD 공식 문서에서 언급하는 실제 성능 이슈:

  • 프로덕션: 트리쉐이킹이 완벽하지 않아 번들 크기 증가
  • 개발환경: 트리쉐이킹이 없어 전체 모듈 로딩으로 인한 속도 저하

FSD 공식 권장 해결책: 세분화된 Public API 구조

typescript
// ❌ 잘못된 패턴: 와일드카드 re-exports
export * from "./ui/Comment"

// 👎 권장하지 않음
export * from "./model/comments"

// 💩 나쁜 practice
// ❌ 큰 배럴 파일
// shared/ui/index.ts - 100개 이상의 컴포넌트
// ✅ FSD 권장 구조:
export import { Button } from "shared/ui/button"
import { TextField } from "shared/ui/text-field"

추가 고려사항:

  • Discoverability 저하: 와일드카드 exports는 어떤 것이 export되는지 파악하기 어렵게 만듦
  • 내부 모듈 노출: 의도하지 않은 내부 모듈이 외부에 노출될 위험
  • 리팩토링 어려움: 외부에서 내부 구조에 의존할 가능성

FSD Layers

FSD는 공식적으로 6개의 layer로 구성됩니다 (process는 v2에서 deprecated).

FSD 공식 명명 규칙에 따른 각 레이어의 정의:

app

  • 공식 정의: 애플리케이션 진입점과 설정 (Application entry point and configuration)
  • 특징: slice를 사용하지 않음 (도메인이 없기 때문)
  • 포함 내용: providers, 라우팅 설정, 전역 스타일, 앱 설정
typescript
📁 app/
  📁 providers/
    📄 index.tsx      # Provider 조합
  📁 routes/
    📄 index.tsx      # 라우팅 설정
  📄 index.css        # 전역 스타일

pages

  • 공식 정의: 라우트에 대응하는 고수준 UI 구조 (High-level UI structure, often corresponding to routes)
  • 특징: 일반적으로 코드량에 제한이 없음
  • 역할: 다른 layer들을 조합하여 완전한 페이지 구성
typescript
📁 pages/
  📁 feed/
  📁 sign-in/
  📁 article-read/
  📁 article-edit/
  📁 profile/
  📁 settings/

widgets

  • 공식 정의: 재사용 가능한 큰 UI 블록 (Large reusable UI blocks)
  • 특징: 비즈니스 로직을 포함할 수 있는 큰 UI 블록
  • 예시: Header, Footer, Sidebar, ProductGrid, LoginDialog
typescript
📁 widgets/
  📁 header/
    📁 ui/
      📄 Header.tsx
    📁 model/
      📄 navigation.ts
  📁 login-dialog/
    📁 ui/
      📄 LoginDialog.tsx
    📄 index.ts

features

  • 공식 정의: 특정 비즈니스 도메인 기능 (Specific business domain functionality)
  • 특징: 사용자와 상호작용이 있는 완전한 비즈니스 기능
  • 예시: 로그인, 장바구니 추가, 댓글 작성
typescript
📁 features/
  📁 add-to-cart/
    📁 ui/
      📄 AddToCartButton.tsx
    📁 model/
      📄 cart.ts
    📁 api/
      📄 addToCart.ts

Redux 슬라이스 분류:

  • Features: 사용자 행동이나 특정 기능 (예: comments)
  • Entities: 비즈니스 도메인 개념 (예: products, users)

entities

  • 공식 정의: 핵심 비즈니스 엔티티와 로직 (Core business entities and their logic)
  • 특징: 비즈니스 로직의 핵심, 다른 entities와 독립적
  • 포함 가능: model, api, ui 모두 포함 가능
typescript
📁 entities/
  📁 user/
    📁 model/
      📄 types.ts      # User 타입 정의
      📄 store.ts      # User 상태 관리
    📁 api/
      📄 getUser.ts    # User API 호출
    📁 ui/
      📄 UserCard.tsx  # User 표시 컴포넌트

entities에서 API와 UI가 가능한 이유: FSD 공식 문서에 따르면 entities는 순수한 데이터 모델이 아니라 비즈니스 도메인의 완전한 표현이기 때문입니다.

shared

  • 공식 정의: 재사용 가능한 컴포넌트, 유틸리티, 타입 (Reusable components, utilities, and types)
  • 특징: slice를 사용하지 않음 (도메인이 없기 때문)
  • 표준 세그먼트: ui, lib, api, config

FSD 공식 가이드라인에 따른 shared 레이어 리팩토링:

typescript
📁 shared/
  📁 ui/              # components/, containers/ → shared/ui
    📁 button/
    📁 input/
  📁 lib/             # helpers/, utils/ → shared/lib (기능별 그룹화)
    📁 date/
    📁 format/
  📁 api/
    📄 client.ts
  📁 config/          # constants/ → shared/config (기능별 그룹화)
    📄 constants.ts

표준 Segments

FSD는 기술적 목적에 따른 표준 세그먼트를 정의합니다:

  • ui: UI 표시 로직, 컴포넌트, 포매터, 스타일
  • api: 백엔드 상호작용, 요청 함수, 데이터 매퍼
  • model: 데이터 모델, 스키마, 인터페이스, 스토어, 비즈니스 로직
  • lib: 다른 모듈에서 필요한 재사용 가능한 라이브러리 코드
  • config: 설정 파일, 기능 플래그

피해야 할 세그먼트: ‘components’, ‘actions’, ‘types’, ‘utils’ 대신 목적별로 그룹화

Next.js에서 FSD 적용하기

Next.js와 FSD를 함께 사용할 때는 라우팅 시스템과의 충돌을 고려해야 합니다.

권장 구조

FSD 공식 가이드라인에서는 다음 구조를 제안합니다:

App Router 사용 시

text
📁 app/                 # Next.js App Router
📁 pages/               # Next.js Pages Router (호환성)
📁 src/
  📁 app/               # FSD app layer
  📁 pages/             # FSD pages layer (views로도 사용한다고 함)
  📁 widgets/
  📁 features/
  📁 entities/
  📁 shared/

Pages Router 사용 시

text
📁 pages/               # Next.js Pages Router
📁 src/
  📁 app/               # FSD app layer
  📁 pages/             # FSD pages layer
  📁 widgets/
  📁 features/
  📁 entities/
  📁 shared/

라우팅 연결

Next.js의 라우팅과 FSD pages를 연결하는 방법:

typescript
// pages/product/[id].tsx (Next.js 라우팅)
export { default } from "src/pages/product-detail"
export { loader } from "src/pages/product-detail"

// src/pages/product-detail/index.ts (FSD pages)
export { ProductDetailPage as default } from "./ui/ProductDetailPage"
export { loader } from "./api/loader"

app 폴더 통합

typescript
// pages/_app.tsx
import "src/app/styles/index.scss"
export { default } from "src/app/providers"

// src/app/providers/index.tsx
export default function App({ Component, pageProps }: AppProps) {
  return (
    <Provider1>
      <Provider2>
        <BaseLayout>
          <Component {...pageProps} />
        </BaseLayout>
      </Provider2>
    </Provider1>
  )
}

Loans FSD 마이그레이션 예시

기존 구조 (AS-IS): 역할 기반 분산 구조

현재 loans 프로젝트의 기존 src/ 폴더는 전형적인 역할 기반(Role-based) 구조를 보여줍니다. 그러면서 도메인 기반도 볼 수 있으나 체계적이지 않습니다.

text
📁 src/
  📁 api/                    # API 호출 함수들
    📁 card/
      📄 detail.ts
      📄 home.ts
    📁 tms/
      📄 tms.ts
  📁 hooks/                  # 재사용 훅들
    📄 useIsApp.ts
    📄 usePlatform.ts
    📄 useStopwatch.ts
  📁 state/                  # 상태 관리 (Redux)
    📁 reducer/
      📁 product/
        📄 product.ts
        📄 interface.ts
      📁 bank/
        📄 bank.ts
    📁 query/
      📁 loan/
  📁 ui/                     # UI 컴포넌트들
    📁 loan-result/
      📁 component/
        📄 LoanList.tsx
        📄 ProductCard.tsx
      📁 hooks/
        📄 useFetchLoanApplyList.ts
      📄 index.tsx
    📁 contract/
      📁 components/
      📁 hooks/
    📁 refinancing/
      📁 assets/
      📁 hooks/
  📁 utils/                  # 유틸리티 함수들
    📄 calculate.ts
    📄 number.ts

기존 구조의 문제점

1. 낮은 응집도 (Low Cohesion)

  • 대출 신청 기능 하나를 수정하려면 여러 폴더를 동시에 수정해야 함

  • 예: 대출 상품 기능 변경 시

    text
    📁 src/api/loan/           # API 수정
    📁 src/state/reducer/product/ # 상태 관리 수정
    📁 src/ui/loan-result/     # UI 컴포넌트 수정
    📁 src/hooks/              # 공통 훅 수정
    📁 src/utils/              # 유틸리티 수정

2. 높은 결합도 (High Coupling)

  • UI 컴포넌트가 여러 폴더의 모듈에 의존

  • 예: src/ui/loan-result/index.tsx에서

    typescript
    import { useGetUserInputLatestQuery } from '@/state/query/loan/credit';
    import useFetchLoanApplyList from './hooks/useFetchLoanApplyList';
    import { selectInquiryRequest } from '@/state/reducer/faas/selectors';
    import { calculateLoan } from '@/utils/calculate';

3. 비즈니스 로직의 파편화

  • 대출(loan) 도메인 관련 코드가 5-6개 폴더에 분산
  • 새로운 개발자가 대출 기능 전체를 파악하기 어려움

현재 faas 구조 평가해보기

typescript
// entities/faas-inquiry/ - 대출 조회 도메인
📁 entities/faas-inquiry/
  📁 api/
    📄 query.ts              # API 호출 함수
    📄 types.ts              # API 타입 정의
  📁 model/
    📄 types.ts              # 비즈니스 도메인 타입
  📄 index.ts                # Public API

// features/faas/loan-list/ - 대출 목록 기능
📁 features/faas/loan-list/
  📁 ui/
    📄 ItemList.tsx          # 목록 컴포넌트
    📄 CreditItem.tsx        # 개별 아이템
  📁 lib/
    📄 calculate.ts          # 계산 로직
    📄 sort.ts               # 정렬 로직
  📄 index.ts                # Public API: export { ItemList, CreditItem }

// pages-flat/faas/item-list/ - 페이지 조합
📁 pages-flat/faas/item-list/
  📁 ui/
    📄 CreditItemListPage.tsx # 페이지 컴포넌트
  📄 index.ts                # Public API

결론

Feature-Sliced 방법론의 핵심 원칙:

  • 표준화된 프론트엔드 프로젝트 구조: 모든 프로젝트에서 일관성 있는 구조 제공
  • 비즈니스 로직에 따른 애플리케이션 분할: 도메인 중심의 논리적 구조
  • 격리된 기능 사용: 암묵적 부작용과 순환 의존성 방지
  • Public API 사용: 내부 모듈 구현에 대한 직접 접근 금지

이러한 원칙을 통해 우리가 얻는 가치는:

  • 코드 탐색을 원활하게 하기 위해서: 개발자가 특정 기능을 찾을 때 직관적으로 위치를 예측할 수 있어야 합니다
  • 모노레포 구조에서 다른 프로젝트여도 구조를 쉽게 이해하기 위해서: 일관된 구조는 팀 간 협업과 프로젝트 이동을 용이하게 만듭니다
  • 변화에 유연하게 대처하기 위해서: 비즈니스 요구사항의 변화에 따라 코드 수정이 최소화되어야 합니다

FAQ

1. pages/ui와 widgets 차이, 기준

FSD 공식 문서의 명확한 구분:

pages/ui:

  • 정의: 특정 라우트에 대응하는 고수준 UI 구조
  • 특징: 다른 pages에서 재사용되지 않음, 라우팅과 직접 연결
  • 역할: 다른 layer들(widgets, features, entities)을 조합하여 완전한 페이지 구성

widgets:

  • 정의: 재사용 가능한 큰 UI 블록 (Large reusable UI blocks)
  • 특징: 여러 페이지에서 재사용 가능, 독립적인 비즈니스 로직 포함 가능
  • 예시: Header, Footer, Sidebar, ProductGrid, LoginDialog
typescript
// pages/home/ui/HomePage.tsx - 특정 페이지용 (라우트 종속)
export function HomePage() {
  return (
    <div>
      <Header />        {/* widgets/header에서 가져옴 */}
      <ProductGrid />   {/* widgets/product-grid에서 가져옴 */}
    </div>
  )
}

// widgets/header/ui/Header.tsx - 재사용 가능한 큰 UI 블록
export function Header() {
  return <header>...</header>
}

// widgets/login-dialog/ui/LoginDialog.tsx - 재사용 가능한 로그인 다이얼로그
export function LoginDialog() {
  return <dialog>...</dialog>
}

2. Next.js pages가 있는데 FSD의 pages가 필요한가? 둘의 차이, 기준

FSD 공식 Next.js 가이드에서 제시하는 명확한 분리:

Next.js pages (파일 시스템 라우팅):

  • 역할: URL 구조 정의, 파일 시스템 기반 라우팅
  • 특징: 얇은 wrapper 역할, 프레임워크 종속적
  • 위치: 프로젝트 루트의 pages/ 또는 app/

FSD pages (비즈니스 로직):

  • 역할: 비즈니스 로직과 UI 조합, 완전한 페이지 구성
  • 특징: 테스트 가능한 컴포넌트, 프레임워크 독립적
  • 위치: src/pages/ (FSD 레이어)
typescript
// pages/product/[id].tsx (Next.js - 라우팅만, 얇은 wrapper)
export { default } from "src/pages/product-detail"
export { loader } from "src/pages/product-detail"

// src/pages/product-detail/index.ts (FSD - 비즈니스 로직, Public API)
export { ProductDetailPage as default } from "./ui/ProductDetailPage"
export { loader } from "./api/loader"

// src/pages/product-detail/ui/ProductDetailPage.tsx (실제 페이지 컴포넌트)
export function ProductDetailPage() {
	// 비즈니스 로직과 UI 조합
}

FSD 공식 권장 구조:

text
├── pages              # Next.js pages (파일 시스템 라우팅)
├── src
│   ├── app            # FSD app layer
│   ├── pages          # FSD pages layer (비즈니스 로직)
│   ├── widgets
│   ├── features
│   ├── entities
│   └── shared

3. entities에선 각 entity는 어떤 역할을 해야 하나? features와 정확한 차이, ui도 가능하고, api 호출도 가능한가?

FSD 공식 정의에 따른 명확한 구분:

entities (핵심 비즈니스 엔티티와 로직):

  • 역할: 비즈니스 도메인의 핵심 개념 (Loan, User, Bank, 등)
  • 특징: 다른 entities와 독립적, 순수한 도메인 로직
  • 포함 가능: model, api, ui 모든 세그먼트 포함 가능

features (특정 비즈니스 도메인 기능):

  • 역할: 사용자 인터랙션과 완전한 비즈니스 기능
  • 특징: entities를 조합하여 완전한 기능 제공

FSD 공식 Redux 슬라이스 분류:

  • Entities: 비즈니스 도메인 개념 (products, users)
  • Features: 사용자 행동이나 특정 기능 (comments, authentication)
typescript
// entities/user/ - 비즈니스 도메인 개념
📁 model/
  📄 types.ts          # User 타입 정의
  📄 userSlice.ts      # User Redux 슬라이스
📁 api/
  📄 getUser.ts        # User 조회 API
  📄 updateUser.ts     # User 업데이트 API
📁 ui/
  📄 UserCard.tsx      # User 표시 컴포넌트
  📄 UserAvatar.tsx    # User 아바타 컴포넌트

// features/auth/ - 사용자 인터랙션 기능
📁 ui/
  📄 LoginForm.tsx     # 로그인 폼
📁 api/
  📄 login.ts          # 로그인 API 호출
📁 model/
  📄 authSlice.ts      # 인증 상태 관리

entities에서 API와 UI가 가능한 핵심 이유:

FSD 공식 문서에 따르면, entities는 “단순한 데이터 타입이 아니라 비즈니스 도메인의 완전한 표현”입니다.

따라서:

  • UI 포함 가능: User를 표시하는 UserCard, UserAvatar 등
  • API 포함 가능: User CRUD 작업을 위한 모든 API 함수들
  • Model 포함: User 타입, 상태 관리, 비즈니스 로직

이는 도메인 중심 설계(Domain-Driven Design)의 원칙과 일치하며, 각 entity가 해당 도메인의 모든 측면을 완전히 캡슐화할 수 있도록 합니다.