폴더 구조, 다들 한 번쯤 고민해봤을 거다
프론트엔드 프로젝트를 시작하면 가장 먼저 하는 일 중 하나가 폴더 구조를 잡는 거다. components, pages, utils, hooks, 대부분 이 정도로 시작한다. 처음엔 깔끔하다. 파일이 열 개쯤일 때는.
문제는 프로젝트가 커지면서 시작된다. components 폴더 안에 파일이 50개, 100개가 되면 "이 컴포넌트가 여기 있는 게 맞나?" 같은 질문이 반복된다. Button과 SearchResultCard가 같은 폴더에 있다. 하나는 어디서든 쓰는 범용 컴포넌트고, 다른 하나는 검색 기능에서만 쓰는 컴포넌트인데, 둘의 위상이 같다.
utils도 마찬가지다. 날짜 포맷팅 함수와 장바구니 계산 로직이 나란히 앉아 있다. 하나는 순수한 유틸리티고, 다른 하나는 비즈니스 로직인데, 폴더 구조는 그 차이를 말해주지 않는다.
결국 "아, 그건 components 안에 있는데... 어디더라" 하면서 검색을 돌리는 자신을 발견하게 된다. 폴더 구조가 코드를 정리하는 게 아니라, 그냥 담아두는 역할만 하고 있는 거다.
기능 기반 구조, 그리고 그 너머
이 문제를 해결하려는 시도는 많았다. 가장 대표적인 게 기능 기반 구조다. features/search, features/cart, features/auth 이런 식으로 기능 단위로 묶는 거다. 타입별로 나누는 것보다는 확실히 낫다. 관련 있는 코드가 한곳에 모이니까.
하지만 이것도 프로젝트가 커지면 한계가 온다. 검색 결과를 장바구니에 담는 기능은 search에 넣어야 하나, cart에 넣어야 하나? 여러 기능에서 공통으로 쓰는 User 모델은 어디에 둬야 하나? 기능과 기능 사이의 경계가 모호해지면서, 또다시 "이거 어디에 넣지?"가 시작된다.
기능 기반 구조의 문제는 수평적이라는 데 있다. 모든 기능이 같은 층위에 있으니, 기능 간의 의존 관계가 제멋대로 얽히기 쉽다. search가 cart를 참조하고, cart가 다시 search를 참조하는 순환 의존이 생기면, 그때부터 구조는 이름만 깔끔하지 실제로는 스파게티가 된다.
FSD가 제안하는 것
Feature-Sliced Design은 여기서 한 발 더 나간다. 핵심 아이디어는 간단하다. 코드를 역할에 따라 레이어로 나누고 , 각 레이어 안에서 도메인에 따라 슬라이스로 나눈다 .
레이어는 6개다.
- app , 앱 전체의 초기화, 라우팅, 프로바이더 설정
- pages , URL과 1:1로 대응하는 페이지 단위
- widgets , 페이지 안에서 독립적으로 동작하는 큰 블록
- features , 사용자가 할 수 있는 하나의 행동 단위
- entities , 비즈니스 도메인 모델
- shared , 특정 도메인에 속하지 않는 공용 코드
그리고 각 레이어 안의 슬라이스는 세그먼트로 나뉜다. ui, model, api, lib 같은 것들이다. 예를 들어 features/search라는 슬라이스 안에 ui (검색 바 컴포넌트), model (검색 상태 관리), api (검색 API 호출) 가 들어간다. 하나의 기능에 필요한 모든 것이 한 슬라이스 안에 모이되, 역할별로 정리되어 있는 거다.
entities가 어색하게 느껴졌던 이유
처음 FSD를 접했을 때 가장 낯설었던 건 entities 레이어였다. entity, model, 이 단어들은 백엔드에서 먼저 만나는 개념이다. 서버에서 entity라고 하면 보통 DB 테이블과 매핑되는 객체가 떠오른다. ORM의 모델, 스키마 정의, 마이그레이션. 그런 맥락이 머릿속에 박혀 있으니, 프론트엔드 폴더 구조에서 entities라는 이름을 보면 어딘가 어색하다.
하지만 프론트엔드에서 entity는 그런 게 아니다. 여기서 entity는 앱이 다루는 대상 그 자체다. 이 앱이 "책"을 다루는 앱이라면, "책"이라는 개념이 entity다. 검색 기능이 존재하려면 먼저 "책"이 있어야 한다. 장바구니에 담으려면 먼저 "책"이 있어야 한다. 위시리스트에 추가하려면 먼저 "책"이 있어야 한다. entity는 기능들이 작동하기 위한 토대, 기능 이전에 있어야 하는 개념이다.
그래서 FSD에서 entities가 features보다 아래 레이어에 있는 거다. 기능은 entity 위에서 동작한다. 검색이라는 기능이 book이라는 entity를 참조하는 건 자연스럽지만, book이 검색을 알아야 할 이유는 없다. 이 관계가 레이어 구조에 그대로 반영되어 있다.
적용해보니 느낀 것
처음엔 솔직히 번거로웠다. 레이어가 6개나 되고, 슬라이스 안에 또 세그먼트가 있고, 파일 하나 만들 때마다 "이게 entity인가 feature인가" 고민해야 했다. 기존에 components 폴더에 넣으면 끝이었던 것과 비교하면, 분명 초기 비용이 있다.
그런데 어느 순간 감이 잡히기 시작했다. "이건 book이라는 도메인 모델이니까 entities/book", "이건 검색이라는 사용자 행동이니까 features/search", "이건 어디서든 쓰는 UI 컴포넌트니까 shared/ui", 이런 판단이 자연스러워졌다.
그 뒤부터는 신기하게도 "이 코드 어디에 넣지?"라는 고민이 거의 사라졌다. 코드의 역할을 생각하면 들어갈 자리가 정해져 있으니까. 새 파일을 만들 때 위치를 고민하는 게 아니라, 코드의 성격을 판단하면 위치가 따라오는 느낌이었다.
진짜 중요한 건 레이어 사이의 규칙이다
FSD에서 폴더 이름은 사실 부차적이다. 진짜 핵심은 레이어 간의 의존 규칙이다.
위의 레이어는 아래의 레이어를 참조할 수 있지만, 그 역은 안 된다. pages는 features를 쓸 수 있고, features는 entities를 쓸 수 있다. 하지만 entities가 features를 import하는 건 금지다. 그리고 같은 레이어 안의 슬라이스끼리는 서로 참조할 수 없다. features/search가 features/cart를 직접 import하는 것도 금지다.
이 두 가지 규칙이 의존성의 방향을 강제한다. 아래로만 흐르고, 옆으로는 흐르지 않는다. 단순한 규칙이지만, 이걸 지키는 것만으로 구조가 무너지지 않는다.
생각해보면 당연하다. 순환 의존이 생기면 "A를 고치려면 B를 알아야 하고, B를 고치려면 A를 알아야 하는" 상황이 온다. 레이어 간 단방향 규칙은 이걸 원천 차단한다. 어떤 코드를 수정할 때 영향 범위가 명확하다. entities/book을 고치면 위의 레이어(features, widgets, pages)만 확인하면 된다. 옆의 entities/user를 걱정할 필요가 없다.
여기에 하나 더 있다. FSD에서 각 슬라이스는 Public API를 통해서만 외부에 노출된다. 보통 index.ts 파일이 그 역할을 한다. 슬라이스 바깥에서는 이 Public API만 import할 수 있고, 내부의 파일 구조에 직접 접근하는 건 금지다. features/search/model/store.ts를 직접 가져오는 게 아니라, features/search가 공개한 것만 쓸 수 있다.
이게 왜 중요하냐면, 슬라이스 내부를 자유롭게 리팩토링할 수 있게 해주기 때문이다. 내부에서 파일을 쪼개든 합치든, Public API만 유지하면 바깥은 아무것도 고칠 필요가 없다. 슬라이스가 사실상 하나의 모듈처럼 작동하는 거다. 레이어 규칙이 수직 방향의 질서를 잡아준다면, Public API는 각 슬라이스의 경계를 단단하게 만들어준다.
규칙을 안 지키면 오히려 복잡해진다
한 가지 분명히 해둘 게 있다. FSD의 폴더 이름만 따라 하고 규칙을 안 지키면, 기존 구조보다 오히려 나빠진다.
레이어를 6개로 나눠놨는데 features끼리 서로 import하고, entities에서 features를 참조하면 어떻게 될까. 폴더 구조만 복잡해졌지, 의존성 그래프는 여전히 스파게티다. 게다가 "우리 FSD 쓰고 있어요"라는 착각까지 생기니까 문제를 인식하기도 더 어려워진다.
FSD는 폴더 이름표가 아니라 의존 규칙이다. 규칙 없이 이름만 빌려오면 그건 FSD가 아니라 폴더 6개짜리 기존 구조일 뿐이다.
모든 프로젝트에 필요한 건 아니다
FSD가 빛을 발하는 건 비즈니스 도메인이 있는 애플리케이션이다. 사용자가 있고, 도메인 모델이 있고, 기능이 여러 개 얽혀 있는 앱. 이커머스, 대시보드, SaaS 같은 프로젝트에서 구조적 질서를 잡아주는 도구다.
반면에 랜딩 페이지 하나, 유틸리티 라이브러리, 작은 사이드 프로젝트에 FSD를 쓰는 건 오버킬이다. entities와 features로 나눌 도메인이 없으니까. 망치가 필요한 곳에 망치를 쓰면 되지, 모든 곳에 들고 다닐 필요는 없다.
비즈니스 목표가 있는 앱을 만들고 있다면, 한 번쯤 이 구조로 잡아보는 걸 권한다. 초기 비용이 있지만, 프로젝트가 커질수록 그 비용은 빠르게 회수된다. "이 코드 어디에 있어야 하지?"라는 질문이 사라지는 경험은, 한 번 해보면 돌아가기 어렵다.