Vue 3 ref의 .value — 템플릿 vs 스크립트 접근 방식 차이
핵심 규칙
위치접근 방식이유<script>count.valueref 객체 그대로 다룸<template>countVue가 자동으로 .value를 언래핑
<script setup>
import { ref } from 'vue'
const count = ref(0)
// script에서는 .value 필수
console.log(count.value) // 0
count.value++
</script>
<template>
<!-- template에서는 .value 없이 바로 접근 -->
<p>{{ count }}</p>
<button @click="count++">+1</button>
</template>
ref란
ref()는 값을 반응형으로 만들기 위해 객체로 감싼 것이다.
const count = ref(0)
// count는 이런 구조의 객체
// { value: 0 }
console.log(count) // RefImpl { value: 0 }
console.log(count.value) // 0
원시 값(number, string, boolean)은 JavaScript에서 참조 추적이 불가능하다. Vue가 변경을 감지하려면 객체로 감싸야 하고, 그래서 .value를 통해 접근한다.
템플릿에서 자동 언래핑
Vue 컴파일러가 템플릿을 처리할 때 ref를 자동으로 언래핑한다.
<template>
<!-- 자동 언래핑: .value 불필요 -->
<p>{{ message }}</p>
<p>{{ count + 1 }}</p>
<input v-model="name" />
<button @click="count++">+1</button>
<div v-if="isVisible">보임</div>
</template>
<script setup>
const message = ref('안녕하세요')
const count = ref(0)
const name = ref('')
const isVisible = ref(true)
</script>
위 템플릿은 내부적으로 이렇게 변환된다.
// Vue 컴파일러가 자동으로 .value를 붙여준다
message.value
count.value + 1
// v-model, v-if, @click 등 모든 디렉티브에서 동일
스크립트에서는 .value 필수
const count = ref(0)
const name = ref('홍길동')
const items = ref([1, 2, 3])
const user = ref({ name: '김철수', age: 20 })
// 읽기
console.log(count.value) // 0
console.log(name.value) // '홍길동'
console.log(items.value[0]) // 1
console.log(user.value.name) // '김철수'
// 쓰기
count.value = 10
name.value = '이영희'
items.value.push(4)
user.value.age = 21
// 비교
if (count.value > 5) { ... }
// 함수 인자로 전달
doSomething(count.value)
computed도 동일
computed도 ref와 같은 규칙을 따른다.
<script setup>
const price = ref(10000)
const quantity = ref(3)
const total = computed(() => price.value * quantity.value)
// script에서는 .value
console.log(total.value) // 30000
</script>
<template>
<!-- template에서는 바로 접근 -->
<p>합계: {{ total }}원</p>
</template>
reactive는 .value가 없다
reactive()는 객체 자체를 반응형으로 만들므로 .value가 불필요하다.
import { ref, reactive } from 'vue'
// ref: .value 필요
const count = ref(0)
count.value++
// reactive: .value 불필요
const state = reactive({ count: 0 })
state.count++
<template>
<!-- 둘 다 template에서는 직접 접근 -->
<p>{{ count }}</p>
<p>{{ state.count }}</p>
</template>
ref vs reactive 비교
refreactive대상모든 타입객체, 배열script 접근.value 필요직접 접근template 접근자동 언래핑직접 접근재할당가능 (ref.value = 새값)불가 (속성만 변경)
// ref는 재할당 가능
const list = ref([1, 2, 3])
list.value = [4, 5, 6] // 전체 교체 가능
// reactive는 재할당 불가
const state = reactive({ list: [1, 2, 3] })
state.list = [4, 5, 6] // 속성 교체는 가능
// state = { list: [4, 5, 6] } // 전체 재할당 불가
흔한 실수
1. script에서 .value를 빼먹음
const count = ref(0)
// 잘못된 사용
if (count > 5) { ... } // 항상 true (ref 객체는 truthy)
console.log(count + 1) // "[object Object]1"
// 올바른 사용
if (count.value > 5) { ... }
console.log(count.value + 1) // 1
2. template에서 .value를 붙임
<template>
<!-- 잘못된 사용 (동작은 하지만 불필요) -->
<p>{{ count.value }}</p>
<!-- 올바른 사용 -->
<p>{{ count }}</p>
</template>
3. reactive 안의 ref는 자동 언래핑
const count = ref(0)
const state = reactive({ count })
// reactive 안에서 ref가 자동 언래핑
console.log(state.count) // 0 (.value 불필요)
state.count++ // ref의 .value가 증가
console.log(count.value) // 1
4. 구조분해 시 반응성 소실
const state = reactive({ name: '홍길동', age: 20 })
// 반응성 소실
const { name, age } = state
// name, age는 일반 변수가 됨 (반응형 아님)
// toRefs로 해결
import { toRefs } from 'vue'
const { name, age } = toRefs(state)
// name.value, age.value로 접근 (반응형 유지)
watch에서의 사용
const count = ref(0)
// ref 자체를 전달 (.value 아님)
watch(count, (newVal, oldVal) => {
// 콜백 인자는 이미 언래핑된 값
console.log(newVal, oldVal) // 숫자가 들어옴
})
// .value를 전달하면 감시 안 됨
watch(count.value, ...) // 그냥 숫자 0을 전달한 것 — 동작 안 함
// getter 함수로 전달
watch(() => count.value, (newVal) => { ... }) // 가능
정리
<template>:.value없이 바로 접근 — Vue가 자동 언래핑<script>:.value필수 — ref는{ value: ... }객체computed: ref와 동일한 규칙reactive:.value자체가 없음 — 속성에 직접 접근watch: ref 자체를 전달,.value로 전달하면 동작 안 함- template에서
.value를 쓰면 불필요, script에서 빼먹으면 버그