들어가기
이번에 P.E.C 캠프 6기로 참여하면서 FSD의 개념을 Next.js와 결합해보는 과정을 진행했었다.
Product Engineer Camp - pec
8주동안 진행하는 Camp 를 통해서, 내 주변에 실제 문제를 해결하며 진짜 성장을 경험하세요. 다양한 UX framework 를 활용하여 설계하고, 이를 토대로 AI 와 효율적으로 협업하는 방법을 학습합니다.
slashpage.com
흔히 알고 있는 역할 중심의 설계를 벗어자세한 FSD의 개념은 하단 블로그에서 상세하게 소개시켜주고 있기에 여기서는 내가 정립했던 FSD와 Next.js의 개념에 대해서만 다뤄보고자 한다.
FSD 관점으로 바라보는 코드 경계 찾기
이번 글의 주제도 FSD(Feature-Slided Design)입니다! 현재 제가 가장 관심있는 관심사 2가지 중 하나가 바로 이 FSD 네요. 지금 하고 있는 일이 레거시 코드를 최신 기술로 고도화하는 작업입니다. 그러
velog.io
우리가 정의한 FSD의 개념
공식문서에도 자세한 설명이 나와있지만, P.E.C 캠프에서 경찬님과 6기 회원분들이랑 정의한 FSD의 개념은 아래와 같았다.
- app: FSD와 동일
- pages: FSD와 동일
- widgets: features들의 모임
- features: POST, PUT, DELETE와 같이 데이터의 상태가 변하는 요청들이 포함된 곳
- entities: GET와 같이 데이터를 보여주는 요청들이 포함된 곳
- shared: API 요청과는 관계없이 여러 도메인에서 사용하는 요소들이 포함된 곳
잘 안와닿을 수도 있겠지만, 집중하고 싶은 키워드는 데이터이다.
왜 데이터 인가? 어떤 데이터 인가?
나는 클라이언트 개발자이다.
그래서 이전까지만해도 클라이언트의 입장에서 레이어를 분리할 때 경계를 아래의 사진처럼 세로로 구현했었다.

위 구조의 장/단점은 아래와 같다.
- 장점
- 컴포넌트와 API의 스펙이 분리되어지기에 API 스펙 변동이 발생하면, 서버와 클라이언트 스펙 간의 변환지점만 수정해주면 된다. 즉, 컴포넌트 자체를 수정해야 하는 일은 거의 없다. 👉 클라이언트와 서버 사이의 의존성을 끊어낼 수 있다.
- 단점
- 단순히 API 응답값을 화면에 보여주는 과정에서도 서버와 클라이언트 스펙 2가지를 모두 구현해야 하기에 중복으로 선언되는 타입들과 변환 과정을 구현하는 파일들이 불필요하게 생겨나게 된다.
사실 이 구조는 데이터의 관점을 클라이언트와 서버 2가지로 바라보고 있었기 때문에 만들어졌다고 볼 수 있다.
내가 과거에 이 방향을 지향했던 이유이다.
클라이언트 개발자의 입장에서 서버와 클라이언트 스펙 자체를 완전히 분리시키면 의존성을 끊어낸다면, API 스펙이 완성되기전까지 마냥 기다리는 클라이언트 개발자가 아니라, API가 개발되어지고 나면 바로 클라이언트와 연결작업을 진행할 수 있는 상태로 업무를 진행할 수 있다는 장점이 컸었다.
다만, 앞서 말한 단점으로 인해 분리가 모호해지는 순간들과 똑같은 프로퍼티를 가지는 타입과 파일들이 점차 비대해지기 시작했다.
결론적으로 바라보았을 때, 서버 API 스펙이 변경되면 클라이언트 스펙들의 수정범위도 점점 퍼져나가게 되는 상황이 오게 된 것이다.
이것이 데이터를 클라이언트, 서버 2개로 나눠서 보았을 때의 상황이다.
그렇다면 데이터를 1개로 바라본다면 어떤 상황에 마주할 수 있을까?

