Architecture

기능 분할 설계(FSD) 아키텍처로 FE 구조 최적화하기

황인선Frontend Developer
2024년 09월 04일 발행
architecturefront-end

최근 프론트엔드 프로젝트 아키텍처 구성에 관심이 많아졌습니다. 새로운 프로젝트를 기획할 때마다 느끼는 설렘과 함께, 개발 단계에서 폴더 구조에 대한 고민이 항상 따라옵니다.

자율성이 높은 프론트엔드 프로젝트의 폴더 구조는 개발자마다 취향이 다르고, 팀원 간에도 의견이 다양하기 때문에 혼란스러울 때가 많습니다.

이전에는 좋아하는 개발자들의 폴더 구조를 막연하게 따라했지만, 회사에서 이를 적용했을 때 규모가 커질수록 관리가 힘들어지는 것을 체감했습니다.

이러한 문제를 해결하기 위해 좀 더 나은 구조를 고민하던 중, 제로초님의 영상을 통해 FSD(Feature-Sliced Design)라는 아키텍처를 알게 되었습니다. FSD는 기능 단위로 프로젝트를 구성하여 유지보수성과 확장성을 높이는 방법으로, 저에게 새로운 가능성을 열어줄 것이라 기대하고 있습니다.

plain
// 이전 프로젝트 폴더 구조
└─src
    ├─app
    ├─components
    ├─lib
    ├─styles
    ├─states
    └─types

위는 이전 프로젝트에서 사용했던 폴더 구조입니다.

이러한 구조는 초기 단계에서는 빠르고 쉽게 관리할 수 있었습니다. 그러나 프로젝트 규모가 커짐에 따라 관리의 어려움이 발생했습니다. 각 폴더가 명확한 역할을 가지고 있음에도 불구하고, 기능 단위로 분리되어 있지 않아 점점 복잡성이 증가했습니다.

plain
// 현재 프로젝트 폴더 구조 (iamseon.com 블로그 프로젝트)
└─src
    ├─app
    │  ├─providers
    │  └─styles
    ├─pages
    │  ├─home
    │  └─post
    ├─features
    │  ├─theme
    │  └─write
    ├─widgets
    │  ├─header
    │  └─series
    └─shared
       ├─lib
       ├─states
       └─ui

위는 현재 블로그 프로젝트에서 사용한 폴더 구조입니다.

현재 구조는 과거 src 폴더에서 하나의 뎁스로 관리한 방식과 달리 기능 분할 설계(FSD) 의 관점에 맞춰 기능과 관심사에 따라 폴더를 분리하였습니다.

이러한 구조는 각 기능을 독립적으로 관리할 수 있게 해주며, 팀원 간의 협업을 용이하게 하여 프로젝트의 확장성과 유연성을 제공합니다.

기능 분할 설계(FSD)란?

FSD(Feature-Sliced Design)는 프론트엔드 프로젝트의 구조를 비즈니스 요구사항에 맞춰 이해하기 쉽고 체계적으로 설계하는 방법론입니다.

FSD는 아래와 같이

plain
Layers
plain
Slices
plain
Segments

의 총 3단계로 구성되어 있습니다.

9f67e86b-3293-444a-9fc2-00b149752bdc.PNG

Layers

Layers는 프로젝트 최상위 디렉토리로, 총 7개의 레이어로 구성되지만, 현재는 process 레이어가 사용되지 않아서 6개의 레이어들을 설명드리겠습니다.

이 레이어들은 일반적으로 react.js 또는 next.js의 기본 디렉토리인 src 폴더의 첫번째 depth에서 확인할 수 있으며, 각 레이어는 관심사에 따라 분리되어 있습니다.

