리액트 프로젝트 구조를 여러 번 개선한 경험 공유
들어가며
개발 프로젝트를 시작할 때 항상 고민하게 되는 것 중 하나는 '프로젝트 구조를 어떻게 설계할 것인가' 입니다. 처음에는 단순한 폴더 구조로 시작하지만 프로젝트가 커질수록 코드의 위치를 찾기 어려워지거나 기능 간 의존성이 복잡해지는 문제를 겪게 됩니다.
저는 약 8개월 동안 리액트 개발 환경을 공부하면서 세 개의 토이 프로젝트를 진행 중인데, 각 프로젝트마다 다른 구조를 적용해 보았습니다.
프로젝트들이 커질수록 구조의 장단점이 보이기 시작했고, 그 과정에서 자연스럽게 아키텍처를 몇 번 변경하게 되었습니다.
이번 글에서는 각 프로젝트를 진행하면서 사용했던 세 가지 구조와 각각의 트레이드오프를 정리해보려고 합니다.
1. 파일 유형 중심 구조
node_modules/
public/
src/
├── routes/
│ └── pages/
├── components/
├── hooks/
├── utils/
├── types/
├── api/
├── providers/
├── tests/
├── assets/
└── lib/첫 번째 프로젝트에서는 코드의 파일 유형에 따라 폴더를 나누는 방식으로 구조를 만들었습니다.
리액트에 익숙하지 않았기 때문에 기능 구현을 우선으로 개발했습니다.
코드가 길어지면 코드의 성격에 따라 components, hooks, lib 같은 폴더를 만들어 분리했습니다.
초기에는 개발 속도를 높이기에 충분히 좋은 구조였습니다. 하지만 프로젝트가 조금만 커졌는데 찾고자 하는 코드가 어디 있는지 파악하기 어려워졌습니다.
또한 새로운 기능을 추가할 때마다 어떤 폴더에 어떤 코드를 넣어야 할지 고민하는 시간이 늘어났습니다.
프로젝트 규모가 작을 때는 문제가 없지만
기능이 늘어나면서 구조가 점점 관리하기 어려워졌습니다.
장점
- 구조가 단순하다
- 작은 프로젝트에서 빠르게 개발할 수 있다
- 러닝 커브가 거의 없다
단점
- 하나의 기능을 수정하려면 여러 폴더를 이동해야 한다
- 관련 코드가 서로 다른 위치에 흩어져 코드 흐름을 파악하기 어렵다
- 기능 단위 리팩토링이 어렵다
2. Feature 기반 구조
파일 유형 중심 구조의 문제를 해결하기 위해 다음 프로젝트에선 Feature 기반 구조를 시도했습니다. Feature 구조는 기능 단위로 코드를 묶어서 관리하는 방식입니다.
node_modules/
public/
e2e/
src/
├── routes/
│ └── pages/
├── features/
│ └── auth/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ ├── types/
│ ├── api/
│ └── lib/
├── components/
├── hooks/
├── utils/
├── types/
├── api/
├── providers/
├── assets/
└── lib/테스트 코드의 경우 첫 프로젝트 구조에선 별도의 tests 폴더에 모아두었지만, 이번엔
- 모듈/컴포넌트 테스트 코드는 테스트 대상과 동일한 폴더에 위치
- e2e 테스트 코드만
e2e폴더에 별도로 위치
하도록 하여 관리했습니다.
이 구조는 bulletproof-react에서 소개된 구조를 참고했습니다.
Feature 단위로 코드가 모이기 때문에 코드 탐색과 기능 수정이 훨씬 편해졌습니다. 또한 shared 성격을 가진 폴더에서는 (ex: src/components, src/hooks, src/types...) features 내부 코드를 import 하지 않도록 규칙을 정했습니다.
예를 들어 다음과 같은 규칙입니다.
shared->featuresimport 금지features->sharedimport 허용
이렇게 하면 의존성 방향이 항상 일정하게 유지되기 때문에 프로젝트 구조를 이해하기 한 층 더 쉬워집니다.
추가로 features 폴더 내부에서는 feature끼리 import 하는 것을 허용했습니다. shared엔 여러 feature에서 공통으로 사용하는 코드만 두고 싶었기 때문입니다. 그리고 배럴 패턴을 사용해 각 feature index.ts 파일에서 어떤 코드가 외부로 export 되는지 관리하여 코드 탐색에 도움이 되도록 했습니다.
// src/features/auth/index.ts
export { LoginForm } from './components/LoginForm';
export { useAuth } from './hooks/useAuth';
export { authApi } from './api/authApi';
export type { authTypes } from './types/authTypes';초기엔 의도대로 코드 중복을 줄일 수 있어서 기능 단위 개발을 하는데 효과적이었습니다. 하지만 프로젝트가 커지면서 feature 간 의존성이 점점 깊어졌고 구조 또한 복잡해졌습니다. 배럴 패턴을 적용해도 코드 파악에 한계가 있었습니다.
또한 코드가 feature 레이어에 과도하게 집중되는 문제도 발생했습니다. 마치 features 폴더가 또다른 앱을 구성하는 듯한 기분도 들었습니다.
기능 중심 개발에는 좋았지만 프로젝트 전체 구조를 통제할 수 있는 규칙이 부족하다는 느낌을 받았습니다. 그래서 더 명확한 구조 규칙을 가진 아키텍처가 필요하다고 생각하게 되었습니다.
장점
- 도메인 단위로 코드가 모인다
- 기능 중심 개발에 적합하다
- 코드 탐색이 쉬워진다
단점
- feature 간 의존성이 생기기 시작하면 구조가 빠르게 복잡해진다
- 특정 feature가 다른 feature의 내부 구현에 의존하게 될 수 있다
- feature 폴더가 사실상 작은 애플리케이션처럼 커질 수 있다
3. 경량 FSD 구조
다음 프로젝트를 진행할 땐 FSD(Feature-Sliced Design) 구조를 참고해 보았습니다. FSD 구조에 대한 설명은 공식 문서를 참고해주세요.
기존 FSD 구조는 이전에 적용했던 구조에 비해 다소 복잡했습니다. 학습 비용이 부담스럽긴 하나 그래도 도메인 중심 구조를 지향한다는 점이 매력적으로 다가왔습니다. 그래서 몇가지 요소를 제거한 경량 FSD 구조를 적용해보기로 했습니다.
제거한 요소
- pages / widgets 레이어
- slice / segment 단위 구조
node_modules/
public/
e2e/
src/
│
├── features/
│ └── auth/
│ ├── ui/
│ ├── model/
│ ├── api/
│ └── lib/
│
├── entities/
│ └── user/
│ ├── ui/
│ ├── model/
│ ├── api/
│ └── lib/
│
└── shared/
├── ui/
├── lib/
├── api/
├── hooks/
├── types/
├── utils/
└── config/경량 FSD 구조의 핵심은 도메인(entity) 중심 레이어 구조입니다.
예를 들어
entities→ user, post 같은 도메인 모델features→ login, signup 같은 사용자 행동shared→ 공용 코드
로 관리합니다.
이렇게 역할을 분리하면 feature에 코드가 과도하게 집중되는 문제를 줄일 수 있습니다.
코드의 의존성은 삭제한 레이어를 제외하곤 기존 FSD와 동일하게 적용하여 명확하게 관리했습니다.
app -> features -> entities -> shared이 구조는 Feature 구조보다 더 명확한 아키텍처 규칙을 제공했습니다. 또한 아키텍처 확장에도 용이합니다. 개발/서비스 중인 앱의 규모가 커지면 삭제했던 pages/ widgets 레이어를 추가하거나 slice/segment 구조를 도입하는 등 점진적으로 아키텍처를 확장할 수 있습니다.
장점
- 레이어 구조가 명확하다
- 의존성 방향을 통제하기 쉽다
- 프로젝트 구조를 예측하기 쉬워진다
- 규모가 커져도 구조가 쉽게 무너지지 않는다
단점
- 초기 설계 비용이 필요하다
- FSD 개념에 대한 이해가 필요하다
앞으로 시도해볼 구조
앞으로는 widgets 레이어만 제외한 FSD 구조도 적용해볼 예정입니다.
FSD는 확장성이 좋은 구조지만 모든 레이어를 그대로 도입하면 프로젝트 규모에 비해 구조가 과도하게 복잡해질 수도 있습니다.
그래서 실제 프로젝트에서는 경량화된 형태로 시작하고 점진적으로 확장하는 방식이 더 현실적이라고 생각하고 있습니다.
정리
8개월 동안 프로젝트 구조를 세 번 변경하면서 다음과 같은 흐름을 경험했습니다.
파일 유형 중심 구조 → Feature 기반 구조 → 레이어 기반 구조 (FSD)
프로젝트 규모가 커질수록
- 코드의 위치
- 의존성 방향
- 구조 규칙
이 중요해지기 때문에 레이어 기반 아키텍처가 필요해진다는 점을 경험하게 되었습니다.