Vue 3 핵심 개념: Composition API와 Reactivity
Vue 3의 두 가지 API 스타일
Vue 3는 컴포넌트를 작성하는 두 가지 방식을 제공합니다.
Options API
Vue 2에서 사용하던 방식입니다. data, methods, computed 등 옵션별로 코드를 분리합니다.
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
},
computed: {
doubled() {
return this.count * 2
}
}
}
Composition API
Vue 3에서 도입된 방식입니다. 관련 로직을 함께 모아서 작성할 수 있습니다.
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubled, increment }
}
}
Composition API의 장점은 관련 기능을 한 곳에 모을 수 있다는 것입니다. Options API에서는 하나의 기능이 data, methods, computed에 흩어져 있지만, Composition API에서는 하나의 함수로 묶을 수 있습니다.
참고로 두 API는 한 프로젝트에서 혼용할 수 있어서, 기존 Options API 프로젝트에 Composition API를 점진적으로 도입하는 것도 가능합니다.
ref vs reactive
Vue 3를 처음 접할 때 가장 헷갈리는 게 ref와 reactive입니다.
ref()
기본값(primitive)이나 단일 값을 반응형으로 만들 때 사용합니다.
import { ref } from 'vue'
const count = ref(0)
const name = ref('Vue')
const isVisible = ref(true)
// 값 접근/변경 시 .value 필요
console.log(count.value) // 0
count.value++
템플릿에서는 .value 없이 사용합니다.
<template>
<p>{{ count }}</p> <!-- .value 불필요 -->
</template>
reactive()
객체를 반응형으로 만들 때 사용합니다. .value 없이 직접 접근합니다.
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'Vue',
age: 3
}
})
// .value 없이 직접 접근
state.count++
state.user.name = 'Vue 3'
언제 뭘 써야 하나?
| 상황 | 추천 |
|---|---|
| 단일 값 (숫자, 문자열, 불리언) | ref() |
| 객체나 배열 | ref() 또는 reactive() |
| 기본값으로 | ref() 사용 |
실무에서도 ref()를 기본으로 쓰는 편이고, Vue 공식 문서도 ref()를 기본으로 사용하는 것을 권장합니다. 이유는:
ref()는 모든 타입에서 동작합니다reactive()는 구조분해 시 반응성이 깨집니다- 일관성 있게
.value를 사용하면 혼란이 줄어듭니다
// reactive()의 함정: 구조분해 시 반응성 손실
const state = reactive({ count: 0 })
const { count } = state // 반응성 손실!
count++ // UI 업데이트 안됨
// 해결: toRefs() 사용
import { toRefs } from 'vue'
const { count } = toRefs(state) // 반응성 유지
Vue의 Reactivity 시스템
Proxy 기반 반응성
Vue 3는 JavaScript Proxy를 사용해 반응성을 구현합니다. data()가 반환하는 객체는 Proxy로 래핑됩니다.
Vue.createApp({
data() {
return {
items: [] // ES6 Proxy 객체로 래핑됨
}
}
})
배열의 데이터를 변경하는 메서드들(push(), splice(), sort())도 함께 래핑되어, 변경 시 자동으로 UI가 업데이트됩니다.
데이터를 변경할 때는 항상 Proxy 객체를 통해서 변경해야 합니다. Vue 인스턴스 외부에서 원본 배열을 직접 변경하면 반응성이 동작하지 않습니다.
렌더링 디렉티브
텍스트 렌더링
v-text: HTML 태그를 이스케이프 처리하여 문자열로 출력합니다.
<h2 v-text="message"></h2>
<!-- 내부적으로 innerText에 연결 -->
v-html: HTML 태그를 파싱하여 렌더링합니다. XSS 공격에 주의해야 합니다.
<div v-html="htmlContent"></div>
<!-- 내부적으로 innerHTML에 연결 -->
데이터 바인딩
v-bind: 단방향 바인딩입니다. 모델 → 뷰 방향으로만 동기화됩니다.
<!-- 전체 문법 -->
<input type="text" v-bind:value="message" />
<img v-bind:src="imgPath" />
<!-- 축약 문법 -->
<input type="text" :value="message" />
<img :src="imgPath" />
v-model: 양방향 바인딩입니다. 폼 요소에서 사용합니다.
<input type="text" v-model="message" />
사용자가 입력하면 message가 자동으로 업데이트됩니다.
<!-- 체크박스, 셀렉트, 라디오에도 사용 가능 -->
<div id="app">
<h2>개발자</h2>
<input type="checkbox" id="devA" value="A" v-model="developer" />
<label for="devA">찬수</label>
<input type="checkbox" id="devB" value="B" v-model="developer" />
<label for="devB">준영</label>
<h3>도메인</h3>
<select v-model="domain">
<option value="C1">의료</option>
<option value="C2">금융</option>
<option value="C3">커머스</option>
</select>
</div>
<script>
const vm = Vue.createApp({
data() {
return {
developer: [], // 체크박스는 배열로
domain: "" // 셀렉트는 문자열로
}
}
}).mount("#app")
</script>
v-model 수식어 (Modifiers)
<!-- .number: 숫자로 자동 변환 -->
<input type="text" v-model.number="age" />
<!-- .trim: 앞뒤 공백 제거 -->
<input type="text" v-model.trim="name" />
<!-- .lazy: change 이벤트 시에만 동기화 (input 아님) -->
<input type="text" v-model.lazy="message" />
<!-- 여러 개 조합 가능 -->
<input type="text" v-model.number.trim="value" />
조건부 렌더링
v-if vs v-show
<!-- v-if: 조건이 false면 DOM에서 제거 -->
<p v-if="isVisible">보임</p>
<p v-else-if="isPending">대기중</p>
<p v-else>숨김</p>
<!-- v-show: 조건이 false면 display:none -->
<p v-show="isVisible">보임</p>
| 특성 | v-if | v-show |
|---|---|---|
| 렌더링 | 조건이 true일 때만 | 항상 렌더링 |
| DOM | 조건 변경 시 생성/제거 | CSS로 숨김 처리 |
| 초기 비용 | 낮음 | 높음 |
| 토글 비용 | 높음 | 낮음 |
| 추천 상황 | 거의 바뀌지 않는 조건 | 자주 토글되는 경우 |
반복 렌더링 (v-for)
기본 사용법
<!-- 배열 -->
<div v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</div>
<!-- 객체 -->
<div v-for="(value, key, index) in object" :key="key">
{{ key }}: {{ value }}
</div>
key 속성의 중요성
<!-- 나쁜 예: index를 key로 사용 -->
<div v-for="(item, index) in items" :key="index">
{{ item.name }}
</div>
<!-- 좋은 예: 고유한 ID를 key로 사용 -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
key에는 고유한 값(DB의 PK 등)을 사용해야 합니다. index를 사용하면:
- 항목 순서가 바뀔 때 불필요한 리렌더링 발생
- 입력 필드의 상태가 꼬일 수 있음
template으로 그룹화
<template v-for="(item, index) in items" :key="item.id">
<tr>
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
</tr>
<hr v-show="index % 2 === 0" />
</template>
기타 유용한 디렉티브
<!-- v-pre: 컴파일하지 않고 그대로 출력 -->
<span v-pre>{{ message }}</span>
<!-- 출력: {{ message }} -->
<!-- v-once: 최초 한 번만 렌더링 -->
<span v-once>{{ timestamp }}</span>
<!-- v-cloak: 컴파일 전까지 숨김 (깜빡임 방지) -->
<style>
[v-cloak] { display: none; }
</style>
<div id="app" v-cloak>
{{ message }}
</div>
동적 인자 (Dynamic Arguments)
디렉티브의 인자를 동적으로 지정할 수 있습니다.
<!-- 동적 속성 바인딩 -->
<img v-bind:[attrName]="attrValue" />
<img :[attrName]="attrValue" />
<!-- 동적 이벤트 바인딩 -->
<button v-on:[eventName]="handler">클릭</button>
<button @[eventName]="handler">클릭</button>
data() {
return {
attrName: 'src',
attrValue: 'https://example.com/image.png',
eventName: 'click'
}
}
여러 속성을 한 번에 바인딩할 수도 있습니다.
<img v-bind="imageAttrs" />
data() {
return {
imageAttrs: {
src: 'https://example.com/image.png',
alt: '이미지 설명',
width: 200
}
}
}
Lifecycle Hooks
컴포넌트의 생명주기에 맞춰 로직을 실행할 수 있습니다. API 호출이나 이벤트 리스너 등록처럼 특정 시점에 실행해야 하는 코드가 있을 때 유용합니다.
Options API
export default {
created() {
console.log('컴포넌트 생성됨')
},
mounted() {
console.log('DOM에 마운트됨')
},
updated() {
console.log('데이터 변경으로 리렌더링됨')
},
unmounted() {
console.log('컴포넌트 제거됨')
}
}
Composition API
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('DOM에 마운트됨')
})
onUpdated(() => {
console.log('리렌더링됨')
})
onUnmounted(() => {
console.log('컴포넌트 제거됨')
})
}
}
| Options API | Composition API | 시점 |
|---|---|---|
| beforeCreate | setup() 시작* | 인스턴스 초기화 직후 |
| created | setup() 끝* | 반응성 설정 완료 |
| beforeMount | onBeforeMount | DOM 마운트 직전 |
| mounted | onMounted | DOM 마운트 완료 |
| beforeUpdate | onBeforeUpdate | 리렌더링 직전 |
| updated | onUpdated | 리렌더링 완료 |
| beforeUnmount | onBeforeUnmount | 언마운트 직전 |
| unmounted | onUnmounted | 언마운트 완료 |
*Composition API의 setup()은 beforeCreate와 created 사이에 실행됩니다. 별도의 훅 함수 없이 setup() 안에서 직접 코드를 작성하면 됩니다.
