far

FSD(Feature-Sliced Design) 실무 도입기 (React, Next.js) 본문

아키텍쳐

FSD(Feature-Sliced Design) 실무 도입기 (React, Next.js)

Eater 2024. 4. 22. 11:43

0. 들어가며

회사에서 멀티레포를 사용하고 있었는데, 프로젝트를 처음 시작할 때 각 리포지토리를 다른 사람들이 설정하고 작업하다 보니 아키텍처 구조가 미묘하게 달랐다. 이로 인해 작업하면서 시간이 지연되거나 헷갈리는 부분들이 많았다.

마침 회사의 업무 방식이 변화하면서, 프로젝트를 전체적으로 리뉴얼하자는 논의가 나왔는데 이 기회를 활용해 모든 리포지토리를 정리하고 싶은 마음이 들었다. 특히 회사의 패턴 라이브러리 디자인 시스템과 호환이 잘 될 것으로 보이는 FSD(Feature-Sliced Design) 아키텍처를 도입할 것을 제안했다.

1. FSD 아키텍쳐란?

FSD(Feature-Sliced Design) 아키텍처는 소프트웨어 시스템을 기능 중심으로 나누어 모듈화하고, 각 기능을 독립적으로 관리하는 방식이다. 이 아키텍처는 특히 대규모 프론트엔드 애플리케이션에서 코드의 유지보수성과 재사용성을 높이기 위해 사용된다. FSD 아키텍처는 각 기능을 독립적인 도메인으로 분리하여 개발할 수 있도록 하고, 코드의 복잡도를 줄이며, 팀 협업을 개선하는 데 목적이 있다.

2. FSD 아키텍처의 기본 개념

FSD는 기능 중심(Feature-Oriented) 접근 방식을 사용한다. 이를 통해 프로젝트가 점진적으로 성장하면서도 유지보수가 용이하게 설계된다. FSD 아키텍처의 기본 원칙은 다음과 같다.

  1. 기능 기반 분할: 애플리케이션을 기능별로 나누어 각 기능을 독립적인 모듈로 관리하며, 기능은 시스템에서 특정한 비즈니스 로직이나 사용자 인터페이스의 한 부분을 담당한다.
  2. 레이어링: 기능을 레이어로 나누어 책임을 분리한다. 일반적으로 Presentation, Domain, Data 레이어로 구성되며, 이는 기능에 따라 구체화된다.
  3. 폴더 구조: 기능별로 폴더 구조를 구성하여 코드의 가독성과 유지보수성을 높이며, 각 폴더는 그 기능과 관련된 모든 것을 포함한다.
  4. 재사용성: 공통 기능이나 유틸리티는 재사용 가능하도록 Shared 레이어에 배치한다. 이를 통해 중복 코드를 줄이고 일관성을 유지한다.

2-1. 세부구조

Processes는 현재 제거되었음.

  • Layers (레이어): 프로젝트의 논리적 단위이며, 각 레이어는 특정 역할을 담당한다. 이는 다른 레이어에 의존하거나 영향을 미칠 수 있다.
  • Slices (슬라이스): 기능별로 나뉜 모듈들을 의미한다. 사진처럼, User, Post, Comment등의 슬라이스가 있을 수 있다.
  • Segments (세그먼트): 각 슬라이스 내에서 기능을 더 세분화한 단위이다. 일반적으로 UI, Model, API로 구분된다.

2-2. 레이어

Processes는 현재 제거되었음.

  • App: 프로젝트의 최상위 계층으로, shared, entities, features, widgets, pages를 사용할 수 있으며, 다른 계층에 의존하지 않는다.
  • Processes: 더이상 사용하지 않음
  • Pages: 개별 페이지의 구현을 담당하며, app 계층에 의존한다.
  • Widgets: 재사용 가능한 UI 구성 요소를 포함하며, pages, app 계층에 사용될 수 있다.
  • Features: 특정 기능을 구현하는 데 필요한 모든 것을 포함하며, widgets, pages , app 계층에서 사용된다.
  • Entities: 도메인 모델과 관련된 모든 것을 포함하며, 상위 모든 계층에서 사용될 수 있다.
  • Shared: 모든 계층에서 사용될 수 있는 유틸리티와 공통 컴포넌트들을 포함한다.

3. FSD 아키텍처의 폴더 구조 예시

React사용시 폴더구조 예시는 아래와 같다. 처음 도입할 때 공식문서의 Example에 올라와있는 구조들을 참고하면서 작성했는데, 대부분 틀은 비슷했다.

