[Next.js] Feature-based vs Layer-based 프로젝트 구조 비교




프로젝트 구조는 애플리케이션의 확장성, 유지보수성, 그리고 개발 생산성에 직접적인 영향을 미치는 중요한 아키텍처 결정입니다. 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 구조로의 전환을 고려해야 합니다.

현실적으로는 두 접근 방식의 장점을 결합한 하이브리드 구조가 가장 실용적인 선택이 될 수 있으며, 프로젝트의 진화에 따라 구조를 점진적으로 개선해 나가는 것이 중요합니다.




댓글 남기기