한눈에 비교

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 (더 범용적)