src/
│
├── app/                      # 애플리케이션 초기화 및 글로벌 설정
│   ├── routes/             # 라우팅 설정
│   ├── providers/            # 애플리케이션 레벨에서 제공되는 프로바이더
│   │   ├── ThemeProvider.tsx # 테마 관련 프로바이더
│   │   ├── AuthProvider.tsx  # 인증 관련 프로바이더
│   │   ├── index.ts          # Provider Export
│   │   └── ...               # 기타 전역 프로바이더들
│   └── ...                   # 다른 글로벌 설정들
│
├── pages/                    # 페이지 컴포넌트
│   ├── HomePage/             # 홈 페이지
│   │   ├── ui/               # 홈 페이지 전용 하위 컴포넌트들
│   │   ├── hooks/            # 홈 페이지 전용 커스텀 훅
│   │   └── ...               # 기타 필요한 리소스
│   ├── AboutPage/            # About 페이지
│   └── ...                   
│
├── widgets/                  # 위젯 컴포넌트 (페이지에서 사용하는 재사용 가능한 구성 요소)
│   ├── layout/               # 레이아웃 관련 위젯들
│   │   ├── Header/           # 헤더 위젯
│   │   │   ├── index.ts      # 헤더 Export
│   │   │   ├── styles.css    # 헤더 스타일
│   │   │   ├── Header.tsx    # 헤더 컴포넌트
│   │   │   └── ...           
│   │   ├── Footer/           # 푸터 위젯
│   │   └── ...
│   └── ...                   # 다른 위젯들
│
├── features/                 # 기능별로 구성된 폴더 (독립적인 비즈니스 로직과 UI 포함)
│   ├── UserAuth/             # 사용자 인증 기능
│   │   ├── api/              # 인증 관련 API 통신 모듈
│   │   ├── ui/               # 인증 관련 UI 컴포넌트
│   │   ├── model/            # 인증 관련 상태 관리
│   │   ├── types/            # 인증 관련 타입 정의 (TypeScript)
│   │   ├── utils/            # 인증 관련 유틸리티 함수
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...               # 기타 필요한 리소스
│   ├── Cart/                 # 장바구니 기능
│   │   ├── api/              # 장바구니 관련 API 통신 모듈
│   │   ├── ui/               # 장바구니 관련 UI 컴포넌트
│   │   ├── model/            # 장바구니 관련 상태 관리
│   │   ├── types/            # 장바구니 관련 타입 정의 (TypeScript)
│   │   ├── utils/            # 장바구니 관련 유틸리티 함수
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...               # 기타 필요한 리소스
│   └── ...
│
├── entities/                 # 엔터티 폴더 (도메인 모델, 상태 관리, API 통신 등)
│   ├── User/                 # 사용자 엔터티
│   │   ├── model/            # 사용자 상태 관리
│   │   ├── api/              # 사용자 관련 API 모듈
│   │   ├── types/            # 사용자 관련 타입 정의 (TypeScript)
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...
│   ├── Product/              # 제품 엔터티
│   │   ├── model/            # 제품 상태 관리
│   │   ├── api/              # 제품 관련 API 모듈
│   │   ├── types/            # 제품 관련 타입 정의 (TypeScript)
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...
│   └── ...
│
├── shared/                   # 공통 유틸리티, 컴포넌트, 훅 등
│   ├── utils/                # 유틸리티 함수
│   ├── hooks/                # 공통 커스텀 훅
│   ├── ui/                   # 공통 컴포넌트 (Button, Input 등)
│   ├── styles/               # 공통 스타일 파일
│   ├── types/                # 공통 타입 정의 (TypeScript)
│   ├── index.ts              # 모듈별로 export 정리
│   └── ...
└── ...

아래는 Next.js로 마이그레이션을 했을때의 폴더구조인데, FSD의 공식문서와는 조금 다르게 작성했다.

