제가 과거에 사용했던 FE 프로젝트 폴더 구조는 많은 프로젝트에서 흔히 볼 수 있는 형태입니다.
├── src
│ ├── assets
│ ├── components
│ ├── hooks
│ ├── pages
│ ├── styles
│ └── utils
그러나 역할별로 분류하는 해당 폴더 구조는 몇 가지 단점이 존재했습니다.
먼저 역할별로 폴더를 분류하다보니 프로젝트의 규모가 커질수록 components 폴더에 너무 많은 컴포넌트들이 보관되었습니다. components 폴더에 너무 많은 파일들이 존재하다보니 원하는 컴포넌트를 찾아가려면 컴포넌트를 거치고 또 다른 컴포넌트를 거쳐서 찾아야하는 불편함이 많았습니다.
또한 역할별로 분리하다보니 관련된 파일들이 다른 폴더에 흩어져있어서 여러 폴더를 이동하거나 한눈에 파악하기 어려운 경우들이 생겼습니다.
이처럼 원하는 코드와 파일의 위치를 찾는 것이 어려워지고 유지보수하기 어려워지는 문제점이 있었습니다.
이러한 문제들을 해결하기 위해 이번 프로젝트에서는 FSD(Feature-Sliced Design)의 계층형 아키텍처를 도입했습니다.
FSD 아키텍처는 기능별로 구조화하기 때문에 관련된 코드들이 한 곳에 모여있어 원하는 코드의 위치를 쉽게 찾을 수 있습니다. 또한 각 계층별로 책임이 명확하게 분리되어 있어서 새로운 기능을 추가할 때도 더 용이할 것이라고 생각해서 도입하게 되었습니다.
기능을 중심으로 분류하고 각 기능을 계층별로 관리