클라이언트, 서버 간의 경계가 사라지고, 하나로 묶어진 상황에서 계층이 좌우가 아닌 위/아래로 나뉘어지게 되는 것이다.
위 구조의 장/단점은 아래와 같다.
- 장점
- 데이터를 2개로 바라보았을때, 중복으로 선언되는 타입들과 변환 과정을 구현하는 파일들이 불필요하게 생겨나지 않는다. 앞서 말한 단점이 사라지게 되는 것이다.
- 단점
- 클라이언트와 서버의 의존성이 있기 때문에 한 쪽에서 변경점이 발생하면 반드시 다른 한 곳에서도 수정이 발생하게 된다.
그러면 여기서 궁금증이 생길 것이다.
1번과 2번을 나누는 기준은 무엇으로 잡아야 하는 것 일까?
이 질문이 앞서 정의한 FSD의 개념의 시발점이 되어주었다.
다시 데이터 1개의 관점에서 FSD 개념을 살펴보자.
앞서 언급한 FSD의 개념에서 features, entities의 정의를 다시 살펴보자.
- features: POST, PUT, DELETE와 같이 데이터의 상태가 변하는 요청들이 포함된 곳 👉 변하는 데이터들이 모인 곳
- entities: GET와 같이 데이터를 보여주는 요청들이 포함된 곳 👉 변하지 않는 데이터들이 모인 곳
각각에 대한 정의를 어떻게 해석하고 있는가?
변하는 데이터 vs 변하지 않는 데이터
변한다는 기준이 클라이언트의 입장으로만 바라보고 있는 것이 아니라 클라이언트와 서버의 2가지 관점을 1개의 그룹으로 뭉쳐서 정의하고 있다는 것을 알 수 있다.
한 번, segements까지 예시를 들어보면
├── 📁 feautres
│ ├── 📁 도메인명
│ │ │ ├── 📁 api // ✅ POST,PUT,DELETE 요청
│ │ │ ├── 📁 model
│ │ │ └── 📁 ui
├── 📁 entities
│ ├── 📁 도메인명 // ✅ GET 요청
│ │ │ ├── 📁 api
│ │ │ ├── 📁 model
│ │ │ └── 📁 ui
features의 하위 세그먼트 안의 api폴더에는 변하는 것들의 요청들이 들어가야 한다.
즉, 사용자의 동작(클라이언트)으로 인해 DB의 값(서버)이 변하게 되는 요청인 POST, PUT, DELETE 등이 features의 api 파일에 위치할 수 있는 것이다.
그렇다면 entities의 개념은 자연스럽게 유추되지 않는가?
예상한대로 사용자의 동작(클라이언트)으로 인해 DB의 값(서버)이 변하지 않는 요청인 GET이 entities의 하위 api 파일에 위치할 수 있는 것이다.
그리고 widgets은 features의 상위 폴더이기에 features들의 모임이라면 widgets에 속할 수 있게 되는 개념으로 정의한 것이다.
이제 Next.js와 결합해볼까?
그럼 위 개념을 요즘 많이 쓰는 Next.js에서 어떻게 적용해보아야 할 까?
App 라우터를 쓰고 있다는 가정으로 폴더를 구성해보자.
기본적으로 App 라우터 폴더링은 다음과 같이 구성되어있다.
├── 📁 app
│ ├── 📁 페이지명1
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ ├── 📁 페이지명2
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
// ✅ 아래의 폴더들은 app폴더와 같은 위계에 있을수도 있고, 다른 위계에 있을 수도 있다.
...
├── 📁 components
│ ├── 컴포넌트명.tsx
│ ├── 컴포넌트명.tsx
│ └── 컴포넌트명.tsx
...
├── 📁 constant
├── 📁 fonts
├── 📁 lib
├── 📁 hooks
├── 📁 util
├── 📁 styles
├── 📁 types
...
├── .env
...
├── webpack.config.js
└── yarn.lock
app 하위에 페이지별로 폴더가 구성되어 있고, 각 폴더안 에는 layout.tsx, page.tsx가 존재한다.
그리고 각 페이지별로 필요한 components, constatns, hooks, utils, styles, types 와 같은 폴더는 apps와 동일한 위계에 놓일 수도 있고, 도메인명 폴더링,페이지명 폴더링 하위와 같은 곳에 놓일 수도 있다.
이제부터 FSD와 Next.js를 결합해보면서 폴더 구조를 만들어 가려고 하는데, components, constatns, hooks, utils, styles, type의 맥락에서 생성해야 하는 파일들을 도메인 하위로 놓는다는 전제로 글을 이어가려고 한다.
그리고 예시로 살펴보고자 상황은 블로그 테마를 목록으로 확인하고, 테마를 생성하고, 수정하는 기능이다.
1️⃣ App의 역할
FSD에서 정의하고 있는 app의 개념과 next.js의 app폴더가 겹치게 된다는 것을 가장 먼저 발견할 수 있다.
각각이 정의하고 있는 app의 역할이다.
구분 | FSD | Next.js |
주요 역할 | 애플리케이션의 전역 설정 (라우팅, 스토어 등) | 새로운 파일 시스템 기반 라우팅을 위한 디렉터리 |
포함 요소 | 라우터 설정, 스토어, 글로벌 스타일, 에러 처리 등 | 레이아웃, 페이지, 서버 컴포넌트, 로딩 처리 등 |
개념적 의미 | 애플리케이션 초기화의 중심 | 라우팅 중심 구조 |
사용 목적 | 아키텍처 관점에서의 책임 분리 | 프레임워크 관점에서의 디렉터리 규칙 |
Next.js의 app 디렉토리는 파일 기반 라우팅을 위한 강제 구조이다. 여기서의 app은 _"이 경로 안에서 페이지와 레이아웃을 정의하세요"_라는 Next.js의 문법적 요구사항에 가깝다.
반면, FSD에서 말하는 app은 애플리케이션 아키텍처 상에서 전역 설정이 모이는 곳이다. 라우터, 전역 상태관리(store), 글로벌 스타일, 전역 에러 핸들링 등, 말 그대로 애플리케이션 전체의 스켈레톤을 구성하는 영역이다.
두 개념 모두 라우팅 기능을 갖고 있다는 점에서 혼란이 발생할 수 있다.
하지만 목적은 다르다.
- Next.js는 파일 시스템 기반 라우팅만을 제공한다.
- FSD는 라우터 라이브러리 자체 설정을 포함한 앱 전체 흐름 제어를 담당한다.
즉, FSD는 Next.js의 라우팅 기능을 포함하거나 감싸는 상위 개념이라고 볼 수 있다.
FSD와 Next.js 모두 "앱"이라는 용어를 사용하지만, 그들이 말하는 앱의 범위와 관점은 다르다.
- FSD는 아키텍처 설계 원칙이다.
- Next.js는 프레임워크의 구현 규칙이다.
다만, Next.js가 제공하는 app 디렉토리를 FSD의 규칙 안에 포함시키는 방식으로 이해하면, 아래와 같이 폴더를 구성할 수 있다.
├── 📁 app
│ ├── 📁 _providers // ✅ 전역설정
│ │ ├── ThemeProvider.tsx
│ │ └── QueryProvider.tsx
│ ├── 📁 auth // ✅ auth 체크
│ ├── 📁 themes // ✅ 라우팅 구현
│ │ ├── 📁 [id]
│ │ │ ├── page.tsx
│ │ │ └── layout.tsx
│ │ ├── page.tsx
│ │ └── layout.tsx
두 개념이 충돌하기보다는 역할과 책임을 구분하는 방식으로 조화를 이룰 수 있다.
2️⃣ model, lib, hooks는 어떻게 통합하지?
hooks라는 폴더를 사용했던 이유는 무엇일까?
아마, React에서 "Custom Hook 만들기"를 처음 배울 때, 자연스럽게 hooks 폴더를 만들기도 했고, "이 훅, 여러 곳에서 쓰일 수 있으니까 공통 디렉토리에 빼야지." 라는 생각이 들면서 hooks라는 폴더링을 사용하지 않았을까 싶다.
hooks의 목적은 다음과 같이 간단하게 정의할 수 있다.
- UI 로직 재사용 ex) useModal
- 비즈니스 상태 관리 ex) useThemeCheckList
- API 호출 로직 ex) useGetThemeList, useThemeDetail
나는 FSD에서 hooks를 제거하고 공식문서에서 정의하고 있는 방향인 model, ui 쪽으로 통합하는 과정을 통해 기능 중심이 아닌 도메인 중심으로 설계를 진행하는 것이 목적이다.
그 관점으로 폴더를 1차적으로 개선해보면, 다음과 같다.
- UI 로직 재사용 👉 model
- 비즈니스 상태 관리 👉 model
- API 호출 로직 👉 api
model에는 어쨋든 데이터(클라이언트,서버의 입장)와 관련된 로직들이 포함되어야 하고, api에는 말 그대로 api호출 로직이 들어가면 된다.
그렇다면 lib은 어떻게 해야하는 거지?
FSD 공식 문서에서는 lib에 대한 명확한 가이드는 없다.
하지만 실무를 하다 보면, model만으로는 책임이 너무 많아지고, 다음과 같은 고민이 생기게 된다:
"상태 관련 로직과 유틸성 로직이 뒤섞이면서 model이 점점 커지고 무거워지는데, 이걸 어떻게 나눠야 하지?"
그래서 나는 다음과 같은 기준으로 lib의 역할을 정의하고, model과 분리하기로 했다.
하지만, 내가 예상했을 때 lib이 존재하지 않으면 model이 엄청나게 비대해질 가능성이 있다고 생각했다.
그래서 lib을 도입하여 model의 역할을 분산시키고자 했다.
수차례 언급하고 있는 데이터의 관점에서 다음과 같이 lib과 model을 분리하고자 했다.
구분 | model | lib |
역할 | 클라이언트·서버 데이터를 포함한 상태 주도 로직, API 요청, 상태 계산, 전역 상태 연동 등 |
사용자 인터랙션에 따른 순수한 연산·변환 로직, UI 상태 변경 유틸, 포맷터 등 |
예시 | useDeleteTheme useUpdateTheme useGetThemeDetails transformCreateThemeData |
useCheckedList calculateThemeCount |
폴더구조로 이해해보자.
- features/theme
├── 📁 features
│ ├── 📁 themes
│ │ ├── 📁 api
│ │ │ ├── deleteThemes.ts
│ │ │ └── postTheme.ts
│ │ ├── 📁 lib
│ │ │ └── useThemeCheckedList.tsx
│ │ ├── 📁 ui
│ │ │ ├── DeleteThemeButton.tsx
│ │ │ └── CreateThemeButton.tsx
│ │ └── 📁 model
│ │ │ ├── transformCreateThemeData.ts
│ │ │ └── useDeleteThemes.ts
폴더 | 설명 |
api | 테마 생성, 삭제 등 외부와의 통신 역할을 담당하는 API 요청 함수들이 위치 |
lib | 클라이언트 상호작용에서 파생되는 UI 중심 로직 위치. useThemeCheckedList.tsx는 체크 상태를 관리하기 위한 훅이지만, 상태 자체보다 UI 반응을 위한 로직에 가깝다. |
ui | 사용자 인터페이스 구성 요소. 실제 버튼이나 리스트 등 뷰 레이어 컴포넌트가 위치 |
model | 상태 계산, 데이터 변환, 상태 관리 훅 등 비즈니스 로직에 가까운 부분 |
- entites/themes
├── 📁 entities
│ ├── 📁 themes
│ │ ├── 📁 api
│ │ │ └── getThemeDetail.ts
│ │ ├── 📁 lib
│ │ │ └── adjustColor.tsx
│ │ ├── 📁 ui
│ │ │ ├── ThemeThumbnailList.tsx
│ │ │ └── ThemeThumbnailCard.tsx
│ │ └── 📁 model
│ │ │ ├── useThemeDetail.tsx
│ │ │ └── useThemeList.tsx
폴더 | 설명 |
api | 테마 상세 조회와 같은 읽기 기반 API 호출 로직이 위치 |
lib | UI 보조 기능 또는 순수 연산에 가까운 유틸 함수 |
ui | 사용자 인터페이스 구성 요소. 실제 테마를 보여주는 카드, 리스트 UI 컴포넌트가 위치 |
model | react-query 기반의 상태 훅 등, 외부 데이터와 내부 상태를 연결하는 비즈니스 로직 |
이와 같이 설계했을 때 얻을 수 있는 장점은 hooks라는 구현 방식 중심의 폴더를 제거함으로써, 도메인 책임에 따라 로직이 명확하게 위치할 수 있으며, model은 상태·데이터 중심 로직, lib은 순수 함수나 UI 헬퍼 역할, api는 외부 통신이라는 명확한 기준을 가질 수 있다는 것이다.
또한, 협업 시 “이 훅 어디 있지?”라는 질문 대신, “이 기능 어디 소속이지?”라는 질문이 되므로 의도 파악이 쉬워진다.
결론적으로 기능 중심이 아닌 도메인 중심으로 model, lib, hooks를 풀어가는 과정에서 hooks 폴더는 구현 방식 중심의 분류일 뿐, 도메인 중심 아키텍처에서는 적합하지 않다고 판단했다.
또한, FSD 관점에서는 UI, 상태, API 로직 모두 의미 있는 경계(도메인) 안에 배치해야 하기에 model은 상태와 데이터 흐름의 중심이 되고, lib은 그 흐름 안에서 쓰이는 보조 연산/가공 도구로써 기능을 분리하는 것이 이상적이라고 생각했다.
아직 함께 고민해봐야 할 FSD 정의들
FSD의 핵심은 ‘의미 있는 책임 단위로 코드를 나누는 것’이다.
하지만 실무에서는 명확히 떨어지지 않는 순간들이 많다. 아래는 개인적으로도 명확한 기준을 세우기 어려웠고, 함께 고민해보고 싶은 주제들이다.
이번 글에서는 FSD와 Next.js를 결합하며 `hooks` 디렉토리를 제거하고, `model`, `lib`, `api`로 로직을 재구성한 경험을 공유했다.
이 과정에서 "데이터를 중심으로 경계를 나눈다"는 사고방식이 설계를 어떻게 바꾸는지를 느낄 수 있었다.
하지만 여전히 아래와 같은 질문은 명확한 정답이 없기에, 함께 고민해보고 싶다.
질문 | 해석 |
lib의 범위는 어디까지 허용해야 할까? | 단순 유틸인지, 특정 도메인에 종속된 계산 로직인지에 따라 구분이 필요할까? |
공통 로직이 여러 도메인에서 반복될 경우, shared에 옮겨야 할 기준은 무엇일까? |
의존성과 추상화 수준의 균형은 어디에서 잡아야 할까? |
서버 컴포넌트가 많아지는 Next.js 환경에서 상태를 model로 유지하는 기준은 어떻게 달라져야 할까? |
- |
widgets는 언제 도입해야 하는가? | widgets가 많아질수록 결국 또다른"features 폴더"가 되지는 않을까? |
Next.js에서 서버 컴포넌트와 FSD는 어떻게 공존할까? | 서버 컴포넌트 내부에서 entities를 불러오는 게 자연스러운가? 아니면 그 자체도 entities로 봐야 하는가? |
정답보다는 상황에 따라 달라지는 선택의 연속일 수 있지만, 이런 구조적 고민을 계속 이어가는 것이 클린 아키텍처의 핵심이라고 믿는다.
그리고 FSD는 단순한 폴더 구조가 아니라 설계의 기준을 어디에 둘 것인가에 대한 이야기다.
정답은 존재하지 않고, 팀의 성격·기술 스택·프로젝트 성격에 따라 유연하게 정의되어야 한다.
그래서 이런 애매한 순간들이 오히려 팀과 함께 아키텍처 기준을 세우는 기회가 된다.
FSD를 도입하려는 사람, 도입했지만 의문을 느낀 사람들과 함께 '우리만의 설계 원칙'을 만들어가고 싶다.
이러한 나의 바람은 곧 커뮤니티 오픈으로 함께 될 예정이다.
PEC Community 에서 진행할 예정이다..!
PEC Community
최근 기술 컨텐츠(4월 오픈 예정) 기술의 등장 배경과 동작 원리에 대한 깊이 있는 컨텐츠를 제공합니다.
www.productengineer.info
'WEB > Next.js' 카테고리의 다른 글
Refine 이관 작업 ( feat. 중고나라 - 셀러지원센터 ) (1) | 2024.09.15 |
---|---|
이미지 최적화로 성능 개선하기 ( feat. FEW ) (0) | 2024.08.23 |
Link 태그와 SEO (0) | 2023.09.26 |
Next.js - V13 (0) | 2022.11.02 |
Next-auth (0) | 2022.07.12 |
들어가기
이번에 P.E.C 캠프 6기로 참여하면서 FSD의 개념을 Next.js와 결합해보는 과정을 진행했었다.
Product Engineer Camp - pec
8주동안 진행하는 Camp 를 통해서, 내 주변에 실제 문제를 해결하며 진짜 성장을 경험하세요. 다양한 UX framework 를 활용하여 설계하고, 이를 토대로 AI 와 효율적으로 협업하는 방법을 학습합니다.
slashpage.com
흔히 알고 있는 역할 중심의 설계를 벗어자세한 FSD의 개념은 하단 블로그에서 상세하게 소개시켜주고 있기에 여기서는 내가 정립했던 FSD와 Next.js의 개념에 대해서만 다뤄보고자 한다.
FSD 관점으로 바라보는 코드 경계 찾기
이번 글의 주제도 FSD(Feature-Slided Design)입니다! 현재 제가 가장 관심있는 관심사 2가지 중 하나가 바로 이 FSD 네요. 지금 하고 있는 일이 레거시 코드를 최신 기술로 고도화하는 작업입니다. 그러
velog.io
우리가 정의한 FSD의 개념
공식문서에도 자세한 설명이 나와있지만, P.E.C 캠프에서 경찬님과 6기 회원분들이랑 정의한 FSD의 개념은 아래와 같았다.
- app: FSD와 동일
- pages: FSD와 동일
- widgets: features들의 모임
- features: POST, PUT, DELETE와 같이 데이터의 상태가 변하는 요청들이 포함된 곳
- entities: GET와 같이 데이터를 보여주는 요청들이 포함된 곳
- shared: API 요청과는 관계없이 여러 도메인에서 사용하는 요소들이 포함된 곳
잘 안와닿을 수도 있겠지만, 집중하고 싶은 키워드는 데이터이다.
왜 데이터 인가? 어떤 데이터 인가?
나는 클라이언트 개발자이다.
그래서 이전까지만해도 클라이언트의 입장에서 레이어를 분리할 때 경계를 아래의 사진처럼 세로로 구현했었다.

