🔽 목표
늘 '팀 프로젝트를 잘하려면 어떻게 구조를 설계/분리해야할까?'라는 궁금증이 생겼다.
'잘 분리'하기 위해서는 재사용성이 좋게 역할과 책임을 분리해야하고, 다른사람이 이해하기 쉬운 코드가 가장 중요하다고 생각했다.
그래서 다른 개발자가 협업함에 있어서 가독성이 좋고, 역할과 책임이 잘 분리된 구조를 설계해보는걸 목표로 진행중이던 프로젝트를 수정해봤다.
🔽 현재 구조적 문제 상황
아래는 기존 프로젝트 구조로 페이지 단위로 분리되었다.
react-shopping-cart
├─ src
│ ├─ apis 👈 api
│ │ ├─ cartItems
│ │ └─ httpClient.ts
│ ├─ pages 👈 페이지별
│ │ ├─ cart
│ │ │ ├─ CartContent
│ │ │ │ ├─ CartList
│ │ │ │ ├─ EmptyCartContainer
│ │ │ │ └─ PriceContainer
│ │ │ └─ hooks
│ │ └─ order
│ ├─ shared 👈 프로젝트 공용 로직
│ │ ├─ components
│ │ ├─ config
│ │ └─ hooks
요구사항이 크지 않았을 때는 하나의 페이지에서 ui와 상태가 관리되었지만, 프로젝트의 규모가 커지고 요구사항이 많아짐에 따라 어떻게 하면 중복을 줄이고, 각각의 도메인에 맞게 프로젝트 구조를 분리하고 설계할 수 있을지 궁금해졌다.
🔽 여러 대안 검토 및 선택 근거
1. 페이지별 분리
- 설명 :
- 컴포넌트, 페이지, 서비스 등 기술적 타입별로 폴더를 구분하는 전통적인 구조.
- 장점 :
- 단순하고 빠르게 분리 가능.
- 소규모 프로젝트, MVP, 단기 프로젝트에 적합.
- 단점 :
- 요구사항이 커지고 페이지가 늘어나면 중복코드, 결합도 증가.
- 상태 및 비즈니스 로직의 위치가 불분명해 유지보수와 협업이 어려워짐.
- 기능 단위로 재사용 or 확장하기 힘듦.
2. Atomic Design
- 설명 :
- UI를 원자(Atom) → 분자(Molecule) → 유기체(Organism) → 템플릿 → 페이지로 계층화하는 구조.
- 장점 :
- UI 컴포넌트 재사용성과 일관성 극대화
- 디자인 시스템, UI중심 프로젝트에 적합
- 단점
- UI레벨에 집중되어 있어 비즈니스 로직, 상태 관리, 도메인 분리에 한계
- 프로젝트가 커질수록 폴더 구조가 복잡해지고, 실제 기능 단위 협업에는 제약
3. Clean Architecture
- 설명 :
- 도메인, 애플리케이션, 프레젠테이션 등 계층별로 명확히 분리하는 백엔드 지향 구조.
- 장점 :
- 복잡한 비즈니스 규칙, 도메인 중심 개발에 적합.
- 테스트하기 용이하고, 명확한 책임 분리가 가능.
- 단점 :
- 프론트엔드에서는 오버엔지니어링일 수 있음.
- UI/UX 중심 프로젝트에서는 개발 속도가 저하되고 러닝커브가 존재함.
4. Feature-Sliced Design (FSD)
- 설명 :
- 기능(Feature) 단위로 폴더를 분리하고, 각 기능 내에 UI, 상태, API, 도메인 로직을 캡슐화.
- 장점 :
- 대규모, 협업 프로젝트에서 확장성과 유지보수성 뛰어남.
- 명확한 역할·책임 분리, 중복 최소화, 팀 단위 병렬 개발 용이.
- 코드베이스가 커져도 구조적 일관성 유지, 신규 기능 추가가 쉬움.
- 의존성, 캡슐화, 계층적 흐름(하위 레이어는 상위 레이어를 모름)을 통한 견고한 설계.
- 단점 :
- 초기 설계, 도입시 러닝커브 존재.
- 소규모, 단기 프로젝트에는 과할 수 있음.
위의 4가지 선택지 비교를 정리하면 아래와 같다.
| 설계 구조 | 장점 | 단점 |
| 페이지별 분리 | 단순, 빠른 시작, 소규모에 적합 | 중복, 결합도↑, 유지보수 어려움, 재사용성↓ |
| Atomic Design | UI 재사용, 일관성, 디자인 시스템에 적합 | 비즈니스 로직/상태 분리에 한계, 대형 프로젝트엔 복잡 |
| Clean Architecture | 도메인 중심, 테스트 용이, 책임 분리 | 프론트엔드엔 오버엔지니어링, 러닝커브, UI/UX엔 비효율적 |
| Feature-Sliced Design (FSD) |
확장성, 유지보수성↑, 역할·책임 명확, 협업/테스트/온보딩 용이 | 초기 러닝커브, 소규모엔 과할 수 있음 |
🔽 FSD(Feature-Sliced Design) 선택 이유
- 요구사항이 커지고, 여러 페이지에서 공통 UI/상태가 중복되는 문제를 해결하고 싶었음.
- 팀 프로젝트를 대비해 각 기능 단위로 역할과 책임이 명확하고, 협업에 유리한 구조를 공부해보고 싶었음.
- 기존 구조의 한계(중복, 결합도, 유지보수성 저하)를 극복하고, 코드 재사용성과 확장성을 높이고자 했음.
- FSD는 기능별로 캡슐화와 계층적 분리를 제공해 실무에서의 유지보수·협업·테스트·온보딩 등 모든 측면에서 장점이 명확했음.
- 실제 도입 후, 코드 중복이 줄고 신규 기능 추가·수정 시 영향 범위가 명확해졌음.
🔽 FSD 아키텍쳐 적용
src/
├── shared/ # 공유 유틸리티, 타입, 상수
├── entities/ # 비즈니스 엔티티 (cart, coupon)
├── features/ # 비즈니스 로직 (order, cart, coupon)
├── widget/ # 독립적인 위젯 컴포넌트
├── pages/ # 페이지 컴포넌트
└── app/ # 앱의 초기화, 전역 설정
- shared → entities → features → widgets → pages→ app 순으로 의존성이 흐르며, 상위 계층은 하위 계층에만 의존하도록 했어요.
각 계층은 아래 같이 각자의 목적에 맞게 분류했다.
- shared
: 재사용 가능한 컴포넌트, utils, 비즈니스 로직에 종속되지 않음.
- entities
: 도메인 데이터와 그자체의 로직으로 '무엇을'에 해당. 상태 관리, 타입 정의, api 통신이 해당함.
- features
: 여러 엔티티를 조합하여 사용자의 요구(비즈니스 플로우)를 구현하여 '어떻게'에 해당. 사용자 시나리오에 맞도록 entities를 조합하여 활용한 ui 컴포넌트, 상태관리, api 호출 등이 여기에 해당
- widget
: 페이지를 구성하는 큰 UI 블록으로 page에서 사용할수 있을 단위. entities와 feature를 조합해서 만든 의미있는 블록 조합.
- page
: widget을 조합해서 만든 하나의 페이지
🔽 적용 효과
예시로 ui를 그리는 컴포넌트의 경우에도 한번더 추상화하여서 shared 와 widget으로 분리했다.
UI 공통 로직을 분리하여 PriceInfoContainer는 순수하게 UI 렌더링만 담당하고, 도메인별 비즈니스 로직이 포함된 경우 아래와 같이 분리했어요.
// 1. UI 공통 로직 (@shared/components/PriceInfoContainer)
const PriceInfoContainer = ({ children, freeDeliveryLimit }) => {
return (
<Container>
<Description description={`무료 배송 안내...`} />
{children}
</Container>
);
};
// 2. 도메인별 컨테이너
// Cart (@widget/cart/PriceContainer)
const CartPriceContainer = ({ orderPrice, deliveryFee, orderTotalPrice }) => {
const orderItems = [
{ title: '주문 금액', price: orderPrice },
{ title: '배송비', price: deliveryFee },
];
return (
<PriceInfoContainer freeDeliveryLimit={FREE_DELIVERY_LIMIT}>
<PriceBox items={orderItems} />
</PriceInfoContainer>
);
};
// Order (@widget/order/PriceContainer)
const OrderPriceContainer = ({ orderPrice, couponDiscountPrice, ... }) => {
const orderItems = [
{ title: '주문 금액', price: orderPrice },
{ title: '쿠폰 할인 금액', price: -couponDiscountPrice },
];
return (
<PriceInfoContainer freeDeliveryLimit={FREE_DELIVERY_LIMIT}>
<PriceBox items={orderItems} />
</PriceInfoContainer>
);
};'React' 카테고리의 다른 글
| 선언적으로 Modal 시스템 구현하기 (0) | 2025.10.20 |
|---|---|
| useQRCode를 만들어보자 (0) | 2025.09.30 |
| requestAnimationFrame(raF), 알고 사용하시나요? (1) | 2025.09.15 |
| [UX 개선] 낙관적 업데이트(Optimistic Update) (0) | 2025.06.03 |
| [TanStack Query] 서버 상태 관리 라이브러리 (0) | 2025.06.02 |