프로젝트 구조는 애플리케이션의 확장성, 유지보수성, 그리고 개발 생산성에 직접적인 영향을 미치는 중요한 아키텍처 결정입니다. Next.js와 React 프로젝트에서 가장 많이 채택되는 두 가지 접근 방식인 Feature-based와 Layer-based 구조를 심층 비교하고, 각각의 장단점과 실제 구현 방법을 살펴보겠습니다.
Layer-based Structure: 기술적 관심사 기반 구조
Layer-based(계층 기반) 구조는 애플리케이션을 기술적 역할에 따라 분리하는 전통적인 접근 방식입니다. MVC 패턴과 유사하게, 각 파일 타입별로 디렉토리를 구성합니다.
Layer-based 구조 예시
src/
├── components/
│ ├── Button.tsx
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── Modal.tsx
├── pages/
│ ├── index.tsx
│ ├── products.tsx
│ └── checkout.tsx
├── services/
│ ├── api.ts
│ ├── auth.ts
│ └── payment.ts
├── hooks/
│ ├── useAuth.ts
│ ├── useCart.ts
│ └── useProduct.ts
├── utils/
│ ├── formatters.ts
│ └── validators.ts
├── styles/
│ ├── globals.css
│ └── components.css
└── types/
├── user.ts
└── product.ts
Layer-based 구조의 장점
1. 직관적인 초기 이해도 새로운 개발자가 프로젝트에 참여할 때, 각 폴더의 역할이 명확하여 학습 곡선이 낮습니다. components 폴더에는 컴포넌트가, services 폴더에는 API 로직이 있다는 것을 즉시 파악할 수 있습니다.
2. 공통 모듈 재사용성 유틸리티 함수나 공통 컴포넌트를 찾기 쉽고, 프로젝트 전반에서 재사용하기 용이합니다.
3. 기술 스택 마이그레이션 용이성 특정 기술 계층만 변경해야 할 때, 해당 폴더만 수정하면 되므로 마이그레이션이 상대적으로 수월합니다.
Layer-based 구조의 단점
1. 기능 개발 시 파일 분산 하나의 기능을 개발하거나 수정할 때 여러 폴더를 오가며 작업해야 합니다. 예를 들어, 제품 상세 페이지 기능을 수정하려면 pages/, components/, hooks/, services/ 등 여러 디렉토리의 파일을 동시에 수정해야 합니다.
2. 도메인 경계 모호성 프로젝트가 성장하면서 각 폴더가 비대해지고, 서로 다른 도메인의 코드가 같은 폴더에 섞여 관리가 어려워집니다.
3. 의존성 관리의 복잡성 계층 간 의존성이 복잡해지면서 순환 참조나 불필요한 결합이 발생하기 쉽습니다.
Feature-based Structure: 도메인 중심 구조
Feature-based(기능 기반) 구조는 비즈니스 도메인이나 기능 단위로 코드를 구성하는 현대적인 접근 방식입니다. 각 기능이 독립적인 모듈로 존재합니다.
Feature-based 구조 예시
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ └── SignupModal.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.ts
│ │ ├── services/
│ │ │ └── authService.ts
│ │ ├── types/
│ │ │ └── auth.types.ts
│ │ └── utils/
│ │ └── tokenManager.ts
│ ├── products/
│ │ ├── components/
│ │ │ ├── ProductCard.tsx
│ │ │ └── ProductList.tsx
│ │ ├── hooks/
│ │ │ └── useProducts.ts
│ │ ├── services/
│ │ │ └── productApi.ts
│ │ └── types/
│ │ └── product.types.ts
│ └── cart/
│ ├── components/
│ │ └── CartDrawer.tsx
│ ├── hooks/
│ │ └── useCart.ts
│ └── store/
│ └── cartStore.ts
├── shared/
│ ├── components/
│ │ ├── Button.tsx
│ │ └── Modal.tsx
│ ├── hooks/
│ │ └── useDebounce.ts
│ └── utils/
│ └── formatters.ts
└── app/
├── layout.tsx
└── page.tsx
Feature-based 구조의 장점
1. 높은 응집도와 낮은 결합도 관련된 코드가 한 곳에 모여 있어 기능 개발과 유지보수가 효율적입니다. 각 기능이 독립적으로 존재하여 다른 기능에 미치는 영향을 최소화합니다.
2. 확장성과 모듈성 새로운 기능을 추가할 때 새로운 feature 폴더를 생성하면 되므로 확장이 용이합니다. 각 기능을 독립적인 패키지로 분리하기도 쉽습니다.
3. 팀 협업 최적화 각 팀이나 개발자가 특정 feature를 담당할 수 있어 병렬 작업이 수월하고, 코드 충돌이 줄어듭니다.
4. 도메인 주도 설계(DDD) 친화적 비즈니스 도메인과 코드 구조가 일치하여 비즈니스 로직을 이해하기 쉽습니다.
Feature-based 구조의 단점
1. 초기 설계 복잡성 프로젝트 초기에 도메인 경계를 명확히 정의해야 하며, 잘못된 경계 설정은 후속 리팩토링을 어렵게 만듭니다.
2. 코드 중복 가능성 각 feature가 독립적이다 보니 유사한 코드가 중복될 수 있으며, shared 폴더 관리가 중요해집니다.
3. Cross-cutting Concerns 처리 인증, 로깅, 에러 처리 등 여러 기능에 걸친 관심사를 처리하는 방법이 복잡할 수 있습니다.
실제 구현 예시와 비교
Next.js App Router에서의 구현
Layer-based 접근:
// app/products/[id]/page.tsx
import { ProductDetail } from '@/components/ProductDetail';
import { getProduct } from '@/services/products';
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductDetail product={product} />;
}
// components/ProductDetail.tsx
import { useCart } from '@/hooks/useCart';
import { formatPrice } from '@/utils/formatters';
export function ProductDetail({ product }) {
const { addToCart } = useCart();
// Component implementation
}
Feature-based 접근:
// app/products/[id]/page.tsx
import { ProductDetailPage } from '@/features/products';
export default function Page({ params }) {
return <ProductDetailPage productId={params.id} />;
}
// features/products/pages/ProductDetailPage.tsx
import { useProduct } from '../hooks/useProduct';
import { ProductInfo } from '../components/ProductInfo';
import { AddToCartButton } from '../../cart/components/AddToCartButton';
export function ProductDetailPage({ productId }) {
const { product, loading } = useProduct(productId);
// Page implementation
}
하이브리드 접근: 두 구조의 장점 결합
많은 실제 프로젝트에서는 두 접근 방식을 결합한 하이브리드 구조를 채택합니다:
src/
├── features/ # 도메인별 기능 모듈
│ ├── auth/
│ ├── products/
│ └── checkout/
├── components/ # 공통 UI 컴포넌트
│ ├── ui/
│ └── layouts/
├── lib/ # 공통 유틸리티와 설정
│ ├── api/
│ ├── constants/
│ └── utils/
├── hooks/ # 공통 커스텀 훅
└── styles/ # 전역 스타일
이 접근 방식은 기능별 모듈화의 이점을 유지하면서도 공통 리소스를 효율적으로 관리할 수 있게 합니다.
선택 기준과 권장사항
Layer-based를 선택해야 할 때:
- 소규모 프로젝트나 프로토타입
- 도메인 경계가 명확하지 않은 초기 단계
- 팀원들이 전통적인 MVC 패턴에 익숙한 경우
- 빠른 개발 속도가 중요한 MVP 프로젝트
Feature-based를 선택해야 할 때:
- 중대규모 엔터프라이즈 애플리케이션
- 명확한 비즈니스 도메인이 존재하는 프로젝트
- 마이크로프론트엔드로 전환 가능성이 있는 경우
- 여러 팀이 병렬로 작업하는 대규모 프로젝트
마이그레이션 전략
Layer-based에서 Feature-based로 점진적 마이그레이션:
// 1단계: 새로운 기능을 feature 폴더에 구현
// 2단계: 기존 코드를 점진적으로 이동
// 3단계: 공통 코드를 shared로 추출
// 마이그레이션 예시
// Before (Layer-based)
components/ProductCard.tsx
hooks/useProduct.tsx
services/productApi.ts
// After (Feature-based)
features/products/
components/ProductCard.tsx
hooks/useProduct.tsx
services/productApi.ts
성능과 번들 사이즈 고려사항
Feature-based 구조는 코드 스플리팅과 lazy loading에 유리합니다:
// 동적 임포트를 통한 feature 레벨 코드 스플리팅
const ProductFeature = lazy(() =>
import('@/features/products').then(module => ({
default: module.ProductFeature
}))
);
// Next.js에서의 자동 코드 스플리팅
// app/products/page.tsx
import { ProductsFeature } from '@/features/products';
// Next.js가 자동으로 별도 청크로 분리
결론
Feature-based와 Layer-based 구조는 각각의 장단점이 명확하며, 프로젝트의 규모, 팀 구성, 비즈니스 요구사항에 따라 적절한 선택이 달라집니다.
소규모 프로젝트나 빠른 프로토타이핑이 필요한 경우 Layer-based 구조가 효율적이지만, 프로젝트가 성장하고 복잡도가 증가하면 Feature-based 구조로의 전환을 고려해야 합니다.
현실적으로는 두 접근 방식의 장점을 결합한 하이브리드 구조가 가장 실용적인 선택이 될 수 있으며, 프로젝트의 진화에 따라 구조를 점진적으로 개선해 나가는 것이 중요합니다.