위 구조의 장/단점은 아래와 같다.
- 장점
- 컴포넌트와 API의 스펙이 분리되어지기에 API 스펙 변동이 발생하면, 서버와 클라이언트 스펙 간의 변환지점만 수정해주면 된다. 즉, 컴포넌트 자체를 수정해야 하는 일은 거의 없다. 👉 클라이언트와 서버 사이의 의존성을 끊어낼 수 있다.
- 단점
- 단순히 API 응답값을 화면에 보여주는 과정에서도 서버와 클라이언트 스펙 2가지를 모두 구현해야 하기에 중복으로 선언되는 타입들과 변환 과정을 구현하는 파일들이 불필요하게 생겨나게 된다.
사실 이 구조는 데이터의 관점을 클라이언트와 서버 2가지로 바라보고 있었기 때문에 만들어졌다고 볼 수 있다.
내가 과거에 이 방향을 지향했던 이유이다.
클라이언트 개발자의 입장에서 서버와 클라이언트 스펙 자체를 완전히 분리시키면 의존성을 끊어낸다면, API 스펙이 완성되기전까지 마냥 기다리는 클라이언트 개발자가 아니라, API가 개발되어지고 나면 바로 클라이언트와 연결작업을 진행할 수 있는 상태로 업무를 진행할 수 있다는 장점이 컸었다.
다만, 앞서 말한 단점으로 인해 분리가 모호해지는 순간들과 똑같은 프로퍼티를 가지는 타입과 파일들이 점차 비대해지기 시작했다.
결론적으로 바라보았을 때, 서버 API 스펙이 변경되면 클라이언트 스펙들의 수정범위도 점점 퍼져나가게 되는 상황이 오게 된 것이다.
이것이 데이터를 클라이언트, 서버 2개로 나눠서 보았을 때의 상황이다.
그렇다면 데이터를 1개로 바라본다면 어떤 상황에 마주할 수 있을까?