구성되는 layer들은 아래와 같습니다.

  • app: 전체 애플리케이션 로직이 초기화 되는 곳으로, provider, routerglobal style 등이 정의됩니다. 이 레이어는 애플리케이션의 entry point 역할을 하며,
    저는 ReactQueryProvider 또는 ThemeProvider와 같은 구성 요소를 이곳에 배치하였습니다.
  • pages: 애플리케이션의 페이지를 포함하는 레이어입니다. react.jsroutes.tsx에서 라우트 구조를 구성하고, next.js는 app 폴더(next.js 13 이상) 내에서 라우트를 설정합니다. 이 레이어는 이러한 라우트 구조에서 사용할 page component들을 담고 있습니다.
  • widgets: 기존에 사용하던 폴더 구조로 예를 들면 layouts 또는 templates component들을 의미합니다. ui는 유사하지만 내용만 달라지는 component들을 FSD에서는 widget이라고 부릅니다. 저는 header, footer, main template 등과 같은 component들을 이 레이어에 배치하였습니다.
  • features (optional): 비즈니스 로직, 사용자 시나리오, 기능을 다루는 레이어입니다. 로그인 페이지에서는 로그인 기능과 입력 검증 로직이 이곳에 포함됩니다. 저는 SignInButton, ThemeToggleButton 등과 같은 component와 관련 로직을 이 레이어에 배치하였습니다.
  • entities (optional): 비즈니스 entitiy를 담는 레이어로, 데이터 영역이라고 생각하시면 됩니다. 이곳에는 데이터베이스나 비즈니스 로직에 필요한 interface 또는 type 들을 정의합니다.
  • shared: 특정 비즈니스 로직이나 레이어에 종속되지 않고 재사용 가능한 component와 utility들을 포함합니다. UI 구성 요소와 각종 유틸 함수, 상수 등을 이곳에 정의하였으며,
    저는 이곳에 assets, consts, entities, lib, states, ui 와 같은 구조로 레이어를 구성하였습니다. 위에서 설명한 6개의 레이어들은 계층적 구조라는 특징을 가지고 있습니다. app 레이어는 하위 5개의 layer를 참조하여 사용할 수 있지만, 예를 들어 features 레이어는 상위 레이어인 app 레이어를 참조할 수 없습니다. 즉, 상위 레이어는 오직 하위 레이어만 활용할 수 있으며, 이러한 구조는 한 방향으로만 향하는 선형적인 흐름을 유지하기 위해 설계되었습니다.
Layer사용 가능한 레이어이 레이어를 사용할 수 있는 레이어
appshared, entities, features, widgets, pages-
pagesshared, entities, features, widgetsapp
widgetsshared, entities, featuresapp, pages
featuresshared, entitiesapp, pages, widgets
entitiessharedapp, pages, widgets, features
shared-app, pages, widgets, features, entities
## Slices 6개의 레이어에는 두 번째 레벨인 slice라는 하위 디렉토리가 존재합니다. slice와 segment는 레이어의 app, pages, ... 폴더 처럼 특정한 네이밍 규칙을 따르지 않으며, 사용자가 현재 프로젝트의 비즈니스 도메인에 따라 자유롭게 구조를 나누고 네이밍할 수 있습니다.

저는 블로그 프로젝트를 진행 중이므로, 포스트, 태그, 시리즈와 같은 slice가 필요로 하여 그에 맞는 이름의 slice 폴더를 생성하였습니다.

slice는 각 레이어에서 독립적으로 사용되어야 하며, 서로 다른 slice 간에는 격리 규칙을 준수해야 합니다. 즉, 한 slice에서 구현한 코드는 다른 slice와 직접적인 공유를 하지 않는 것이 이상적입니다.

Segments

segment는 각 slice를 구성하는 요소로, 기술적 목적에 따라 slice 내의 코드를 세분화하는 작은 모듈이라고 할 수 있습니다. segment는 진행 중인 프로젝트의 성격과 팀의 요구 사항에 따라서 구성 및 이름이 결정됩니다. 아래는 가장 일반적인 segment들을 소개한 것입니다.

  • api: 필요한 서버 요청
  • ui: slice를 구성하는 ui component
  • model: 비즈니스 로직, 즉 상태와 상호작용, action 및 selectors가 해당됨
  • lib: slice 내에서 사용되는 보조 기능, util 함수 및 custom hook 등
  • consts: 필요한 상수 값

Public API

각 slice와 segment는 공개 API를 가집니다. 공개 API는 index.js 파일 또는 index.ts 파일이고, 이 파일을 통해서 필요한 기능만 외부로 export하고 불필요한 기능들은 격리할 수 있습니다.

javascript
// slice index.ts 파일의 예
export * from './ui'

// segment index.ts 파일의 예
export * from './AppHeader';

다음은 공식 문서에서 설명하는 공개 api에 대한 규칙입니다.

  • app slice와 segment는 공개 api의 index 파일에 정의된 slice 기능과 컴포넌트만 사용합니다.
  • 공개 api에 정의되지 않은 slice 또는 segment의 내부 부분은 격리된 것으로 간주되며 slice 또는 segment 내부에서만 접근할 수 있습니다. 공개 api를 쉽게 설명하자면, 외부와 공유해야 하는 부분은 index.ts 파일에서 export를 하고, 공유할 필요가 없는 부분은 export하지 않는 방식입니다. 이러한 규칙을 따르면 나중에 import 및 export 하는 부분에서 실수가 발생하더라도, 해당 index.ts 파일만 수정하면 되므로 유지보수가 훨씬 용이해졌습니다.

FSD에 대해서 깊게 알아보기

FSD(Feature-Sliced Design)가 어떻게 구성되는지, 이전 아키텍처들과 어떤 점에서 차별화되는지, 그리고 FSD의 장단점에 대해 FSD 공식 문서를 참고하여 살펴보겠습니다.