src/
│
├── app/                      # 애플리케이션 초기화 및 글로벌 설정
│   ├── _providers/           # 애플리케이션 레벨에서 제공되는 프로바이더
│   │   ├── ThemeProvider.tsx # 테마 관련 프로바이더
│   │   ├── AuthProvider.tsx  # 인증 관련 프로바이더
│   │   ├── index.ts          # Provider Export
│   │   └── ...               # 기타 전역 프로바이더들
│   └── Layout.tsx            # 공통 레이아웃 컴포넌트
│   (pages)/                  # 페이지 컴포넌트 (Next.js 라우팅에 사용됨)
│   ├── HomePage/             # 홈 페이지
│   │   ├── ui/               # 홈 페이지 전용 하위 컴포넌트들
│   │   ├── hooks/            # 홈 페이지 전용 커스텀 훅
│   │   ├── page.tsx          # Next.js의 페이지 컴포넌트
│   │   └── ...               # 기타 필요한 리소스
│   ├── AboutPage/            # About 페이지
│   │   ├── page.tsx          # About 페이지 컴포넌트
│   │   └── ...               
│   └── ...
│
├── widgets/                  # 위젯 컴포넌트 (페이지에서 사용하는 재사용 가능한 구성 요소)
│   ├── layout/               # 레이아웃 관련 위젯들
│   │   ├── Header/           # 헤더 위젯
│   │   │   ├── index.ts      # 헤더 Export
│   │   │   ├── styles.css    # 헤더 스타일
│   │   │   ├── Header.tsx    # 헤더 컴포넌트
│   │   │   └── ...           
│   │   ├── Footer/           # 푸터 위젯
│   │   └── ...
│   └── ...                   # 다른 위젯들
│
├── features/                 # 기능별로 구성된 폴더 (독립적인 비즈니스 로직과 UI 포함)
│   ├── UserAuth/             # 사용자 인증 기능
│   │   ├── api/              # 인증 관련 API 통신 모듈
│   │   ├── ui/               # 인증 관련 UI 컴포넌트
│   │   ├── model/            # 인증 관련 상태 관리
│   │   ├── types/            # 인증 관련 타입 정의 (TypeScript)
│   │   ├── utils/            # 인증 관련 유틸리티 함수
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...               # 기타 필요한 리소스
│   ├── Cart/                 # 장바구니 기능
│   │   ├── api/              # 장바구니 관련 API 통신 모듈
│   │   ├── ui/               # 장바구니 관련 UI 컴포넌트
│   │   ├── model/            # 장바구니 관련 상태 관리
│   │   ├── types/            # 장바구니 관련 타입 정의 (TypeScript)
│   │   ├── utils/            # 장바구니 관련 유틸리티 함수
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...               # 기타 필요한 리소스
│   └── ...
│
├── entities/                 # 엔터티 폴더 (도메인 모델, 상태 관리, API 통신 등)
│   ├── User/                 # 사용자 엔터티
│   │   ├── model/            # 사용자 상태 관리
│   │   ├── api/              # 사용자 관련 API 모듈
│   │   ├── types/            # 사용자 관련 타입 정의 (TypeScript)
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...
│   ├── Product/              # 제품 엔터티
│   │   ├── model/            # 제품 상태 관리
│   │   ├── api/              # 제품 관련 API 모듈
│   │   ├── types/            # 제품 관련 타입 정의 (TypeScript)
│   │   ├── index.ts          # 모듈별로 export 정리
│   │   └── ...
│   └── ...
│
├── shared/                   # 공통 유틸리티, 컴포넌트, 훅 등
│   ├── utils/                # 유틸리티 함수
│   ├── hooks/                # 공통 커스텀 훅
│   ├── ui/                   # 공통 컴포넌트 (Button, Input 등)
│   ├── styles/               # 공통 스타일 파일
│   ├── types/                # 공통 타입 정의 (TypeScript)
│   ├── index.ts              # 모듈별로 export 정리
│   └── ...
└── ...

4. FSD 도입

FSD 아키텍쳐를 도입하면서 겪었던 첫번째 난관은 Features와 Entities는 어떤 기준으로 로직을 나눠야 하는가 였다. 나의 경우 서버로 데이터를 전송해야 하는 로직 중에서 Create, Update, Delete는 Features에 저장하고 도메인 모델을 정의해야하는 Read의 경우 Entities에 작성을 했다. 하지만 작성하다 보니 한가지 의문이 들었다. 만약 아이템마다 Favoirte 아이콘이 붙어있다고 할 때, 이 아이콘은 GET으로 데이터를 가져오고 POST로 보내서 업데이트를 해야한다. 그렇다면 Favorite은 Features에 들어가야 하는지 Entities에 들어가야하는지, 아니면 아이콘만 Shared로 나눠야하나? 라는 의문이다. 고민끝에 폴더 구조에는 일관적인 규칙이 있어야 한다고 생각해서 특정 기능에 대한 비즈니스 로직을 포함하면 Features에 작성하기로 했다. 왜냐하면 FSD 아키텍쳐의 장점인 각 기능을 독립적으로 개발하고 테스트, 배포될 수 있어야한다는 원칙에 맞게 작성하려면 이 방법이 제일 좋다고 생각했기 때문이다.

 

두번째로 원칙상 Widgets이지만, 특정 페이지에 1회만 사용되는 위젯의 관리법이다. Widgets은 어디까지나 UI와 관련된 레이어이기 때문에 한번만 사용할 컴포넌트를 전부 넣어버리면 역으로 컴포넌트 찾기가 복잡해질 가능성이 있다는 생각이 들었다. 그래서 특정 페이지를 감싸는 UI라면 그 pages의 폴더 안에 폴더를 만들어서 관리하기로 하였다. 물론 광고블럭이나 푸터 등 그 자체로 위젯이 되는 부분들은 위젯에서 관리한다.

 

