Vue 3의 가장 큰 변화 중 하나는 Composition API의 도입입니다. 기존의 Options API와 비교하여 더 나은 코드의 구조화, 재사용성, 유지보수성을 제공하며, 특히 TypeScript와의 호환성이 뛰어납니다. 이 글에서는 Composition API의 기본 개념부터 실제 사용법까지 깊이 있게 다루겠습니다.
1. Composition API란?
Composition API는 Vue 3에서 새롭게 도입된 API로, setup() 함수 내에서 Vue의 반응형(Reactivity) 기능과 라이프사이클을 활용하여 컴포넌트를 구성하는 방식입니다. 기존 Options API와 비교했을 때 다음과 같은 장점이 있습니다.
🔹 Composition API의 장점
- 논리적 코드 분리: 기능별로 코드를 그룹화할 수 있어 유지보수가 용이합니다.
- 재사용성 증가: 로직을 별도의 함수 또는 composable로 분리하여 재사용이 쉬워집니다.
- TypeScript 친화적: Composition API는 TypeScript와의 호환성이 뛰어나며, 명확한 타입 정의가 가능합니다.
- Vue 2의 mixin보다 직관적: mixin의 단점(변수 충돌, 추적 어려움 등)을 보완하여 가독성과 확장성이 뛰어납니다.
- 테스트가 용이함: 비즈니스 로직을 독립적인 함수로 분리할 수 있어 테스트 코드 작성이 수월합니다.
- 코드의 일관성 유지: 다양한 기능이 동일한 패턴으로 작성되어 팀 협업 시 가독성과 유지보수성이 향상됩니다.
2. Composition API의 기본 사용법
2.1 setup() 함수
setup()은 Vue 3 컴포넌트에서 가장 먼저 실행되는 함수로, 컴포넌트의 props를 받고, 반응형 상태를 정의하며, 라이프사이클 훅을 사용할 수 있습니다.
<script setup>
import { ref, computed, onMounted } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
onMounted(() => {
console.log('컴포넌트가 마운트됨');
});
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">+</button>
</div>
</template>
✅ setup() 함수 특징
- ref()와 computed()를 활용하여 반응형 상태를 관리합니다.
- onMounted() 라이프사이클 훅을 사용하여 마운트 시 동작을 정의합니다.
- setup() 내에서 정의한 변수와 함수는 template에서 직접 사용할 수 있습니다.
- setup() 내부에서 props를 받을 수 있으며, context API를 활용할 수도 있습니다.
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
title: String
});
</script>
<template>
<h1>{{ props.title }}</h1>
</template>
2.2 reactive()를 이용한 객체 상태 관리
ref()는 기본적으로 원시 값을 감싸는 반응형 상태를 제공합니다. 그러나 객체나 배열을 다룰 때는 reactive()를 사용하는 것이 더 직관적일 수 있습니다.
<script setup>
import { reactive } from 'vue';
const user = reactive({
name: '홍길동',
age: 30
});
function incrementAge() {
user.age++;
}
</script>
<template>
<div>
<p>이름: {{ user.name }}</p>
<p>나이: {{ user.age }}</p>
<button @click="incrementAge">나이 증가</button>
</div>
</template>
reactive()는 ref()와 다르게 객체를 감싸므로 .value를 사용할 필요가 없습니다.
3. Composition API 주요 기능
3.1 반응형 상태 관리
ref() vs reactive() 비교
기능 | ref() | reactive() |
원시 값 | ✅ 지원 | ❌ 지원하지 않음 |
객체/배열 | ✅ 지원하지만 .value 필요 | ✅ .value 없이 바로 사용 가능 |
구조 분해(Destructuring) 시 반응성 유지 | ❌ 유지되지 않음 | ✅ 유지됨 |
반응형 상태를 사용할 때 ref()는 원시 값을 감쌀 때 유용하고, reactive()는 객체를 다룰 때 직관적인 장점을 가집니다.
3.2 watch와 watchEffect
Vue 3의 Composition API에서 상태 변화를 감지하고 특정 로직을 실행할 때 watch()와 watchEffect()를 사용할 수 있습니다. 하지만 두 함수는 동작 방식에 차이가 있어 상황에 따라 적절하게 선택해야 합니다.
watch
watch()는 특정 반응형 상태(ref, reactive)의 변화를 감지하여 실행됩니다. 감시할 대상을 명확히 지정해야 합니다.
✅ watch 특징
- 특정 상태의 변화(이전 값 → 새로운 값)를 감지하여 실행됨
- 감시할 대상을 직접 지정해야 함
- 변경 이전 값과 이후 값을 함께 받을 수 있음
- 비동기 작업(예: API 호출)에 적합
📌 watch() 사용 예제
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`Count 변경됨: ${oldVal} → ${newVal}`);
});
function increment() {
count.value++;
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">증가</button>
</div>
</template>
- watch(count, (newVal, oldVal) => {...})에서 count 값이 변경될 때마다 콜백이 실행됨
- 이전 값(oldVal)과 새로운 값(newVal)을 사용할 수 있음
watchEffect
watchEffect()는 내부에서 사용된 반응형 상태를 자동으로 추적하여 실행됩니다. 즉, 어떤 상태를 감시할지 직접 지정할 필요 없이, 사용된 모든 반응형 변수들을 자동으로 감지합니다.
✅ watchEffect 특징
- 내부에서 참조하는 모든 반응형 상태를 자동으로 감지
- 즉시 실행됨 (선언되는 순간 실행됨)
- 감시 대상의 이전 값과 새로운 값을 직접 받을 수 없음
- 비동기 작업에도 활용 가능하지만, watch()보다 정교한 제어가 어려움
📌 watchEffect 사용 예제
<script setup>
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log(`현재 Count 값: ${count.value}`);
});
function increment() {
count.value++;
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">증가</button>
</div>
</template>
- watchEffect() 내부에서 count.value를 참조하고 있기 때문에, count 값이 변경될 때마다 자동으로 실행됨
- watch()와 달리 oldVal, newVal을 직접 받을 수 없음
- 선언된 즉시 한 번 실행됨
watch() vs watchEffect() 비교
기능 비교 | watch | watchEffect |
감시 대상 지정 | 명시적으로 지정해야 함 | 내부에서 참조하는 반응형 변수를 자동 감지 |
즉시 실행 여부 | 값이 변경될 때만 실행 | 선언 즉시 실행됨 |
이전 값 (oldVal) 접근 가능 여부 | ✅ 가능 | ❌ 불가능 |
반응형 상태 추적 방식 | 특정 상태를 감시 | 모든 사용된 반응형 상태를 자동 감지 |
비동기 처리 | 가능 | 가능하지만 정교한 제어 어려움 |
3.3 provide와 inject를 이용한 상태 공유
Vue 3에서 컴포넌트 간 데이터를 전달하는 방법으로 가장 많이 사용하는 것이 props와 emit입니다. 하지만 여러 개의 중첩된 컴포넌트에서 데이터를 전달할 때는 provide()와 inject()를 사용하면 더욱 효율적입니다.
✅ provide()
- 부모 컴포넌트에서 데이터를 제공(provide)
- 자식, 손자, 그 이하 모든 하위 컴포넌트에서 inject()로 받을 수 있음
✅ inject()
- 하위 컴포넌트에서 부모가 제공한 데이터(inject) 가져오기
- 중간에 있는 컴포넌트에서 데이터를 전달할 필요 없이 직접 접근 가능
📌 쉽게 말하면:
- provide() → 부모가 데이터를 제공
- inject() → 자식이 부모의 데이터를 가져옴
🏗 props vs provide/inject
방법사용 범위데이터 전달 방식적합한 상황
방법 | 사용 범위 | 데이터 전달 방식 | 적합한 상황 |
props | 부모 → 자식 | 직접 전달 (1단계) | 단순한 데이터 전달 |
provide/inject | 부모 → (중간 단계 없이) 자손 | 전역적으로 전달 (다단계) | 깊은 컴포넌트 트리에서 데이터 공유 |
provide 예제 - 부모 컴포넌트 (데이터 제공)
<script setup>
import { ref, provide } from 'vue';
import ThemeComponent from './ThemeComponent.vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
<template>
<div :class="theme">
<button @click="toggleTheme">테마 변경</button>
<ThemeComponent />
</div>
</template>
inject 예제 - 자식 컴포넌트 (데이터 주입)
<script setup>
import { inject } from 'vue';
const theme = inject('theme');
const toggleTheme = inject('toggleTheme');
</script>
<template>
<div>
<h2>현재 테마: {{ theme }}</h2>
<button @click="toggleTheme">테마 변경</button>
</div>
</template>
✅ provide()와 inject()를 사용할 때 주의할 점 🚨
- provide/inject는 부모-자식 관계에서만 작동
- 형제 컴포넌트끼리는 사용할 수 없음
- 형제 간 데이터 공유는 상위 부모에서 provide()한 후, 하위에서 inject()하여 사용
- 반응형 객체는 reactive() 또는 ref()로 제공해야 함
- provide('count', 0) (❌ → 기본 데이터는 반응형이 아님)
- provide('count', ref(0)) (✅ → 반응형 데이터 제공 가능)
- provide()는 부모가 먼저 실행되어야 함
- 부모가 provide()를 실행하기 전에 자식이 inject()를 호출하면 오류 발생
4. Composition API vs Options API
비교 항목 | Composition API | Options API |
코드 구조 | 논리적으로 그룹화 가능 | 옵션별로 분리됨 (data, methods, computed 등) |
재사용성 | 높은 재사용성 (Composable 활용) | Mixin 사용, 충돌 가능성 있음 |
유지보수 | 용이함 | 복잡한 컴포넌트에서 어려움 |
Vue 3의 Composition API는 유지보수성과 재사용성을 극대화하는 강력한 도구입니다. 특히 대규모 프로젝트나 TypeScript와 함께 사용할 때 큰 장점을 제공합니다. 기존 Options API에서 Composition API로의 전환을 고민하는 개발자라면 한 번 시도해 보길 추천합니다! 🚀