대규모 앱 아키텍처
작은 프로젝트에서는 아무 구조나 동작하지만, 앱이 커지면 아키텍처가 생산성과 유지보수성을 결정합니다.
면접에서 "대규모 Vue 프로젝트를 어떻게 구조화하나요?"라고 물으면, 기능 기반 모듈 구조 와 관심사 분리 원칙 을 설명할 수 있어야 합니다.
프로젝트 구조 — 기능 기반 (Feature-Based)
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
레이어 분리
┌─────────────────────────────────┐
│ Views (페이지) │ ← 라우트에 매핑
├─────────────────────────────────┤
│ Components (UI 조각) │ ← Props/Emit으로 소통
├─────────────────────────────────┤
│ Composables (비즈니스 로직) │ ← 상태 + 로직 캡슐화
├─────────────────────────────────┤
│ Stores (전역 상태) │ ← 공유 상태
├─────────────────────────────────┤
│ API (HTTP 통신) │ ← 서버 통신 추상화
├─────────────────────────────────┤
│ Types (타입 정의) │ ← 계약(Contract)
└─────────────────────────────────┘
컴포넌트 설계 원칙
1. 스마트 vs 멍청한 컴포넌트
- Container (Smart): 데이터 가져오기, 상태 관리 → views/
- Presentational (Dumb): props만 받아서 표시 → components/
2. 단일 책임 원칙
- 하나의 컴포넌트는 하나의 역할
- 200줄 넘어가면 분리 고려
3. 네이밍 컨벤션
- Base 컴포넌트: BaseButton, BaseInput (앱 전체 공통)
- 기능 컴포넌트: UserCard, ProductList (도메인 특화)
- 단일 인스턴스: TheHeader, TheSidebar (한 번만 사용)
상태 관리 전략
// 상태의 종류에 따라 적절한 도구 선택
// 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 플러그인
// 테마, 사용자 설정 등
라우터 모듈화
// 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 레이어로 책임을 나눕니다. 상태는 범위에 따라 적절한 도구를 선택하고, 라우터와 스토어를 모듈화하여 확장성을 확보합니다.
댓글 로딩 중...