반응형 기초
반응형(Reactivity)이란, 데이터가 변경되면 그 데이터를 사용하는 모든 곳이 자동으로 업데이트되는 것입니다.
"ref와 reactive의 차이가 뭔가요?"는 Vue 면접의 단골 질문입니다. 단순히 "원시값은 ref, 객체는 reactive"라고 답하면 절반만 맞는 셈입니다. 진짜 차이는 Proxy 래핑 방식 에 있습니다.
ref — 어떤 값이든 반응형으로
ref는 모든 타입의 값을 반응형으로 만들 수 있습니다.
<script setup lang="ts">
import { ref } from 'vue'
// 원시값
const count = ref(0)
const name = ref('Vue')
const isActive = ref(true)
// 객체도 가능 — 내부적으로 reactive로 감싸짐
const user = ref({
name: '심정훈',
age: 25
})
// .value로 접근 (script 내에서)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// 객체 ref의 중첩 속성도 반응형
user.value.name = '김개발'
</script>
<template>
<!-- 템플릿에서는 .value 없이 직접 접근 -->
<p>{{ count }}</p>
<p>{{ user.name }}</p>
</template>
왜 .value가 필요한가? JavaScript에서 원시값은 참조가 아닌 값으로 전달됩니다. 함수에 number를 넘기면 원본과의 연결이 끊어집니다. ref는 원시값을 { value: ... } 객체로 감싸서 참조를 유지합니다.
reactive — 객체 전용 반응형
reactive는 객체를 Proxy로 감싸서 반응형으로 만듭니다.
<script setup lang="ts">
import { reactive } from 'vue'
// 객체를 직접 Proxy로 감싸기
const state = reactive({
count: 0,
user: {
name: '심정훈',
skills: ['Vue', 'TypeScript']
}
})
// .value 없이 직접 접근
state.count++
state.user.skills.push('Pinia')
// 중첩 객체도 자동으로 반응형 (Deep Reactivity)
console.log(state.user.skills) // ['Vue', 'TypeScript', 'Pinia']
</script>
<template>
<p>{{ state.count }}</p>
<ul>
<li v-for="skill in state.user.skills" :key="skill">
{{ skill }}
</li>
</ul>
</template>
ref vs reactive — 핵심 차이
| 항목 | ref | reactive |
|---|---|---|
| 대상 | 모든 타입 | 객체/배열/Map/Set만 |
| 접근 방식 | .value 필요 (script) | 직접 접근 |
| 재할당 | 가능 (ref.value = newObj) | 불가 (Proxy 참조 소실) |
| 구조 분해 | .value로 접근 | ** 반응형 소실 위험** |
| 권장 사용 | 대부분의 경우 | 관련 상태 그룹핑 시 |
reactive의 함정 — 구조 분해와 재할당
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
// 구조 분해 — 반응형 소실!
let { count } = state
count++ // 이 변경은 state.count에 반영되지 않음
// 재할당 — Proxy 참조 소실!
// state = reactive({ count: 1, name: 'Vue' }) // 원래 반응형 연결이 끊어짐
이 문제 때문에 Vue 공식 문서에서도 ref를 기본으로 권장 합니다.
computed — 파생 상태
computed는 다른 반응형 데이터로부터 계산된 값을 캐싱합니다.
<script setup lang="ts">
import { ref, computed } from 'vue'
const items = ref([
{ name: '사과', price: 1000, quantity: 3 },
{ name: '바나나', price: 500, quantity: 5 },
{ name: '오렌지', price: 800, quantity: 2 }
])
// 읽기 전용 computed
const totalPrice = computed(() => {
// items가 변경될 때만 재계산됨
return items.value.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
})
// 읽기/쓰기 가능한 computed (writable)
const firstName = ref('정훈')
const lastName = ref('심')
const fullName = computed({
get: () => `${lastName.value}${firstName.value}`,
set: (newValue: string) => {
lastName.value = newValue[0]
firstName.value = newValue.slice(1)
}
})
</script>
<template>
<p>총 금액: {{ totalPrice.toLocaleString() }}원</p>
<p>이름: {{ fullName }}</p>
</template>
computed vs method — 면접 빈출
| 항목 | computed | method |
|---|---|---|
| 캐싱 | 의존성이 변할 때만 재계산 | 호출할 때마다 실행 |
| 사용법 | {{ total }} | {{ getTotal() }} |
| 용도 | 파생 데이터 | 이벤트 핸들러, 사이드 이펙트 |
<script setup lang="ts">
import { ref, computed } from 'vue'
const list = ref([1, 2, 3, 4, 5])
// computed — 캐싱됨, list가 변할 때만 재계산
const evenNumbers = computed(() =>
list.value.filter(n => n % 2 === 0)
)
// method — 매번 재실행
const getEvenNumbers = () =>
list.value.filter(n => n % 2 === 0)
</script>
<template>
<!-- computed: 템플릿에서 여러 번 참조해도 한 번만 계산 -->
<p>{{ evenNumbers }}</p>
<p>{{ evenNumbers.length }}개</p>
<!-- method: 참조할 때마다 계산 -->
<p>{{ getEvenNumbers() }}</p>
<p>{{ getEvenNumbers().length }}개</p>
</template>
toRef와 toRefs — reactive에서 안전하게 분해
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({
count: 0,
name: 'Vue'
})
// toRef — 단일 속성을 ref로 변환 (원본과 연결 유지)
const countRef = toRef(state, 'count')
countRef.value++ // state.count도 1로 변경됨
// toRefs — 모든 속성을 ref로 변환
const { count, name } = toRefs(state)
count.value++ // state.count도 2로 변경됨
composable 함수에서 reactive 객체를 반환할 때 toRefs로 감싸는 패턴이 일반적입니다.
readonly — 읽기 전용 반응형
import { ref, readonly } from 'vue'
const original = ref(0)
const readonlyRef = readonly(original)
// readonlyRef.value++ // 경고 발생! 읽기 전용
original.value++ // 원본 변경은 가능, readonlyRef도 업데이트됨
Props처럼 자식 컴포넌트에서 변경하면 안 되는 데이터에 유용합니다.
면접 팁
- "ref를 기본으로 쓰고, 관련 상태를 그룹핑할 때만 reactive를 고려한다"가 실무 권장 패턴입니다
- computed의 캐싱 동작을 설명할 때 "의존성 추적"이라는 키워드를 사용하면 좋습니다
- reactive의 구조 분해 문제와 해결법(toRefs)을 함께 설명하면 심화 이해도를 보여줄 수 있습니다
요약
ref는 모든 타입에 사용할 수 있는 범용 반응형 래퍼이고, reactive는 객체 전용입니다. computed는 의존성 기반 캐싱이 핵심이며, method와의 차이를 명확히 알아야 합니다. reactive의 구조 분해 시 반응형 소실 문제는 toRefs로 해결할 수 있습니다.
댓글 로딩 중...