목차14개 항목
출처와 참고
테오(teo.v)의 글 「폴더구조의 변화로 이해하는 프론트엔드 멘탈모델 변천사」(2025.06)를 요약·재구성한 노트다.
폴더 구조는 멘탈 모델이다
멘탈 모델은 "이 프로젝트가 어떻게 생겼고, 각 부분이 어떻게 연결되는가"에 대한 머릿속 그림이다. 사람은 정보를 위치와 함께 기억하고 예측 가능한 패턴을 좋아한다. 폴더 구조가 멘탈 모델과 맞으면 "이 코드 어디 있더라", "새 코드는 어디 두지" 같은 고민이 줄어든다. 작은 프로젝트에서 괜찮던 구조가 커지며 문제가 되는 것도 결국 머릿속 구조와 코드 구조가 어긋나기 때문이다.
정답인 구조는 없다. 폴더 구조가 어떤 문제를 풀려다 어떻게 진화했는지 알면, 지금 프로젝트에 맞는 구조를 고르는 판단 기준이 생긴다.
1부. 일반적인 구조가 되기까지
flowchart LR A["역할 기반<br>(components, hooks, ...)"] --> B["역할 안 세분화<br>(파일이 많아짐)"] --> C["Atomic Design<br>(계층만으로는 분류 불가)"] --> D["UI / 도메인 분리<br>(도메인 축 발견)"] --> E["페이지 독립<br>(파일 기반 라우팅)"]
1. 역할 기반 구조
/components, /hooks, /services, /utils. 직관적이고 튜토리얼 대부분이 이렇게 시작한다. 파일이 적을 땐 충분하다. 파일이 늘면 폴더마다 하위 폴더가 필요해지는데, 이 단계엔 정해진 규칙이 없어 기준이 흔들린다.
2. Atomic Design의 한계
원자 → 분자 → 유기체 → 템플릿 → 페이지는 디자인 시스템에는 잘 맞지만 폴더 구조로는 반만 통한다. ProductCard처럼 비즈니스 로직이 들어간 컴포넌트는 분자인지 유기체인지 애매해진다. 원인은 컴포넌트를 계층 하나의 축으로만 나눌 수 없다는 데 있다. 자세한 내용은 Atomic Design과 컴포넌트 폴더 구조에 있다.
3. 순수 UI 컴포넌트와 도메인 컴포넌트
| 순수 UI 컴포넌트 | 도메인 컴포넌트 | |
|---|---|---|
| 예 | Button, Input, Modal | ProductItem, UserProfile, OrderSummary |
| 도메인 지식 | 필요 없음 | 특정 비즈니스 개념을 표현 |
| 재사용성 | 높을수록 좋다 | 중요한 가치가 아니다 |
| 따라가는 변화 | 디자인 시스템 | 비즈니스 요구사항 |
Button처럼 props로 모든 걸 주입받는 방식을 도메인 컴포넌트에 적용하면 조건문투성이가 된다. 같은 화면이라도 도메인이 다르면 중복을 인정하고 나눈다. 판단 기준과 어댑터 예시는 컴포넌트를 분리하는 기준과 방법에 있다.
도메인 컴포넌트를 다루려고 나온 패턴들:
| 패턴 | 푸는 문제 |
|---|---|
| Container–Presenter | 로직(컨테이너)과 UI(프레젠터) 분리. 카탈로그용·장바구니용 컨테이너가 각자 맥락에 맞는 로직을 갖는다 |
| 커스텀 훅 | 비즈니스 로직을 컴포넌트 밖으로 |
| Context·전역 상태 | props drilling 없이 도메인 상태에 직접 접근 ("모두 props로" → "필요한 것을 직접") |
이 UI/도메인 구분은 컴포넌트만의 일이 아니다. 훅(useDebounce vs useCart), API(httpClient vs cartApi), 유틸(formatDate vs calculateShippingFee)에도 똑같이 있다. 그래서 역할 폴더 안을 ui / common과 도메인별로 나누게 됐다.
4. 페이지의 독립
HomePage가 Button 옆에 있고, 라우트 설정은 따로 있어 URL → 컴포넌트를 찾으려면 import를 따라가야 했다. 페이지는 라우팅 진입점이자 데이터 페칭의 출발점이고 SEO를 맡는 다른 종류의 컴포넌트다. 파일 기반 라우팅(/pages/products/[id].tsx)이 이를 구조로 드러냈다.
지금의 일반적인 구조
/src
/pages (또는 /app) 라우팅 진입점
/components
/ui 순수 UI
/product /cart 도메인
/layout
/hooks /ui /product /cart /auth
/services /api /product /cart
/store
/utils /common /product
/types역할(1차) + 도메인(2차) + 독립된 페이지. 필요할 때 폴더를 하나씩 더하며 점진적으로 키울 수 있어서 널리 쓰인다. 중간 규모까지는 이걸로 충분하다.
2부. 더 커지면: 기능 중심 응집
규모가 임계점을 넘으면 코드 파편화가 드러난다. 장바구니 하나를 고치려고 /components/cart, /hooks/cart, /services/cart, /store/cart를 오간다. 초기엔 화면 중심으로 생각하지만 프로젝트가 성숙하면 문제는 데이터 흐름에서 생기고, 역할 구조는 그 흐름을 끊어 놓는다. 역할 구조는 쓸 때 분류하기 편하지만 이해·디버깅하기엔 불편하다.
대안으로는 모노레포 패키지, 마이크로 프론트엔드, 레이어드 아키텍처, 기능 중심 아키텍처(FSD) 등이 있다. 기능 중심 구조는 세 가지 응집 기준으로 이해할 수 있다.
| 응집 기준 | FSD 이름 | 관심사 | 무엇을 모으나 |
|---|---|---|---|
| 데이터 | entities | 무엇을 표시하나 | 원천 데이터 모델과 그 흐름: API(DTO) → 변환(엔티티) → 계산 → 기본 표현 |
| 화면 | widgets | 어디에, 어떤 구조로 배치하나 | 페이지 안의 독립된 구획 (이미지 갤러리, 리뷰 목록, 추천 상품) |
| 행동 | features | 어떻게, 왜 다루나 | 사용자 행동 하나의 전체 흐름 (검색 입력 → 요청 → 가공 → 결과 표시) |
엔티티: 데이터 흐름을 한곳에
도메인을 최상위로 올리면 폴더만 봐도 서비스가 무엇을 다루는지 보인다. 도메인 안은 엔티티(원천 데이터 모델) 를 기준으로 묶는다. 서버 DTO를 프론트엔드 모델로 바꾸는 함수, 엔티티 기반 계산, 기본 표현이 한 흐름으로 모인다. "이 데이터 어디서 왔지?"를 덜 묻게 된다.
모든 도메인에 필요한 건 아니다. 거의 안 바뀌는 페이지나 UI 조합 수준의 도메인엔 과하다. Product·User·Order·Cart처럼 여러 화면에서 재사용되고 흐름이 복잡한 핵심 데이터에 효과가 크다.
위젯: 화면의 구획을 코드로
거대한 페이지를 사람이 화면을 볼 때 자연스럽게 느끼는 시각적·기능적 구획으로 나눈다. 다들 Section, Block, Area 같은 이름으로 이미 하던 일을 명시적 컨벤션으로 만든 것이다.
| 계층 | 책임 |
|---|---|
| 페이지 | 라우팅 진입점, 레이아웃, 위젯 조립, 페이지 수준 데이터와 구획 간 조율 |
| 위젯 (화면 구획) | 독립된 관심사, 필요한 데이터·상태를 스스로 관리, 다른 페이지에서도 재사용 |
| UI 컴포넌트 | 시각 표현, props만 받는 순수 함수 지향 |
기능: 사용자 행동의 흐름을 한 폴더에
프론트엔드의 사이클은 데이터 → 화면 → 사용자 행동 → 데이터 → … 이다. 엔티티가 데이터를, 위젯이 화면을 묶었다면 기능은 행동을 묶는다. "제품 검색"은 입력 폼, 검색 훅, API, 가공 유틸, 결과 컴포넌트가 다섯 폴더에 흩어져 있었다. 기능 중심이면 product-search 폴더 하나로 간다.
이벤트와 행동은 다르다. 같은 클릭이 맥락에 따라 검색, 필터, 정렬이 된다. 기능은 이벤트를 행동으로 해석하고, 서비스를 부르고, 데이터를 가공해 보여 주는 한 바퀴 전체를 책임진다.
원칙으로 다시 읽기
FSD를 규칙으로만 적용하면 "entities냐 features냐" 분류에 시간을 쓰고, 팀을 설득하기 어렵고, 오히려 파편화가 늘어나기 쉽다. FSD의 레이어는 소프트웨어 공학 원칙을 폴더 이름으로 옮긴 것으로 읽을 때 쓸모가 있다.
| 원칙 | FSD에서 |
|---|---|
| 관심사의 분리 | entities = 데이터, widgets = 화면 구성, features = 행동 흐름 |
| 단일 책임 원칙 | 데이터 구조가 바뀌어도 화면 레이아웃까지 흔들리지 않게 |
| 의존 방향 | pages → widgets → features → entities → shared. 아래일수록 안정적·범용적, 위일수록 구체적·자주 바뀜 |
따질 것은 "어느 폴더냐"보다 "이 코드는 어떤 관심사를 다루고, 무엇을 책임지고, 무엇에 의존해야 하나"다.
프로젝트 시기·성격에 맞게 변용하기
프론트엔드는 데이터 → 화면 → 사용자 → 행동 → 데이터로 순환하고, 프로젝트 시기마다 이 중 어디에 관심이 쏠리는지가 다르다. 구조를 바꿀 신호는 두 질문으로 잡는다. 지금 무엇에 관심을 두고 있나? 무엇이 변하고 무엇이 변하지 않나?
| 시기 | 주된 관심 | 구조 |
|---|---|---|
| 초창기 | 화면 구현 | 역할별 + 페이지별, 재사용은 shared |
| 조금 커짐 | 서로 독립된 페이지들 | 페이지 안에 그 페이지의 상태·API를 함께 둔다. 공통 폴더 비대화를 막는다 |
| 릴리스 막바지 | API, 데이터 흐름 | 화면과 도메인이 엉킨 만능 컴포넌트를 도메인 데이터 중심(api·model·ui)으로 정리 |
| 릴리스 이후 | 기능 추가·제거 | 기능 단위로 모은다. 흩어져 있으면 커밋 범위가 넓어지고 기능을 안전하게 걷어 내기 어렵다 |
| 프로젝트 성격 | 어울리는 구조 |
|---|---|
| 관리자 콘솔 (엔티티 CRUD 중심) | entities 위주의 얕은 계층 |
| 애플리케이션형 (페이지가 적고, 기능을 오가며 쓰고, 엔티티를 앱 전체가 공유) | FSD에 가까운 계층 구조. features·widgets가 많다 |
| 대형 웹사이트형 (페이지가 많고, 메뉴마다 미니 사이트처럼 독립) | 메뉴·페이지 중심 상위 구조 + 그 안에서 엔티티·화면·기능 분리. 일반 FSD는 오히려 파편화를 키운다 |
대형 웹사이트형에 맞는 한 가지 예는 메뉴별로 서비스처럼 나누고, 페이지 안에서만 FSD식으로 나누는 이중 구조다. 폴더 이름도 알파벳 순서가 계층 순서가 되게 고른다(base → entities → features → modules → pages).
/apps
/1-intro 주요 메뉴
/app 라우트·레이아웃
/entities 메뉴 공통 데이터
/pages
/cases
/base 페이지 공통 UI·라이브러리
/entities 페이지 엔티티
/features 내부 기능
/modules 화면 구성 모듈 (widgets 대신)
PageCase.tsx
/news
/2-product
/3-support정리
- 폴더 구조의 목표는 팀의 멘탈 모델과 코드 구조를 일치시키는 것이다.
- 작게는 역할 기반으로 시작하고, 커지면 역할 안을 UI/도메인으로 나누고, 페이지를 독립시킨다. 대부분의 중규모 프로젝트는 여기서 충분하다.
- 같은 화면이라도 도메인이 다르면 나눈다. 도메인 컴포넌트에 재사용성을 억지로 요구하지 않는다.
- 더 커지면 데이터(엔티티)·화면(위젯)·행동(기능) 기준으로 응집한다. 의존은 위에서 아래로만.
- FSD는 따를 규칙이 아니라 구조를 보는 렌즈다. 원칙을 이해하고 프로젝트 성격에 맞게 고친다.
관련 노트: Feature-Sliced Design, 프론트엔드와 SOLID 원칙, 프론트엔드에서 의존성 관리하기, 합성: 상속 대신 Composition, 프론트엔드에서 관심사의 분리, Atomic Design과 컴포넌트 폴더 구조, 컴포넌트를 분리하는 기준과 방법