Vue 완벽 가이드 | Vue 3·Composition API·반응형 내부·컴파일러·프로덕션 패턴
이 글의 핵심
Vue 3는 컴포지션 API(Composition API)를 중심으로 타입스크립트와의 궁합을 강화했고, 런타임은 프록시(Proxy) 기반 반응형과 가상 DOM(Virtual DOM) 패치, 템플릿 컴파일러 최적화가 맞물려 동작합니다. 이 글에서는 API 사용법뿐 아니라, 왜 그렇게 설계되었는지와 내부 구현 관점에서의 동작을 함께 다룹니다.
1. Vue 3를 한 장으로
Vue는 선언적 UI와 반응형 데이터를 제공하는 프론트엔드 프레임워크입니다. Vue 3에서는 다음이 특히 중요합니다.
- Composition API: 로직을 함수 단위로 묶어 재사용(
composables)하고,setup/script setup에서 상태와 생명주기를 표현합니다. - 반응형 시스템: 객체는
Proxy로, 원시값·참조 래핑은ref로 추적합니다. - 렌더링: 컴파일러가 템플릿을 렌더 함수로 바꾸고, 런타임이 VNode 트리를 비교·갱신(
patch)합니다. - 생태계: 공식 라우터(Vue Router), 상태 관리(Pinia), 빌드 도구(Vite)와 잘 맞습니다.
Options API도 지원하지만, 새 프로젝트는 Composition API + <script setup>을 권장하는 흐름이 일반적입니다.
2. ref·reactive·computed·watch
2.1 ref (단일 값·래핑)
ref는 내부적으로 반응형 참조 객체를 만들고, 템플릿에서는 자동으로 unwrap됩니다. 스크립트에서는 .value로 접근합니다.
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<p>Count: {{ count }}</p>
<button type="button" @click="increment">+1</button>
</template>
2.2 reactive (객체 전용)
객체 전체를 반응형으로 만듭니다. 깊은 반응형이 기본이며, 구조 분해 시 반응성을 잃을 수 있어 toRefs/storeToRefs 등과 함께 쓰는 패턴이 문서화되어 있습니다.
<script setup lang="ts">
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: { name: 'Ada', email: '[email protected]' },
});
function increment() {
state.count++;
}
</script>
<template>
<p>{{ state.count }} — {{ state.user.name }}</p>
<button type="button" @click="increment">+1</button>
</template>
2.3 computed
읽기 전용 또는 getter/setter 형태의 파생 상태를 정의합니다. 내부적으로 의존성이 추적되며, 캐시된 값은 의존 데이터가 바뀔 때만 재계산됩니다.
<script setup lang="ts">
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
const reversedName = computed(() =>
fullName.value.split('').reverse().join(''),
);
</script>
<template>
<input v-model="firstName" />
<input v-model="lastName" />
<p>{{ fullName }}</p>
<p>{{ reversedName }}</p>
</template>
2.4 watch와 watchEffect
watch는 명시적으로 감시 대상을 지정하고, watchEffect는 콜백이 읽는 반응형 값을 자동으로 추적합니다.
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
const message = ref('hello');
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`);
});
watch([count, message], ([c, m]) => {
console.log('both:', c, m);
});
watchEffect(() => {
console.log(`effect sees count=${count.value}`);
});
</script>
3. Composables와 Pinia (요약)
Composable은 useXxx 형태로 상태와 로직을 묶어 재사용하는 패턴입니다. Pinia는 Vue 3 권장 상태 관리로, defineStore와 ref/computed를 조합해 스토어를 정의합니다. 상세는 Pinia 완벽 가이드를 참고하면 됩니다. 여기서는 내부 이해를 위해 “스토어도 결국 반응형 객체와 effect의 소비자”라는 점만 기억하면 충분합니다.
4. 내부 동작: 반응형 시스템 (Proxy 기반)
Vue 3의 반응형은 ES2015 Proxy와 Reflect를 사용해 속성 접근·할당을 가로챕니다. Vue 2의 Object.defineProperty 방식과 비교하면, 추가·삭제된 속성, 인덱스 기반 배열 변경 등을 일관되게 다루기 쉬워졌습니다.
4.1 추적(track)과 트리거(trigger)
의미 있는 모델로 말하면 다음과 같습니다.
- effect: 반응형 데이터를 읽는 실행 단위(렌더 effect,
watchEffect콜백,computedgetter 등). - track: effect가 실행되는 동안 어떤 객체·키를 읽었는지 의존성으로 기록합니다.
- trigger: 해당 키가 바뀌면 그 키를 구독한 effect들을 다시 스케줄합니다.
렌더링 effect는 보통 비동기 배치되어, 동일 틱에서 여러 상태 변경이 있어도 DOM 업데이트는 합쳐질 수 있습니다.
4.2 ref와 reactive의 역할 분담
reactive: 대상이 객체일 때 Proxy로 감싸 깊은 반응형 트리를 만듭니다.ref: 원시값 등을 객체 래퍼 안에 넣어,.value접근 시 동일한 track/trigger 규칙이 적용되게 합니다. 템플릿에서는 unwrap 규칙으로 인해 개발자 경험이 단순해집니다.
readonly·shallowReactive·shallowRef 등은 이 파이프라인의 추적 깊이·쓰기 가능 여부를 바꾸는 변형입니다.
4.3 스케줄링과 일관된 UI
같은 데이터에 여러 번 쓰기가 일어나도, 스케줄러가 effect를 묶으면 중간 상태의 불필요한 렌더를 줄일 수 있습니다. 반대로 nextTick으로 “DOM 반영 이후” 콜백을 예약하는 패턴이 생깁니다. 이는 데이터와 화면 사이에 항상 한 박자 차이가 있을 수 있다는 것을 의미하므로, DOM 측정·포커스 제어 시 nextTick이 자주 등장합니다.
5. Virtual DOM과 패치 알고리즘
Vue는 선언적 템플릿 → 렌더 함수 → VNode 트리를 만들고, 이전 트리와 비교해 최소한의 DOM 작업만 수행합니다. 이 비교 과정이 patch입니다.
5.1 VNode란?
VNode(Virtual Node)는 실제 DOM 노드를 대표하는 경량 객체입니다. 태그명, props, 자식, 패치 힌트 등을 담습니다. 컴포넌트 타입의 VNode는 서브트리를 어떻게 만들지에 대한 정보(컴포넌트 정의, slots 등)를 가집니다.
5.2 patch와 자식 재조정
같은 위치의 두 VNode를 비교할 때, 타입이 다르면 교체(unmount/mount)에 가깝게 처리하고, 같으면 속성·텍스트·자식을 갱신합니다.
리스트에서는 key가 매우 중요합니다. key는 형제 사이에서 노드의 정체성을 나타내며, 재정렬·삽입 시 올바른 DOM 이동·재사용을 가능하게 합니다. Vue 3의 자식 비교는 키가 있는 패치에서 동적 프로그래밍과 최장 증가 부분 수열(LIS) 아이디어를 활용해 이동 최소화를 노립니다. 그래서 “key를 인덱스로 쓰면 안 되는 이유”가 이 알고리즘과 직결됩니다.
5.3 블록 트리(Block Tree)
Vue 3 컴파일러는 정적인 부분을 블록으로 묶고, 변경 가능성이 있는 노드만 동적 자식으로 추적합니다. 그 결과 전체 트리를 매번 완전 순회하지 않아도 되는 경우가 많아집니다. 이는 아래 컴파일러 최적화와 연결됩니다.
6. Composition API 구현 관점
6.1 setup과 컴포넌트 인스턴스
setup(props, ctx)는 렌더 effect가 실행되기 전에 호출되어, 반응형 상태·계산값·감시자를 준비합니다. 반환 객체는 렌더링 시 인스턴스에 병합되거나, script setup에서는 컴파일러가 동등한 바인딩을 생성합니다.
6.2 <script setup>의 컴파일 결과
<script setup>은 빌드 시 일반 setup 함수 코드로 변환됩니다. import한 컴포넌트가 자동 등록되고, defineProps/defineEmits 등은 컴파일 타임 매크로로 처리되어 런타임 오버헤드와 보일러플레이트를 줄입니다.
6.3 provide / inject
상위 컴포넌트가 provide한 값은 하위 트리에서 inject로 주입됩니다. 구현은 컴포넌트 인스턴스의 provide 컨텍스트 체인을 따라가며, 반응형으로 주입하려면 ref/computed를 넘기는 패턴이 문서화되어 있습니다.
7. 컴파일러 최적화
템플릿 컴파일러는 런타임 비용을 줄이기 위해 여러 최적화를 적용합니다.
7.1 정적 호이스팅(Static Hoisting)
반복 생성되는 정적 VNode는 한 번 만들어 재사용할 수 있습니다. 동적 부분만 매 렌더마다 갱신하면 됩니다.
7.2 패치 플래그(Patch Flags)
컴파일러는 노드에 “무엇이 바뀔 수 있는지”를 비트 플래그로 표시합니다. 런타임은 전체 props를 비교하지 않고 필요한 속성만 비교할 수 있습니다.
7.3 v-once와 캐시
v-once는 한 번 렌더된 서브트리를 이후 업데이트에서 건너뛰게 할 수 있습니다. 데이터가 정말로 고정일 때만 쓰는 것이 안전합니다.
8. 프로덕션 Vue 패턴
8.1 대용량 데이터와 shallowRef / markRaw
깊은 반응형이 불필요한 큰 리스트·외부 클래스 인스턴스에는 shallowRef나 markRaw로 추적 비용을 줄입니다. “반응형이 필요한 최소 단위”만 남기는 것이 핵심입니다.
8.2 v-memo
리스트 항목이 특정 의존성이 바뀔 때만 갱신되면 될 때 v-memo로 서브트리 메모이제이션을 할 수 있습니다. 조건이 잘못되면 오래된 UI가 남을 수 있으므로, 의존 배열을 정확히 설계해야 합니다.
8.3 비동기 컴포넌트와 코드 분할
defineAsyncComponent로 라우트·조건부 UI 단위를 나누면 초기 번들을 줄일 수 있습니다. 로딩·에러 슬롯을 함께 구성하면 운영 품질이 좋아집니다.
8.4 Suspense와 에러 처리
비동기 의존성이 있는 트리는 Suspense로 폴백 UI를 제공할 수 있습니다. 앱 전역 에러는 app.config.errorHandler 등으로 수집하는 편이 좋습니다.
8.5 빌드 체크리스트
- 프로덕션 빌드에서 dev 전용 경고 제거·트리 쉐이킹 확인
- 라우트 단위 청크와 이미지·아이콘 최적화
- 핵심 웹 바이탈 관점에서 장시간 작업은
requestIdleCallback등으로 분할 검토
정리
- 반응형은 Proxy + effect 스케줄링으로, “읽기”가 추적되고 “쓰기”가 무효화를 일으킵니다.
- 렌더링은 VNode diff(patch)이며, 리스트는
key+ 효율적인 자식 재조정에 의존합니다. - Composition API는 로직 재사용과 TS에 유리하고,
script setup은 컴파일 타임 지원으로 보일러플레이트를 줄입니다. - 컴파일러는 호이스팅·패치 플래그·블록 트리로 런타임 비용을 줄입니다.
- 프로덕션에서는 얕은 반응형·메모·코드 분할·에러/Suspense까지 고려해야 합니다.
자주 묻는 질문 (FAQ)
Q. Options API와 Composition API 중 무엇을 써야 하나요?
A. 신규 프로젝트는 Composition API + <script setup>을 권장합니다. 타입 추론과 로직 재사용에 유리합니다.
Q. ref만 써도 되나요?
A. 팀 규칙에 따라 다르지만, 원시값·객체 모두 ref로 통일하는 팀도 많습니다. reactive는 객체 중심 API를 선호할 때 유용합니다.
Q. Vue의 반응형이 React의 상태와 가장 크게 다른 점은?
A. Vue는 자동 추적이 기본이며, useMemo/useCallback처럼 의존 배열을 매번 손으로 적지 않는 경우가 많습니다. 대신 “반응형이 필요한 범위”를 설계하는 것이 중요합니다.
Q. Virtual DOM이 늘 병목인가요?
A. 아닙니다. Vue 3는 컴파일러 최적화와 블록 트리로 실제 비교·패치 비용을 줄입니다. 그래도 리스트 1만 개를 한 번에 그리는 것은 설계 문제일 수 있어 가상 스크롤 등을 검토합니다.
이 글에서 다루는 키워드
Vue, Vue 3, Composition API, Reactivity, Proxy, Virtual DOM, Patch, Compiler, Pinia, TypeScript, 프로덕션
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Vue 완벽 가이드 | Vue 3·Composition API·반응형 내부·컴파일러·프로덕션 패턴」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Vue 완벽 가이드 | Vue 3·Composition API·반응형 내부·컴파일러·프로덕션 패턴」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.