ref와 reactive로 반응형 상태 다루기
ref와 reactive로 반응형 상태 다루기
요약: 값이 바뀌면 화면이 다시 그려지도록 ref로 상태를 두고, 계산 값과 감시 로직을 나눈다.
1. 왜 반응형인가
일반 변수 let n = 0은 값이 바뀌어도 화면이 모른다. Vue의 반응형은 값이 바뀌면 그 값을 쓰는 템플릿을 다시 그리게 만든다.
언제 무엇을 쓰는가.
| 상황 | 선택 |
|---|---|
| 숫자, 문자열, 불리언, 한 객체 참조 | ref |
| 서로 붙어 다니는 필드 묶음 | reactive 또는 ref({ ... }) |
| 다른 상태에서 계산만 하는 값 | computed |
| 값이 바뀔 때 API 호출 등 | watch / watchEffect |
실무 기본선은 거의 모든 상태를 ref로 두고, 필드가 많은 폼만 reactive를 검토한다.
2. 핵심 개념
| API | 한 줄 |
|---|---|
ref |
값이 바뀌면 화면을 다시 그릴 상자. script에서는 .value |
reactive |
객체 전체에 Proxy를 씌운다. 프로퍼티는 바로 읽는다 |
| 언래핑 | 템플릿은 ref 상자를 자동으로 연다 |
computed |
다른 상태에 의존하는 캐시된 파생 값 |
watch |
특정 상자가 바뀔 때 부수 효과를 실행한다 |
ref로 객체를 담아도 된다. user.value.name = 'a'처럼 연 뒤 필드를 바꾼다. Vue 3는 객체 ref의 안쪽도 반응형이다.
3. 동작하는 예: 프로필 폼
src/components/ProfileForm.vue:
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
const count = ref(0)
const form = reactive({
name: '',
age: 0,
})
const summary = computed(() => {
if (!form.name) return '이름을 입력한다'
return `${form.name} (${form.age}세), 저장 ${count.value}회`
})
watch(
() => form.name,
(next, prev) => {
if (prev && next !== prev) {
count.value = 0
}
},
)
function save() {
if (!form.name.trim()) return
count.value += 1
}
function reset() {
form.name = ''
form.age = 0
count.value = 0
}
</script>
<template>
<form @submit.prevent="save">
<label>
이름
<input v-model="form.name" type="text" autocomplete="name" />
</label>
<label>
나이
<input v-model.number="form.age" type="number" min="0" />
</label>
<p>{{ summary }}</p>
<button type="submit">저장</button>
<button type="button" @click="reset">초기화</button>
</form>
</template>
count는 단일 숫자 →refform은 이름·나이가 함께 움직임 →reactivesummary는 둘을 읽어 문장을 만든다 →computed(직접 할당하지 않는다)- 이름이 바뀌면 저장 횟수를 0으로 →
watch
템플릿의 v-model="form.name"은 form.name에 직접 붙는다. v-model="count"는 템플릿 언래핑 덕분에 .value 없이 동작한다.
4. 구조 분해와 교체
reactive 객체를 구조 분해하면 반응형이 끊긴다.
const form = reactive({ name: '', age: 0 })
const { name } = form // name은 일반 문자열. 이후 form.name 변경이 이 변수에 안 온다
필드를 따로 쓰려면 toRefs를 쓴다.
import { reactive, toRefs } from 'vue'
const form = reactive({ name: '', age: 0 })
const { name, age } = toRefs(form) // name.value, age.value
객체 통째 교체도 reactive의 약점이다.
let state = reactive({ name: 'a' })
state = reactive({ name: 'b' }) // 새 객체. 템플릿이 옛 Proxy를 보고 있을 수 있다
통째 교체가 필요하면 ref로 담는다.
const state = ref({ name: 'a' })
state.value = { name: 'b' } // 상자 안만 갈아끼운다
5. computed와 watch 구분
import { ref, computed, watch, watchEffect } from 'vue'
const price = ref(1000)
const qty = ref(2)
const total = computed(() => price.value * qty.value)
watch(qty, (n) => {
if (n < 1) qty.value = 1
})
watchEffect(() => {
console.log('합계', total.value)
})
computed |
watch |
|
|---|---|---|
| 목적 | 파생 값 | 부수 효과 |
| 캐시 | 의존이 같으면 재계산 안 함 | 매번 콜백 |
| 쓰기 | 기본은 읽기 전용 | 값 변경·API·로그 |
화면 표시용 계산은 computed다. 저장 API, localStorage, 포커스 이동은 watch다.
6. 주의 / 흔한 실수
- script에서
.value누락.count = 1은ref상자를 숫자로 덮어 반응형이 죽는다.count.value = 1이다. reactive에 원시값.reactive(0)은 객체가 아니라서 기대한 추적이 안 된다. 원시는ref.reactive변수 재할당. 위 4절. 교체가 필요하면ref.- deep
watch남용.{ deep: true }는 큰 객체에서 비용이 크다. 경로를 좁힌다. computed안에서 상태 변경. 파생만 한다. 쓰기는watch나 이벤트 핸들러.
정리
상태를 만들 때 질문 하나는 이것이다. 값 하나인가, 필드 묶음인가.
- 하나 →
ref - 묶음 →
reactive또는ref({ ... }) - 파생 →
computed - 부수 효과 →
watch
거의 모든 신규 코드는 ref만으로도 충분하다.
연습
ref로score를 두고,computed로 60 이상이면통과아니면재시험을 표시한다.reactive주소 객체{ city, street }를 만들고toRefs로 템플릿에 나눠 바인딩한다.watch로score가 100이 되면console.log('만점')을 한 번 출력한다.
댓글
0아직 댓글이 없습니다. 첫 댓글을 남겨보세요.