세번째로 Widgets내부에 위젯 컴포넌트를 불러오는 경우이다. 이 경우는 금방 해결 할 수 있었는데 아래의 사이트를 참고했다. FSD의 원칙상으로는 이를 지양하고 있는 것 같지만, 같은 레이어계층일 경우는 융통성있게 사용해도 되지 않을까 한다.

https://nukeapp.netlify.app/

 

Nuke App 🦄⚛️

 

nukeapp.netlify.app

 

5. ESLint 설정

작업을 하던 도중 ESLint를 사용해 하위 레이어에서 상위 레이어를 불러오는걸 막아버리는 방법이 있다는 글을 봤다. 좋은 방법인것 같아서 설정을 해봤는데, 나도 하위 레이어에서 상위 레이어를 불러오고 있는 컴포넌트가 있었다. 어지간해서 일어나지는 않지만, 그래도 협업을 한다면 이를 설정해주는편이 좋다고 생각한다.

npm install eslint-plugin-import -D
module.exports = {
  {
  "extends": [
    'plugin:import/typescript',
    'plugin:import/recommended'
  ],
  "rules": {
    "import/no-restricted-paths": [
      "error",
      {
        "zones": [
          { "target": "./src/features", "from": "./src/app" },
          { "target": "./src/features", "from": "./src/widgets" },
          { "target": "./src/entities", "from": "./src/app" },
          { "target": "./src/entities", "from": "./src/widgets" },
          { "target": "./src/entities", "from": "./src/features" },
          { "target": "./src/shared", "from": "./src/app" },
          { "target": "./src/shared", "from": "./src/widgets" },
          { "target": "./src/shared", "from": "./src/features" },
          { "target": "./src/shared", "from": "./src/entities" }
        ]
      }
    ]
  }
}
}

6. 장단점

최근 몇 달간 Feature-Sliced Design(FSD) 아키텍처를 적용해본 결과, 여러 가지 이점을 체감할 수 있었다. 가장 큰 장점은 레이어별로 기능을 명확하게 분리할 수 있다는 점이다. 이로 인해 각 모듈이 최대한 의존성을 배제하도록 설계되어, 유지보수가 훨씬 수월해졌다. 또한, 아키텍처에 적응된 후에는 개인적으로 작업 능률이 크게 향상된 것을 느꼈다.

하지만 초기 설계 단계에서 시행착오를 많이 겪었고, 기능을 개발하다 보면 의존성이 불가피하게 생길 때가 있는데, 이 경우 아키텍처 구조에 맞추려다 보니 코드가 다소 비효율적으로 작성되는 경우도 있었다. 또한, 폴더 관리 측면에서 FSD는 장기적으로 유리할 수 있지만, 단순한 프로젝트나 촉박한 일정에서는 과잉 설계로 느껴질 수 있다는 생각이 들었다.

7. 마지막

FSD 구조를 공부하고 실제 프로젝트에 도입해보면서, 예상보다 러닝 커브가 높다는 점을 깨달았다. 처음에는 FSD의 개념을 이해하기 위해 별도의 연습 프로젝트를 만들어 실습해봤고, 이후 회사 프로젝트의 구조를 FSD로 전환하면서 "아직 주니어인 내가 굳이 이 구조를 도입해 프로젝트를 복잡하게 만들고 있는 건 아닐까?"라는 고민도 했었다. 생소한 아키텍처를 도입하는 일은 생각보다 많은 시간과 노력을 요구했고, 특히 팀 전체가 FSD 구조를 이해하고 적용하는 데 시간이 꽤 걸렸다. 이 덕분에 새 아키텍쳐를 도입한다는건 꽤 어려운일이고 불편함이 따를 수 있다는 점을 느꼈다.

 

참고

https://emewjin.github.io/feature-sliced-design/

 

(번역) 기능 분할 설계 - 최고의 프런트엔드 아키텍처

기능 분할 설계(Feature-Sliced Design, FSD) 아키텍처의 개념과 이 아키텍처 방법론이 해결하는 문제를 이야기하고, FSD를 기존 아키텍처 및 모듈식 아키텍처와 비교한 뒤 장단점에 대해 소개합니다.

emewjin.github.io

위 링크는 emewjin.log라는 블로그에서 공식문서를 번역해주신 글이다. 자세한 설명과 예시를 모두 볼 수 있다.

Comments