클라이언트, 서버 간의 경계가 사라지고, 하나로 묶어진 상황에서 계층이 좌우가 아닌 위/아래로 나뉘어지게 되는 것이다.
위 구조의 장/단점은 아래와 같다.
- 장점
- 데이터를 2개로 바라보았을때, 중복으로 선언되는 타입들과 변환 과정을 구현하는 파일들이 불필요하게 생겨나지 않는다. 앞서 말한 단점이 사라지게 되는 것이다.
- 단점
- 클라이언트와 서버의 의존성이 있기 때문에 한 쪽에서 변경점이 발생하면 반드시 다른 한 곳에서도 수정이 발생하게 된다.
그러면 여기서 궁금증이 생길 것이다.
1번과 2번을 나누는 기준은 무엇으로 잡아야 하는 것 일까?
이 질문이 앞서 정의한 FSD의 개념의 시발점이 되어주었다.
다시 데이터 1개의 관점에서 FSD 개념을 살펴보자.
앞서 언급한 FSD의 개념에서 features, entities의 정의를 다시 살펴보자.
- features: POST, PUT, DELETE와 같이 데이터의 상태가 변하는 요청들이 포함된 곳 👉 변하는 데이터들이 모인 곳
- entities: GET와 같이 데이터를 보여주는 요청들이 포함된 곳 👉 변하지 않는 데이터들이 모인 곳
각각에 대한 정의를 어떻게 해석하고 있는가?
변하는 데이터 vs 변하지 않는 데이터
변한다는 기준이 클라이언트의 입장으로만 바라보고 있는 것이 아니라 클라이언트와 서버의 2가지 관점을 1개의 그룹으로 뭉쳐서 정의하고 있다는 것을 알 수 있다.
한 번, segements까지 예시를 들어보면
├── 📁 feautres
│ ├── 📁 도메인명
│ │ │ ├── 📁 api // ✅ POST,PUT,DELETE 요청
│ │ │ ├── 📁 model
│ │ │ └── 📁 ui
├── 📁 entities
│ ├── 📁 도메인명 // ✅ GET 요청
│ │ │ ├── 📁 api
│ │ │ ├── 📁 model
│ │ │ └── 📁 ui
features의 하위 세그먼트 안의 api폴더에는 변하는 것들의 요청들이 들어가야 한다.
즉, 사용자의 동작(클라이언트)으로 인해 DB의 값(서버)이 변하게 되는 요청인 POST, PUT, DELETE 등이 features의 api 파일에 위치할 수 있는 것이다.
그렇다면 entities의 개념은 자연스럽게 유추되지 않는가?
예상한대로 사용자의 동작(클라이언트)으로 인해 DB의 값(서버)이 변하지 않는 요청인 GET이 entities의 하위 api 파일에 위치할 수 있는 것이다.
그리고 widgets은 features의 상위 폴더이기에 features들의 모임이라면 widgets에 속할 수 있게 되는 개념으로 정의한 것이다.
이제 Next.js와 결합해볼까?
그럼 위 개념을 요즘 많이 쓰는 Next.js에서 어떻게 적용해보아야 할 까?
App 라우터를 쓰고 있다는 가정으로 폴더를 구성해보자.
기본적으로 App 라우터 폴더링은 다음과 같이 구성되어있다.
├── 📁 app
│ ├── 📁 페이지명1
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ ├── 📁 페이지명2
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
// ✅ 아래의 폴더들은 app폴더와 같은 위계에 있을수도 있고, 다른 위계에 있을 수도 있다.
...
├── 📁 components
│ ├── 컴포넌트명.tsx
│ ├── 컴포넌트명.tsx
│ └── 컴포넌트명.tsx
...
├── 📁 constant
├── 📁 fonts
├── 📁 lib
├── 📁 hooks
├── 📁 util
├── 📁 styles
├── 📁 types
...
├── .env
...
├── webpack.config.js
└── yarn.lock
app 하위에 페이지별로 폴더가 구성되어 있고, 각 폴더안 에는 layout.tsx, page.tsx가 존재한다.
그리고 각 페이지별로 필요한 components, constatns, hooks, utils, styles, types 와 같은 폴더는 apps와 동일한 위계에 놓일 수도 있고, 도메인명 폴더링,페이지명 폴더링 하위와 같은 곳에 놓일 수도 있다.
이제부터 FSD와 Next.js를 결합해보면서 폴더 구조를 만들어 가려고 하는데, components, constatns, hooks, utils, styles, type의 맥락에서 생성해야 하는 파일들을 도메인 하위로 놓는다는 전제로 글을 이어가려고 한다.
그리고 예시로 살펴보고자 상황은 블로그 테마를 목록으로 확인하고, 테마를 생성하고, 수정하는 기능이다.
1️⃣ App의 역할
FSD에서 정의하고 있는 app의 개념과 next.js의 app폴더가 겹치게 된다는 것을 가장 먼저 발견할 수 있다.
각각이 정의하고 있는 app의 역할이다.
구분 | FSD | Next.js |
주요 역할 | 애플리케이션의 전역 설정 (라우팅, 스토어 등) | 새로운 파일 시스템 기반 라우팅을 위한 디렉터리 |
포함 요소 | 라우터 설정, 스토어, 글로벌 스타일, 에러 처리 등 | 레이아웃, 페이지, 서버 컴포넌트, 로딩 처리 등 |
개념적 의미 | 애플리케이션 초기화의 중심 | 라우팅 중심 구조 |
사용 목적 | 아키텍처 관점에서의 책임 분리 | 프레임워크 관점에서의 디렉터리 규칙 |
Next.js의 app 디렉토리는 파일 기반 라우팅을 위한 강제 구조이다. 여기서의 app은 _"이 경로 안에서 페이지와 레이아웃을 정의하세요"_라는 Next.js의 문법적 요구사항에 가깝다.
반면, FSD에서 말하는 app은 애플리케이션 아키텍처 상에서 전역 설정이 모이는 곳이다. 라우터, 전역 상태관리(store), 글로벌 스타일, 전역 에러 핸들링 등, 말 그대로 애플리케이션 전체의 스켈레톤을 구성하는 영역이다.
두 개념 모두 라우팅 기능을 갖고 있다는 점에서 혼란이 발생할 수 있다.
하지만 목적은 다르다.
- Next.js는 파일 시스템 기반 라우팅만을 제공한다.
- FSD는 라우터 라이브러리 자체 설정을 포함한 앱 전체 흐름 제어를 담당한다.
즉, FSD는 Next.js의 라우팅 기능을 포함하거나 감싸는 상위 개념이라고 볼 수 있다.
FSD와 Next.js 모두 "앱"이라는 용어를 사용하지만, 그들이 말하는 앱의 범위와 관점은 다르다.
- FSD는 아키텍처 설계 원칙이다.
- Next.js는 프레임워크의 구현 규칙이다.
다만, Next.js가 제공하는 app 디렉토리를 FSD의 규칙 안에 포함시키는 방식으로 이해하면, 아래와 같이 폴더를 구성할 수 있다.
├── 📁 app
│ ├── 📁 _providers // ✅ 전역설정
│ │ ├── ThemeProvider.tsx
│ │ └── QueryProvider.tsx
│ ├── 📁 auth // ✅ auth 체크
│ ├── 📁 themes // ✅ 라우팅 구현
│ │ ├── 📁 [id]
│ │ │ ├── page.tsx
│ │ │ └── layout.tsx
│ │ ├── page.tsx
│ │ └── layout.tsx
두 개념이 충돌하기보다는 역할과 책임을 구분하는 방식으로 조화를 이룰 수 있다.
2️⃣ model, lib, hooks는 어떻게 통합하지?
hooks라는 폴더를 사용했던 이유는 무엇일까?
아마, React에서 "Custom Hook 만들기"를 처음 배울 때, 자연스럽게 hooks 폴더를 만들기도 했고, "이 훅, 여러 곳에서 쓰일 수 있으니까 공통 디렉토리에 빼야지." 라는 생각이 들면서 hooks라는 폴더링을 사용하지 않았을까 싶다.
hooks의 목적은 다음과 같이 간단하게 정의할 수 있다.
- UI 로직 재사용 ex) useModal
- 비즈니스 상태 관리 ex) useThemeCheckList
- API 호출 로직 ex) useGetThemeList, useThemeDetail
나는 FSD에서 hooks를 제거하고 공식문서에서 정의하고 있는 방향인 model, ui 쪽으로 통합하는 과정을 통해 기능 중심이 아닌 도메인 중심으로 설계를 진행하는 것이 목적이다.
그 관점으로 폴더를 1차적으로 개선해보면, 다음과 같다.
- UI 로직 재사용 👉 model
- 비즈니스 상태 관리 👉 model
- API 호출 로직 👉 api
model에는 어쨋든 데이터(클라이언트,서버의 입장)와 관련된 로직들이 포함되어야 하고, api에는 말 그대로 api호출 로직이 들어가면 된다.
그렇다면 lib은 어떻게 해야하는 거지?
FSD 공식 문서에서는 lib에 대한 명확한 가이드는 없다.
하지만 실무를 하다 보면, model만으로는 책임이 너무 많아지고, 다음과 같은 고민이 생기게 된다:
"상태 관련 로직과 유틸성 로직이 뒤섞이면서 model이 점점 커지고 무거워지는데, 이걸 어떻게 나눠야 하지?"
그래서 나는 다음과 같은 기준으로 lib의 역할을 정의하고, model과 분리하기로 했다.
하지만, 내가 예상했을 때 lib이 존재하지 않으면 model이 엄청나게 비대해질 가능성이 있다고 생각했다.
그래서 lib을 도입하여 model의 역할을 분산시키고자 했다.
수차례 언급하고 있는 데이터의 관점에서 다음과 같이 lib과 model을 분리하고자 했다.
구분 | model | lib |
역할 | 클라이언트·서버 데이터를 포함한 상태 주도 로직, API 요청, 상태 계산, 전역 상태 연동 등 |
사용자 인터랙션에 따른 순수한 연산·변환 로직, UI 상태 변경 유틸, 포맷터 등 |
예시 | useDeleteTheme useUpdateTheme useGetThemeDetails transformCreateThemeData |
useCheckedList calculateThemeCount |
폴더구조로 이해해보자.
- features/theme
├── 📁 features
│ ├── 📁 themes
│ │ ├── 📁 api
│ │ │ ├── deleteThemes.ts
│ │ │ └── postTheme.ts
│ │ ├── 📁 lib
│ │ │ └── useThemeCheckedList.tsx
│ │ ├── 📁 ui
│ │ │ ├── DeleteThemeButton.tsx
│ │ │ └── CreateThemeButton.tsx
│ │ └── 📁 model
│ │ │ ├── transformCreateThemeData.ts
│ │ │ └── useDeleteThemes.ts
폴더 | 설명 |
api | 테마 생성, 삭제 등 외부와의 통신 역할을 담당하는 API 요청 함수들이 위치 |
lib | 클라이언트 상호작용에서 파생되는 UI 중심 로직 위치. useThemeCheckedList.tsx는 체크 상태를 관리하기 위한 훅이지만, 상태 자체보다 UI 반응을 위한 로직에 가깝다. |
ui | 사용자 인터페이스 구성 요소. 실제 버튼이나 리스트 등 뷰 레이어 컴포넌트가 위치 |
model | 상태 계산, 데이터 변환, 상태 관리 훅 등 비즈니스 로직에 가까운 부분 |
- entites/themes
├── 📁 entities
│ ├── 📁 themes
│ │ ├── 📁 api
│ │ │ └── getThemeDetail.ts
│ │ ├── 📁 lib
│ │ │ └── adjustColor.tsx
│ │ ├── 📁 ui
│ │ │ ├── ThemeThumbnailList.tsx
│ │ │ └── ThemeThumbnailCard.tsx
│ │ └── 📁 model
│ │ │ ├── useThemeDetail.tsx
│ │ │ └── useThemeList.tsx
폴더 | 설명 |
api | 테마 상세 조회와 같은 읽기 기반 API 호출 로직이 위치 |
lib | UI 보조 기능 또는 순수 연산에 가까운 유틸 함수 |
ui | 사용자 인터페이스 구성 요소. 실제 테마를 보여주는 카드, 리스트 UI 컴포넌트가 위치 |
model | react-query 기반의 상태 훅 등, 외부 데이터와 내부 상태를 연결하는 비즈니스 로직 |
이와 같이 설계했을 때 얻을 수 있는 장점은 hooks라는 구현 방식 중심의 폴더를 제거함으로써, 도메인 책임에 따라 로직이 명확하게 위치할 수 있으며, model은 상태·데이터 중심 로직, lib은 순수 함수나 UI 헬퍼 역할, api는 외부 통신이라는 명확한 기준을 가질 수 있다는 것이다.
또한, 협업 시 “이 훅 어디 있지?”라는 질문 대신, “이 기능 어디 소속이지?”라는 질문이 되므로 의도 파악이 쉬워진다.
결론적으로 기능 중심이 아닌 도메인 중심으로 model, lib, hooks를 풀어가는 과정에서 hooks 폴더는 구현 방식 중심의 분류일 뿐, 도메인 중심 아키텍처에서는 적합하지 않다고 판단했다.
또한, FSD 관점에서는 UI, 상태, API 로직 모두 의미 있는 경계(도메인) 안에 배치해야 하기에 model은 상태와 데이터 흐름의 중심이 되고, lib은 그 흐름 안에서 쓰이는 보조 연산/가공 도구로써 기능을 분리하는 것이 이상적이라고 생각했다.
아직 함께 고민해봐야 할 FSD 정의들
FSD의 핵심은 ‘의미 있는 책임 단위로 코드를 나누는 것’이다.
하지만 실무에서는 명확히 떨어지지 않는 순간들이 많다. 아래는 개인적으로도 명확한 기준을 세우기 어려웠고, 함께 고민해보고 싶은 주제들이다.
이번 글에서는 FSD와 Next.js를 결합하며 `hooks` 디렉토리를 제거하고, `model`, `lib`, `api`로 로직을 재구성한 경험을 공유했다.
이 과정에서 "데이터를 중심으로 경계를 나눈다"는 사고방식이 설계를 어떻게 바꾸는지를 느낄 수 있었다.
하지만 여전히 아래와 같은 질문은 명확한 정답이 없기에, 함께 고민해보고 싶다.
질문 | 해석 |
lib의 범위는 어디까지 허용해야 할까? | 단순 유틸인지, 특정 도메인에 종속된 계산 로직인지에 따라 구분이 필요할까? |
공통 로직이 여러 도메인에서 반복될 경우, shared에 옮겨야 할 기준은 무엇일까? |
의존성과 추상화 수준의 균형은 어디에서 잡아야 할까? |
서버 컴포넌트가 많아지는 Next.js 환경에서 상태를 model로 유지하는 기준은 어떻게 달라져야 할까? |
- |
widgets는 언제 도입해야 하는가? | widgets가 많아질수록 결국 또다른"features 폴더"가 되지는 않을까? |
Next.js에서 서버 컴포넌트와 FSD는 어떻게 공존할까? | 서버 컴포넌트 내부에서 entities를 불러오는 게 자연스러운가? 아니면 그 자체도 entities로 봐야 하는가? |
정답보다는 상황에 따라 달라지는 선택의 연속일 수 있지만, 이런 구조적 고민을 계속 이어가는 것이 클린 아키텍처의 핵심이라고 믿는다.
그리고 FSD는 단순한 폴더 구조가 아니라 설계의 기준을 어디에 둘 것인가에 대한 이야기다.
정답은 존재하지 않고, 팀의 성격·기술 스택·프로젝트 성격에 따라 유연하게 정의되어야 한다.
그래서 이런 애매한 순간들이 오히려 팀과 함께 아키텍처 기준을 세우는 기회가 된다.
FSD를 도입하려는 사람, 도입했지만 의문을 느낀 사람들과 함께 '우리만의 설계 원칙'을 만들어가고 싶다.
이러한 나의 바람은 곧 커뮤니티 오픈으로 함께 될 예정이다.
PEC Community 에서 진행할 예정이다..!
PEC Community
최근 기술 컨텐츠(4월 오픈 예정) 기술의 등장 배경과 동작 원리에 대한 깊이 있는 컨텐츠를 제공합니다.
www.productengineer.info
'WEB > Next.js' 카테고리의 다른 글
Refine 이관 작업 ( feat. 중고나라 - 셀러지원센터 ) (1) | 2024.09.15 |
---|---|
이미지 최적화로 성능 개선하기 ( feat. FEW ) (0) | 2024.08.23 |
Link 태그와 SEO (0) | 2023.09.26 |
Next.js - V13 (0) | 2022.11.02 |
Next-auth (0) | 2022.07.12 |