Vue 3 ref와 reactive의 차이
한눈에 비교
refreactive대상모든 타입 (원시값, 객체, 배열)객체, 배열만접근 (script).value 필요직접 접근접근 (template)자동 언래핑직접 접근재할당가능불가구조분해반응성 유지반응성 소실
ref
ref()는 어떤 값이든 반응형으로 만든다. 내부적으로 { value: 값 } 객체로 감싼다.
import { ref } from 'vue'
// 원시값
const count = ref(0)
const name = ref('홍길동')
const isOpen = ref(false)
// 객체, 배열도 가능
const user = ref({ name: '김철수', age: 20 })
const list = ref([1, 2, 3])
// script에서는 .value로 접근
count.value++
user.value.name = '이영희'
list.value.push(4)
<template>
<!-- template에서는 .value 불필요 -->
<p>{{ count }}</p>
<p>{{ user.name }}</p>
</template>
reactive
reactive()는 객체나 배열을 반응형으로 만든다. .value 없이 직접 접근한다.
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: '홍길동',
items: [1, 2, 3]
})
// .value 없이 직접 접근
state.count++
state.name = '이영희'
state.items.push(4)
<template>
<p>{{ state.count }}</p>
<p>{{ state.name }}</p>
</template>
핵심 차이
1. 원시값 처리
// ref: 원시값 가능
const count = ref(0) // 동작함
const name = ref('hello') // 동작함
// reactive: 원시값 불가
const count = reactive(0) // 경고, 반응성 없음
const name = reactive('hello') // 경고, 반응성 없음
2. 재할당
// ref: 전체 교체 가능
const user = ref({ name: '홍길동' })
user.value = { name: '김철수' } // 반응성 유지
const list = ref([1, 2, 3])
list.value = [4, 5, 6] // 반응성 유지
// reactive: 전체 교체 불가
const state = reactive({ name: '홍길동' })
// state = { name: '김철수' } // 변수 재할당 — 반응성 소실
// reactive는 속성 변경만 가능
state.name = '김철수' // 이건 가능
API 응답으로 객체를 통째로 교체할 때 ref가 편리하다.
// ref: 간단
const users = ref([])
async function fetchUsers() {
users.value = await api.getUsers() // 전체 교체
}
// reactive: 우회 필요
const state = reactive({ users: [] })
async function fetchUsers() {
state.users = await api.getUsers() // 속성에 할당
// 또는
const data = await api.getUsers()
state.users.length = 0
state.users.push(...data)
}
3. 구조분해
// reactive: 구조분해 시 반응성 소실
const state = reactive({ count: 0, name: '홍길동' })
const { count, name } = state // 일반 변수가 됨, 반응성 없음
// toRefs로 해결
import { toRefs } from 'vue'
const { count, name } = toRefs(state) // ref로 변환, 반응성 유지
count.value++ // .value 필요
// ref: 구조분해 해당 없음 (단일 값)
const count = ref(0) // 이미 개별 ref
4. 타입 체크
import { isRef, isReactive } from 'vue'
const count = ref(0)
const state = reactive({ count: 0 })
isRef(count) // true
isReactive(count) // false
isRef(state) // false
isReactive(state) // true
중첩 객체에서의 동작
ref 안의 객체
const user = ref({
name: '홍길동',
address: {
city: '서울'
}
})
// 내부 객체도 반응형 (자동으로 reactive 적용)
user.value.address.city = '부산' // 반응성 동작
reactive 안의 ref
const count = ref(0)
const state = reactive({ count })
// reactive 안에서 ref는 자동 언래핑
console.log(state.count) // 0 (.value 불필요)
state.count++
console.log(count.value) // 1 (원본 ref도 변경됨)
단, 배열 안의 ref는 자동 언래핑되지 않는다.
const item = ref('hello')
const list = reactive([item])
console.log(list[0]) // RefImpl 객체
console.log(list[0].value) // 'hello' (.value 필요)
실무에서의 선택 기준
ref를 쓰는 경우
// 1. 단일 원시값
const count = ref(0)
const isLoading = ref(false)
const searchQuery = ref('')
// 2. API 응답 전체 교체
const posts = ref([])
posts.value = await fetchPosts()
// 3. 컴포넌트 간 전달 (props, emit)
const selected = ref(null)
// 4. composable 반환값
function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
reactive를 쓰는 경우
// 1. 관련된 상태를 그룹핑
const form = reactive({
email: '',
password: '',
rememberMe: false
})
// 2. 복잡한 상태 관리
const editor = reactive({
content: '',
fontSize: 14,
theme: 'dark',
history: [],
cursor: { line: 0, col: 0 }
})
// 3. .value 타이핑이 번거로울 때
const mouse = reactive({ x: 0, y: 0 })
window.addEventListener('mousemove', (e) => {
mouse.x = e.clientX // mouse.value.x 보다 간결
mouse.y = e.clientY
})
watch에서의 차이
const count = ref(0)
const state = reactive({ count: 0, name: '홍길동' })
// ref: 직접 전달
watch(count, (newVal) => {
console.log('count:', newVal)
})
// reactive: 직접 전달하면 deep watch
watch(state, (newVal) => {
console.log('state 변경')
})
// reactive: 특정 속성만 감시하려면 getter 사용
watch(() => state.count, (newVal) => {
console.log('state.count:', newVal)
})
흔한 실수
1. reactive를 재할당
let state = reactive({ count: 0 })
// 반응성 소실 — 새 객체를 할당해도 기존 반응성과 무관
state = reactive({ count: 10 }) // 템플릿이 업데이트 안 됨
// 해결: ref 사용 또는 속성 변경
const state = ref({ count: 0 })
state.value = { count: 10 } // ref는 재할당 가능
2. reactive에서 속성 삭제
const state = reactive({ a: 1, b: 2 })
delete state.b // 반응성 동작함 (Vue 3는 Proxy 기반)
3. ref를 reactive에 넣었다 뺄 때
const count = ref(0)
const state = reactive({ count })
// state.count는 언래핑된 값
state.count = 5
console.log(count.value) // 5
// 새 ref로 교체하면 연결 끊어짐
state.count = ref(10)
console.log(count.value) // 5 (이전 ref는 변경 안 됨)
정리
- 원시값이면 →
ref - 객체 전체 교체가 필요하면 →
ref - 관련 상태 그룹핑이 목적이면 →
reactive .value타이핑이 싫으면 →reactive- composable 반환값으로는 →
ref(구조분해 안전) - 확신이 없으면 →
ref(더 범용적)