핵심 규칙

위치접근 방식이유<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에서 빼먹으면 버그