추상화 및 비즈니스 로직

계층이 높은 레이어일 수록 특정 비즈니스 노드에 대한 종속성이 커지고, 그에 따라 더 많은 비즈니스 로직이 포함됩니다. 반대로, 낮은 계층의 레이어는 추상화 수준이 높고, 재사용성이 높으며, 레이어 자체의 자율성이 제한적입니다.

예를 들어, shared 레이어에서는 코드가 다양한 모듈이나 컴포넌트에서 재사용될 수 있어야 하므로, 추상화 수준을 높이는 것이 중요합니다. 이를 통해 shared 레이어의 구성 요소는 특정 비즈니스 요구 사항에 종속되지 않고, 다양한 상황에서 유연하게 활용될 수 있습니다.

FSD의 문제 해결 방식

FSD(Feature-Sliced Design)의 도전 과제는 결합을 느슨하게 하고 응집력을 높이는 것입니다.

객체지향프로그래밍(OOP)에서는 다형성, 캡슐화, 상속 및 추상화와 같은 개념을 통해 위 도전 과제를 해결해왔습니다. 이러한 접근 방법은 코드의 격리성, 재사용성 및 다양성을 보장하고 이를 통해서 컴포넌트나 기능을 사용하는 방법에 따라 다른 결과를 얻을 수 있습니다.

FSD는 이러한 원칙들을 프론트엔드에 적용하는데 도움을 줍니다.

추상화와 다형성레이어를 통해서 이루어집니다. 낮은 계층의 레이어는 높은 수준의 추상화를 제공하므로, 상위 레이어에서 재사용이 될 수 있습니다. 이때, 상위 레이어는 parameter나 props를 통해 서로 다른 기능을 구현할 수 있는 유연성을 가지게 됩니다.

캡슐화는 공개 API를 통해서 이루어집니다. 공개 API는 외부에 노출할 필요가 없는 부분은 export하지 않고 내부적으로 격리하는 방식으로 구현됩니다. 이러한 구조 덕분에, 공개 api는 slice 또는 segment의 기능 및 component에 접근할 수 있는 유일한 방법입니다.

상속 또한 레이어를 통해 달성 됩니다. 추상화와 다형성의 특성을 바탕으로 낮은 계층의 레이어는 높은 계층에서 재사용될 수 있도록 설계됩니다.

고전 아키텍처와의 비교

개발자가 필요에 의해 또는 팀의 성향에 따라 맞춰 구성하는 방식이 고전 아키텍처의 특징입니다. 그러나 유튜브, 인터넷 강의와 같은 커뮤니티가 활성화되면서, 다양한 프로젝트의 아키텍처를 서로 공유하게 되었고, 그 중에서 많이 사용되는 방식들이 표준처럼 여겨지기도 했습니다. 저 또한 이러한 방식으로 아키텍처를 구성해왔습니다.

하지만 이러한 접근 방식은 규모가 작거나 사이드 프로젝트에서는 큰 문제를 드러내지 않았습니다. 그러나 프로젝트가 커지고, 회사에서처럼 장기적인 유지보수가 필요한 경우에는 컴포넌트 간의 암묵적인 연결, 모듈의 복잡성 등 다양한 문제가 발생할 수 있습니다. 이로 인해 유지보수가 어려워지고, 확장성이 떨어지는 등의 부작용이 나타날 수 있습니다.

FSD 아키텍처의 장단점

공식 문서에서 언급한 장단점에 대해 살펴보겠습니다.

장점

  • 통일성: 코드는 영향력 범위(layers), 도메인(slices), 기술적 목적(segments)에 따라 구성됩니다. 따라서 초보자도 쉽게 이해할 수 있는 표준화된 아키텍쳐가 만들어집니다.
  • 로직의 제어된 재사용: 각 아키텍처 컴포넌트는 목적과 예측 가능한 종속성이 있습니다. 이를 통해 DRY 원칙을 따르는 것과 적응 가능성 사이의 균형을 유지할 수 있습니다.
  • 변경 및 리팩토링에 대한 안정성: 특정 레이어의 모듈은 같은 레이어 또는 그 위의 레이어에 있는 다른 모듈을 사용할 수 없습니다. 이를 통해 예기치 않은 결과 없이 격리된 수정이 가능해집니다.
  • 비즈니스 및 사용자 요구사항에 대한 오리엔테이션: 앱이 비즈니스 도메인으로 나뉘면 코드를 탐색하여 모든 프로젝트 기능을 발견하고 더 깊이 이해할 수 있습니다.

