Vue.js 아키텍처 설계 가이드
아키텍처 그림 설명
[Route / Page]
|
v
[Feature Layer]
Product / Checkout / Account
|
+---+--------------------+
| |
v v
[Composable] [UI Components]
| |
v v
[API Client / Server State] [Base UI / Design Tokens]
|
v
[Backend / BFF]
이 구조의 핵심은 페이지가 모든 책임을 가지지 않도록 중간에 Feature Layer를 두는 것입니다. Vue에서는 composable이 로직 조립 지점이 되고, UI 컴포넌트는 표현 책임에 집중하며, API 계층은 서버 상태 접근 규칙을 모읍니다. 결국 유지보수성은 컴포넌트 개수보다 이 경계가 얼마나 분명한지에서 결정됩니다.
Vue 아키텍처를 볼 때의 기본 축
좋은 Vue 아키텍처는 보통 다섯 가지 축에서 균형을 잡습니다.
- 화면과 도메인 로직을 얼마나 분리하는가
- 로컬 상태와 전역 상태를 어떻게 구분하는가
- API 요청과 서버 상태를 어떤 경계로 관리하는가
- 재사용 가능한 UI와 화면 전용 UI를 어떻게 나누는가
- 팀이 구조를 쉽게 읽고 확장할 수 있는가
Vue는 유연해서 여러 스타일을 허용하지만, 이 다섯 축을 초기에 정리하지 않으면 유연성이 곧 일관성 부족으로 바뀌기 쉽습니다.
Vue 프로젝트를 구성하는 대표 레이어
실무에서는 보통 다음 레이어로 나누면 읽기 쉽습니다.
pages: 라우트에 직접 매핑되는 화면 단위features: 특정 도메인 기능 단위 묶음components: 공통 UI 컴포넌트stores: 공유 상태 관리composables: 재사용 가능한 상태-행동 로직services또는api: 외부 API 호출과 데이터 접근models또는types: 도메인 타입과 변환 규칙
중요한 점은 폴더 이름이 아니라 의존 방향입니다. pages는 features와 components를 사용하고, features는 composables와 services를 사용하되, 공통 계층이 화면 계층을 거꾸로 참조하지 않게 유지하는 편이 좋습니다.
컴포넌트는 UI 단위가 아니라 책임 단위로 나눈다
Vue를 쓰다 보면 컴포넌트를 너무 작은 단위로 쪼개거나, 반대로 페이지 안에 너무 많은 로직을 밀어 넣기 쉽습니다. 좋은 기준은 “이 컴포넌트가 자신의 책임을 한 문장으로 설명할 수 있는가”입니다.
예를 들면 다음과 같이 구분할 수 있습니다.
UserCard: 사용자 정보 표시라는 명확한 UI 책임UserListPage: 검색, 필터, 목록 조회를 조합하는 화면 책임useUserFilter: 필터 상태와 파생 조건 계산 책임
이렇게 나누면 Vue 컴포넌트는 화면 표현에 집중하고, 재사용 가능한 조합 로직은 composable로 빠질 수 있습니다.
상태는 범위에 따라 계층화해야 한다
Vue 프로젝트가 복잡해지는 가장 흔한 이유는 상태 범위를 섞어 쓰기 때문입니다. 상태는 최소 세 가지로 나눠 보는 편이 좋습니다.
- 로컬 UI 상태: 모달 열림, 입력창 값, 탭 선택 같은 화면 전용 상태
- 공유 클라이언트 상태: 인증 정보, 사용자 설정, 장바구니 같은 여러 화면 공통 상태
- 서버 상태: API에서 받아오고 다시 동기화해야 하는 데이터
로컬 UI 상태는 컴포넌트나 가까운 상위 컴포넌트에 두고, 공유 클라이언트 상태는 Pinia 같은 store로, 서버 상태는 캐시와 재요청 전략을 포함한 별도 데이터 계층으로 두는 것이 좋습니다. 모든 것을 store에 몰아 넣는 순간 구조는 빠르게 흐려집니다.
Composition API는 구조를 더 잘 드러내는 도구
Composition API의 장점은 단순히 setup() 문법이 아니라, 관심사를 함수 단위로 분리할 수 있다는 점입니다. 예를 들면 useAuth, usePagination, useInfiniteScroll, useSearchFilter 같은 composable은 화면 여러 군데에서 같은 패턴을 반복하지 않게 만들어 줍니다.
하지만 모든 로직을 composable로 빼는 것도 좋은 구조는 아닙니다. composable은 재사용성과 책임이 명확할 때만 의미가 있습니다. 아직 한 화면에서만 쓰이고 맥락이 강한 로직이라면 페이지 안에 두는 편이 더 읽기 쉬울 수 있습니다.
라우팅 구조는 정보 구조를 반영해야 한다
Vue Router는 단순 페이지 이동 도구가 아닙니다. URL이 곧 정보 구조가 되기 때문에, 라우트 설계는 프런트엔드 아키텍처의 일부입니다.
좋은 라우트는 다음을 만족합니다.
- URL만 봐도 사용자 흐름이 이해된다
- 중첩 라우트가 레이아웃 경계를 설명한다
- 인증과 권한 정책을 특정 페이지에 일관되게 적용할 수 있다
- lazy loading 전략과도 잘 맞는다
예를 들어 관리자 화면이라면 /admin/users, /admin/audit-logs, /admin/settings처럼 기능과 도메인이 URL에도 드러나는 편이 유지보수에 유리합니다.
API 계층은 컴포넌트에서 분리할수록 좋다
초기에는 페이지 안에서 바로 fetch()를 호출해도 문제 없어 보이지만, 시간이 갈수록 에러 처리, 인증 헤더, 응답 변환, 재시도 정책, 로깅 요구가 생깁니다. 이때 컴포넌트 안에 API 호출이 흩어져 있으면 수정 비용이 커집니다.
그래서 보통은 다음처럼 나눕니다.
api client: 공통 HTTP 설정, 인터셉터, 에러 매핑service: 도메인별 API 함수mapper: 서버 응답을 UI 모델로 변환
이 구조를 두면 백엔드 응답이 조금 바뀌어도 전체 UI가 흔들리지 않습니다.
공통 UI와 도메인 UI는 섞지 않는다
디자인 시스템 성격의 버튼, 인풋, 모달, 배지 컴포넌트와 특정 기능에서만 의미가 있는 카드, 필터 패널, 정렬 탭은 분리하는 편이 좋습니다. 전자는 범용 UI, 후자는 도메인 UI입니다.
범용 UI가 도메인 요구를 너무 많이 품기 시작하면 재사용성이 무너지고, 반대로 도메인 UI가 너무 일반화되면 읽기 어려워집니다. Vue 컴포넌트 라이브러리 구조는 이 균형을 잘 잡아야 합니다.
성능은 나중 최적화가 아니라 구조 문제이기도 하다
Vue는 기본 성능이 좋은 편이지만, 구조가 나쁘면 성능 문제도 빨리 드러납니다.
- 불필요하게 거대한 store를 만들면 리렌더링 범위가 커진다
- 깊은 props 전달과 중복 계산이 많으면 화면이 무거워진다
- 라우트 단위 코드 분할이 없으면 초기 번들이 커진다
- 서버 상태를 전역 상태처럼 오래 들고 있으면 stale 데이터가 많아진다
성능은 결국 상태 범위, 데이터 흐름, 코드 분할 전략과 연결되어 있습니다.
팀 협업 관점에서 중요한 아키텍처 기준
혼자 개발할 때는 머릿속으로 유지되던 규칙도 팀이 커지면 명시해야 합니다. Vue 프로젝트에서는 특히 다음 기준이 있으면 협업이 쉬워집니다.
- 페이지, feature, composable, store의 역할 문서화
- API 호출 위치와 에러 처리 규칙 통일
- 공통 UI와 도메인 UI의 기준 정의
- 폴더 구조와 import 경계 합의
- 테스트 우선순위와 대상 명확화
구조는 코드보다 먼저 팀의 공통 언어가 되어야 합니다.
언제 Vue 아키텍처를 다시 정비해야 하나
다음 신호가 보이면 구조를 다시 봐야 합니다.
- 페이지마다 같은 API 호출과 에러 처리 코드가 반복된다
- Pinia store 하나가 너무 많은 도메인을 품고 있다
- 비슷한 UI가 여러 버전으로 존재한다
- composable 이름은 많지만 책임이 애매하다
- 새 화면 추가 시 어디에 파일을 둬야 할지 자주 헷갈린다
이때는 기능 추가만 계속하기보다 경계를 다시 정리하는 것이 전체 속도를 높입니다.
마무리
Vue.js 아키텍처의 핵심은 프레임워크 기능을 얼마나 많이 아느냐가 아니라, 상태와 책임과 의존 방향을 얼마나 명확히 나누느냐입니다. 컴포넌트, composable, store, API 계층, 라우터가 각자 왜 존재하는지 분명해지면 Vue 프로젝트는 훨씬 더 오래 버티는 시스템이 됩니다.
운영 환경에서 어려워지는 지점
- Vue 아키텍처는 경계가 흐려지고 모든 기능이 서로의 내부로 들어가기 시작하면 빠르게 무너진다.
- 대부분의 유지보수 문제는 JSX나 템플릿 때문이 아니라 데이터, 부작용, 조합의 소유권이 불명확하기 때문에 생긴다.
- Vue 코드베이스는 컴포넌트, composable, 라우트, store 경계가 명시적일 때 강해진다.
중요한 아키텍처 결정
- 파일 타입 편의보다 feature와 책임 기준으로 먼저 조직한다.
- 비즈니스 로직은 도메인 모듈 가까이에, UI 조합은 route나 screen 가까이에 둔다.
- 공용 유틸리티, 데이터 접근, 상태 추상화가 어떤 경계를 넘을 수 있는지 정한다.
실무 예시
유지보수 가능한 프런트 구조는 프레임워크 프리미티브보다 소유권을 반영한다.
features/
billing/
catalog/
shared/
ui/
api/
utils/
app/
routes/
providers/
피해야 할 안티패턴
- 도메인 묶음 없는 거대한 컴포넌트 트리를 만드는 것.
- 모든 것이 흘러들어가는 shared 폴더를 키우는 것.
- 전송 계층, 비즈니스 규칙, 표현 포맷팅을 같은 모듈에 섞는 것.
운영 체크리스트
- shared 코드와 feature 코드 사이 의존 방향을 검토한다.
- 컴포넌트와 훅/composable 재사용이 단순 중복 제거가 아니라 의미 있는 추상화인지 본다.
- 공개 모듈 API를 작고 의도적으로 유지한다.
- route 수준 복잡도가 feature 바깥으로 새기 시작하면 리팩터링한다.
최종 판단
Vue 아키텍처는 모듈 경계가 제품 동작을 설명할 때 강하다. 소유권 없는 폴더 정리는 보기 좋은 껍데기에 가깝다.
Continue Reading
다음으로 읽기 좋은 글
Vue + SPA 아키텍처 가이드
Vue 기반 SPA를 빠르게 만드는 방법이 아니라, 오래 운영 가능한 단일 페이지 애플리케이션으로 설계하는 방법을 정리합니다. 라우팅, 상태, 데이터 패칭, 캐싱, 배포와 성능까지 다룹니다.
🖥️ FrontendReact + SPA 아키텍처 가이드
React 기반 SPA를 설계할 때 필요한 화면 경계, 상태 구조, 라우팅, 데이터 계층, 성능 전략을 실무 관점에서 정리합니다.
⚙️ BackendCQRS + Event Sourcing 실전 구현 가이드
CQRS와 Event Sourcing을 도메인 경계와 운영 복잡성의 관점에서 설명하고, Aggregate, Event Store, Projection, Snapshot, 정합성 모델의 선택 기준을 정리합니다.
💬 LanguageI/O 경계에서의 타입 좁히기 전략
언어의 타입 시스템은 경계 안에서 강하지만, 외부 입력 앞에서는 다시 확인이 필요합니다. I/O 경계에서 타입을 좁히는 전략을 정리합니다.
다음 탐색