레이어 > 슬라이스 > 세그먼트로 폴더를 구분해나갈 수 있습니다.
1️⃣ 레이어
최상위 디렉토리로 책임에 따라 분류되어 현재는 6가지(processes는 더 이상 사용되지 않음)로 분류됨
apps: 애플리케이션의 초기화와 전역 설정을 담당pages: 애플리케이션의 각 페이지를 정의widgets: 페이지에 사용되는 독립적인 UI 컴포넌트features: 비즈니스 기능이나 사용자 시나리오를 처리entities: 애플리케이션에서 사용하는 비즈니스 엔티티를 정의shared: 비즈니스 로직에 종속되지 않고 여러 레이어에서 재사용 가능한 코드
💡 레이어들은 각자 책임을 가지고 계층 구조로 분류됨
→ 계층이 높은 레이어만 낮은 레이어들을 활용할 수 있는 선형적 흐름을 가짐
| 레이어 | 사용할 수 있는 계층 |
|---|---|
| app | shared, entities, features, widgets, pages |
| pages | shared, entities, features, widgets |
| widgets | shared, entities, features |
| features | shared, entities |
| entities | shared |
| shared | - |
계층이 높은 레이어일수록 많은 비즈니스 로직이 포함됨
계층이 낮은 레이어일수록 추상화 수준이 높고 재사용성이 높아지게 됨
2️⃣ 슬라이스
도메인 별로 분류, 레이어를 구성
3️⃣ 세그먼트
기술적 관심사에 따라 분류, 슬라이스를 구성
- 일반적으로 다음과 같이 분류될 수 있다고 합니다
api- 필요한 서버와 통신하는 로직ui- 슬라이스의 UI 컴포넌트model- 슬라이스의 비즈니스 로직, 즉 상태와의 상호 작용lib- 슬라이스 내에서 사용되는 공통 유틸리티 함수config- 슬라이스에서 필요한 특정한 설정값consts- 슬라이스에서 사용할 상수값
+) barrel 파일을 사용하자!
슬라이스 또는 세그먼트에서 필요한 기능만 외부로 export하는 진입점 역할을 할 수 있음
→ 외부에서 필요하지 않은 것은 격리시킬 수 있음
Import 문을 간소화할 수 있음
내부 구조 변경 시에 외부 코드의 import 경로를 일일이 변경할 필요가 없음
이렇듯 FSD를 통해 같은 기능끼리 모아두면서 기능 간의 결합은 느슨하게 응집력은 높게 만들 수 있습니다!
저희 프로젝트의 FE 폴더 구조를 다음처럼 설계하게 되었습니다.
├── src
│ ├── app // app을 구성하는 설정, Provider, style 계층
│ ├── page // 라우팅을 구성하는 계층
│ ├── widget // 페이지에서 사용되거나 feature를 조합하는 계층
│ ├── feature // 비즈니스 로직 계층
│ └── shared // 비즈니스에 소속되지 않는 공통 로직 계층
→ 프로젝트의 규모가 작기 때문에 entity와 feature 계층을 병합해서 하나의 feature 계층으로 사용하고자 했습니다.
이후에 개발이 진행되었을 때 FE 폴더 구조는 다음처럼 구성되었습니다.
├── src
├── app
│ ├── mock
│ │ └── MockRepository
│ ├── query
│ ├── router
│ ├── style
│ └── test
├── feature
│ ├── CodeView
│ ├── Comment
│ ├── History
│ │ └── components
│ ├── Lotus
│ ├── LotusSearch
│ ├── Pagination
│ └── User
│ └── utils
├── page
│ ├── (main)
│ │ ├── lotus
│ │ │ ├── $lotusId
│ │ │ └── create
│ │ └── user
│ └── login
│ ├── error
│ └── success
├── shared
│ ├── components
│ │ ├── AsyncBoundary
│ │ ├── ErrorBoundary
│ │ └── Overlay
│ ├── hooks
│ │ ├── useDebounce
│ │ ├── useLocalStorage
│ │ ├── useOverlay
│ │ ├── useTimer
│ │ └── useToast
│ └── utils
└── widget
├── History
├── LotusCodeInput
├── LotusCreate
├── LotusList
├── Navigation
└── User
프론트엔드 개발을 진행하면서 다음과 같은 문제점들이 존재했습니다.
- 폴더명 규칙이 대문자와 소문자, 복수형과 단수형이 섞여 있음
- 분리 기준이 애매해서 어디에 파일을 위치해야하는지 어려움을 느낌
따라서 폴더 구조를 더 명확한 기준으로 분리해서 통일성을 확보하기 위해 한 번 더 폴더 구조에 대해 논의하게 되었습니다.
먼저, 폴더명이 대문자, 소문자로 시작하거나 복수형, 단수형이 섞여있어서 기준을 정했습니다.
→ 폴더명은 모두 소문자로 작성하고 단수형으로 통일했습니다.
계획한 대로 폴더 구조를 분류하고 개발하면서 계속 “이 코드는 feature인가? widget인가?” 라는 의문이 계속 들었습니다.
→ 특정 도메인이나 비즈니스 로직이 포함되어 있는 경우
feature폴더에, 해당 feature들을 사용해서 화면을 구성하는 컴포넌트인 경우widget폴더에 배치했습니다.
모든 코드를 엄격히 분리하는 것에 어려움을 느꼈습니다. 따라서 슬라이스와 세그먼트의 경우 새로운 기준을 세웠습니다.
- 관련 파일이 2개 이상일 경우에만 새로운 폴더로 분리
- 나누기 어려운 부분들은 일단 같은 폴더에 두다가 분리가 가능하면 새로운 폴더로 묶기
widget 폴더의 경우
어디에 두어야할지 몰라서 분리하지 못하고 widget 폴더 하위에 두었던 파일들을 lotusUpdate, lotusDelete 등의 도메인으로 묶어서 관리했습니다.
Header.tsx 처럼 여러 도메인에서 사용되는 파일은 그대로 widget 폴더 하위에 두었습니다.
feature 폴더의 경우
codeView라는 슬라이스 안에 파일들이 세그먼트로 분류되지 않고 관리되었습니다. 기술적 관심사에 따라 세그먼트를 분리할 때 2개 이상의 파일로 관리된다면 component, model처럼 폴더로 묶고 그렇지 않다면 constants.ts, hook.ts처럼 한 파일로 관리했습니다.

shared 폴더의 경우
한 기능이 한 파일로만 관리되는 파일들을 common 폴더에 모아두었습니다. 이후에 한 기능이 여러 파일로 관리된다면 common 폴더에서 분리했습니다.