단점

  • 다른 아키텍처 솔루션들에 비해 진입 장벽이 높습니다.
  • 인식, 팀 문화 및 개념 준수가 필요합니다.
  • 도전 과제와 문제를 미루지 않고 즉시 해결해야 합니다. 코드 문제와 개념에서 벗어난 부분을 즉시 확인할 수 있습니다. 그러나 이는 장점으로도 볼 수 있습니다.

Next.js와 FSD의 충돌

Next.js의 인기가 높아짐에 따라, 많은 개발자들이 Next.js와 FSD 아키텍처를 함께 사용하는 추세입니다. Next.js는 FSD 아키텍처와 잘 어울리지만, 버전 12 이하에서 사용되는 pages 폴더와, next 13 이상에서 도입된 app 폴더 두 가지 영역에서 충돌이 발생합니다.

Pages

버전 12 이하 Next.js에서는 pages 디렉토리가 파일 라우팅을 담당하며, 각 컴포넌트가 하나의 라우트를 나타냅니다. FSD 아키텍처에서 pages 레이어는 페이지들의 컴포넌트들을 담고 있는 중요한 레이어로 사용됩니다. 이로 인해 Next.js의 pages 폴더와 FSD 아키텍처의 pages 폴더 간의 결합 방식에 대한 충돌이 발생할 수 있습니다.

충돌에 대한 해결 방법을 알아보겠습니다.

  • [root]/pages/ 와 같이 Next.js의 pages 폴더를 프로젝트 루트에 저장하는 방법이 있습니다. 그리고 FSD 아키텍처의 pages 폴더는 [root]/src/pages/와 같이 src 폴더 내에 저장합니다.
plain
├─pages // Next.js의 pages 폴더
└─src
    ├─app
    ├─pages // FSD 아키텍처의 pages 폴더
    ├─features
    ├─widgets
    └─shared
  • FSD 아키텍처의 pages 폴더의 이름을 pages-flat 으로 수정하고 Next.js의 pages 폴더를 유지하는 방법이 있습니다. 실제 페이지 코드는 pages-flat에 저장하고, 그 후 pages 폴더로 내보낼 수 있습니다.
plain
└─src
    ├─app
    ├─pages // Next.js의 pages 폴더
    ├─views // FSD 아키텍처의 pages 폴더
    ├─features
    ├─widgets
    └─shared

App Directory

버전 13 이상 Next.js 에서는 app 디렉토리에서 라우팅을 담당합니다. 위에 설명드린 버전 12 이하의 pages 폴더 방식과 크게 다르지 않게 충돌을 해결할 수 있습니다.

충돌에 대한 해결 방법을 알아보겠습니다.

  • [root]/app/ 와 같이 Next.js의 app 폴더를 프로젝트 루트에 저장하는 방법이 있습니다. 그리고 FSD 아키텍처의 app 폴더는 [root]/src/app/와 같이 src 폴더 내에 저장합니다. 공식 문서에서 추천하는 방법입니다.
plain
├─app // Next.js의 app 폴더
└─src
    ├─app // FSD 아키텍처의 app 폴더
    ├─pages
    ├─features
    ├─widgets
    └─shared

마무리하며

처음에는 프로젝트 규모가 크지 않아 이전 아키텍처에 비해 불편함을 느꼈고, 이렇게까지 구조를 세분화해야 하는지 의문이 들었습니다. 하지만 기능별로 폴더를 나누고 코드를 작성할 때도 기능별로 분리하게 되면서, FSD 아키텍처의 도전 과제인 결합도를 낮추고, 응집도를 높이는 문제를 성공적으로 해결했다고 생각합니다.

FSD 아키텍처가 모든 상황에 대한 정답은 아닙니다. 프로젝트와 팀의 성격에 따라서 다른 아키텍처나 자율적인 구성을 통해서도 좋은 아키텍처를 구축할 수 있다고 생각합니다.

하지만 저처럼 프론트엔드 아키텍처에 대해서 고민을 하고 있으며 좋은 아키텍처를 찾고 있는 분들에게는 FSD 아키텍처를 도입하는 것이 매우 유익한 시도가 될 수 있다고 생각합니다.

FSD 아키텍처의 단점에서 언급했듯이, 이 아키텍처를 효과적으로 활용하기 위해서는 나 혼자만 이해하는 것이 아니라 팀의 모든 구성원이 FSD 아키텍처의 원칙을 알고 이를 준수하려는 노력이 필요합니다. 당장 실무에 적용하기는 어려울 수 있지만, 먼 훗날 FSD 아키텍처 또는 그 당시 등장할 새로운 아키텍처를 팀원들과 공유하고 조언을 해줄 수 있는 성장한 개발자가 되기를 희망하며 마무리하겠습니다.

reference


기능 분할 설계(FSD)를 이용한 FE 아키텍처 구성FSD 공식 문서velog - 니들이 FSD를 알아??