작은 프로젝트에서는 아무 구조나 동작하지만, 앱이 커지면 아키텍처가 생산성과 유지보수성을 결정합니다.

면접에서 "대규모 Vue 프로젝트를 어떻게 구조화하나요?"라고 물으면, 기능 기반 모듈 구조 와 관심사 분리 원칙 을 설명할 수 있어야 합니다.


프로젝트 구조 — 기능 기반 (Feature-Based)

PLAINTEXT
src/
├── features/                    # 기능(도메인) 단위 모듈
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.vue
│   │   │   └── RegisterForm.vue
│   │   ├── composables/
│   │   │   └── useAuth.ts
│   │   ├── stores/
│   │   │   └── auth.ts
│   │   ├── api/
│   │   │   └── auth.ts
│   │   ├── types/
│   │   │   └── auth.ts
│   │   └── views/
│   │       ├── LoginView.vue
│   │       └── RegisterView.vue
│   ├── dashboard/
│   │   ├── components/
│   │   ├── composables/
│   │   └── views/
│   └── products/
│       ├── components/
│       ├── composables/
│       ├── stores/
│       └── views/
├── shared/                      # 공유 리소스
│   ├── components/              # 공통 UI 컴포넌트
│   │   ├── BaseButton.vue
│   │   ├── BaseInput.vue
│   │   └── BaseModal.vue
│   ├── composables/             # 공통 composable
│   │   ├── useFetch.ts
│   │   └── useForm.ts
│   ├── utils/                   # 유틸리티 함수
│   ├── types/                   # 공유 타입
│   └── constants/               # 상수
├── layouts/                     # 레이아웃
├── router/                      # 라우터 설정
├── plugins/                     # 플러그인
├── assets/                      # 정적 리소스
├── App.vue
└── main.ts

레이어 분리

PLAINTEXT
┌─────────────────────────────────┐
│        Views (페이지)           │  ← 라우트에 매핑
├─────────────────────────────────┤
│      Components (UI 조각)       │  ← Props/Emit으로 소통
├─────────────────────────────────┤
│    Composables (비즈니스 로직)   │  ← 상태 + 로직 캡슐화
├─────────────────────────────────┤
│      Stores (전역 상태)          │  ← 공유 상태
├─────────────────────────────────┤
│       API (HTTP 통신)           │  ← 서버 통신 추상화
├─────────────────────────────────┤
│     Types (타입 정의)           │  ← 계약(Contract)
└─────────────────────────────────┘

컴포넌트 설계 원칙

PLAINTEXT
1. 스마트 vs 멍청한 컴포넌트
   - Container (Smart): 데이터 가져오기, 상태 관리 → views/
   - Presentational (Dumb): props만 받아서 표시 → components/

2. 단일 책임 원칙
   - 하나의 컴포넌트는 하나의 역할
   - 200줄 넘어가면 분리 고려

3. 네이밍 컨벤션
   - Base 컴포넌트: BaseButton, BaseInput (앱 전체 공통)
   - 기능 컴포넌트: UserCard, ProductList (도메인 특화)
   - 단일 인스턴스: TheHeader, TheSidebar (한 번만 사용)

상태 관리 전략

TYPESCRIPT
// 상태의 종류에 따라 적절한 도구 선택

// 1. 컴포넌트 로컬 상태 — ref/reactive
const isOpen = ref(false)  // 모달 열림/닫힘

// 2. 부모-자식 공유 — Props/Emit
// 깊이 1-2단계까지

// 3. 형제/먼 컴포넌트 공유 — Pinia Store
const useCartStore = defineStore('cart', () => {
  const items = ref([])
  return { items }
})

// 4. 트리 범위 공유 — Provide/Inject
// 특정 서브트리에서만 필요한 데이터

// 5. 전역 + 영속 — Pinia + localStorage 플러그인
// 테마, 사용자 설정 등

라우터 모듈화

TYPESCRIPT
// router/modules/auth.ts
export const authRoutes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('@/features/auth/views/LoginView.vue'),
    meta: { requiresGuest: true }
  },
  {
    path: '/register',
    component: () => import('@/features/auth/views/RegisterView.vue')
  }
]

// router/index.ts
import { authRoutes } from './modules/auth'
import { dashboardRoutes } from './modules/dashboard'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    ...authRoutes,
    ...dashboardRoutes,
    { path: '/:pathMatch(.*)*', component: () => import('@/views/NotFound.vue') }
  ]
})

면접 팁

  • **기능 기반 구조 vs 타입 기반 구조 **: 타입 기반(components/, views/, stores/)은 작은 프로젝트에 적합하고, 기능 기반은 대규모에서 모듈 간 의존성을 줄여줍니다
  • "모든 상태를 Pinia에 넣지 않는다"는 원칙을 설명하세요
  • 컴포넌트 네이밍 컨벤션(Base, The, 기능 접두사)을 알고 있으면 팀 협업 경험을 보여줄 수 있습니다

요약

대규모 Vue 앱은 기능(Feature) 기반 모듈 구조로 관심사를 분리하고, Views → Components → Composables → Stores → API 레이어로 책임을 나눕니다. 상태는 범위에 따라 적절한 도구를 선택하고, 라우터와 스토어를 모듈화하여 확장성을 확보합니다.

댓글 로딩 중...