→ 처음부터 엄격하게 나누려고만 생각하다보니까 너무 어렵게 느껴졌었는데 관련된 파일이 2개 이상이 되었을 때 분리하는 것으로 변경함으로써 분리 기준이 더 명확해졌습니다.
shared 폴더가 기존에는 components, hooks, utils로 역할별로 분류되어 있는데 이를 도메인별로 분류한 후에 그 안에서 역할별로 분류했습니다.
Overlay와 useOverlay가 원래는 components와 hooks 폴더로 분리되어 있는데 이를 overlay라는 폴더로 합쳐서 관리하도록 변경
→ 같은 기능을 구현하는 파일들을 한곳에 모아서 관리하기 편리하도록 변경했습니다.
세그먼트의 경우는 다음처럼 분류했습니다
- api - 필요한 서버와 통신하는 로직
- query - tanstack query를 이용한 hook 정의
- component - 슬라이스의 UI 컴포넌트
- constant - 슬라이스에서 사용할 상수값
- hook - 슬라이스에서 사용할 훅
- model - 도메인 비즈니스 로직을 관리할 model class
- type - interface, type을 정의
- util - 공통 유틸리티 함수
app 폴더 : 애플리케이션의 전반적인 설정 및 엔트리 포인트를 관리합니다.page 폴더 : 라우팅과 각 페이지별 뷰를 관리합니다.widget 폴더 : 독립적이고 재사용 가능한 UI 컴포넌트(위젯)를 관리합니다.feature 폴더 : 특정 기능(도메인)을 중심으로 비즈니스 로직이 포함된 컴포넌트 관리합니다.shared 폴더 : 전역적으로 공유되는 리소스와 유틸리티를 관리합니다.├── src
│ ├── app
│ │ ├── main.tsx
│ │ ├── mock
│ │ ├── query
│ │ ├── router
│ │ ├── style
│ │ ├── test
│ │ └── vite-env.d.ts
│ ├── feature
│ │ ├── codeView
│ │ ├── comment
│ │ ├── history
│ │ ├── lotus
│ │ │ ├── api.tsx
│ │ │ ├── component.tsx
│ │ │ ├── index.ts
│ │ │ ├── model.ts
│ │ │ ├── query.ts
│ │ │ ├── type.ts
│ │ │ └── util.ts
│ │ └── user
│ ├── page
│ │ ├── (main)
│ │ ├── __root.tsx
│ │ ├── index.lazy.tsx
│ │ └── login
│ ├── shared
│ │ ├── boundary
│ │ ├── common
│ │ ├── index.ts
│ │ ├── overlay
│ │ ├── pagination
│ │ └── toast
│ └── widget
│ ├── Header.tsx
│ ├── history
│ ├── lotusCodeRun
│ ├── lotusCreate
│ ├── lotusDelete
│ ├── lotusDetail
│ ├── lotusList
│ ├── lotusUpdate
│ ├── navigation
│ └── user
FSD 아키텍처의 개념이 익숙하지 않다보니 처음에 어려움을 많이 느꼈었습니다.
개발을 시작하기 전, FSD 아키텍처를 참고해서 폴더 구조를 계획할 때 슬라이스와 세그먼트에 논의를 충분히 나누지 못해서 더 애매한 부분들이 많았던 것 같습니다.
따라서 전체적인 폴더 구조를 함께 논의하며 저희 프로젝트의 각 레이어들의 역할을 정의하고 레이어 > 슬라이스 > 세그먼트를 어떻게 분리할 지에 대한 기준을 명확하게 세우는 시간을 가졌습니다.
엄격하게 분리하기보다는 작은 규모의 프로젝트에 맞게 저희만의 분리 기준을 정하려고 했습니다.
이를 통해 기능을 중심으로 폴더를 관리함으로써 특정 파일을 찾아가거나 디버깅하는 과정이 훨씬 수월해졌습니다.
페이지 자체에서 문제가 발생했다면
page레이어로
컴포넌트에서 문제가 발생했다면widget레이어로
API 호출과 관련된 문제라면feature레이어의api또는query로
비즈니스 로직과 관련한 문제라면feature레이어의model로
이동해서 문제의 위치를 빠르게 파악할 수 있었습니다.
또한 폴더 구조를 명확히 정한 후에는 이전에 느꼈던 파일을 어디에 두어야하는지에 대한 모호함도 줄어들었고 협업 과정에서 일관성을 유지할 수 있었습니다.
이후에 프로젝트가 커지더라도 기존 구조를 확장하는 방식으로 쉽게 적용할 수 있을 것 같다고 느껴졌습니다.
https://emewjin.github.io/feature-sliced-design/
https://velog.io/@teo/separation-of-concerns-of-frontend
https://feature-sliced.design/docs