반응형(Reactivity)이란, 데이터가 변경되면 그 데이터를 사용하는 모든 곳이 자동으로 업데이트되는 것입니다.

"ref와 reactive의 차이가 뭔가요?"는 Vue 면접의 단골 질문입니다. 단순히 "원시값은 ref, 객체는 reactive"라고 답하면 절반만 맞는 셈입니다. 진짜 차이는 Proxy 래핑 방식 에 있습니다.


ref — 어떤 값이든 반응형으로

ref는 모든 타입의 값을 반응형으로 만들 수 있습니다.

VUE
<script setup lang="ts">
import { ref } from 'vue'

// 원시값
const count = ref(0)
const name = ref('Vue')
const isActive = ref(true)

// 객체도 가능 — 내부적으로 reactive로 감싸짐
const user = ref({
  name: '심정훈',
  age: 25
})

// .value로 접근 (script 내에서)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

// 객체 ref의 중첩 속성도 반응형
user.value.name = '김개발'
</script>

<template>
  <!-- 템플릿에서는 .value 없이 직접 접근 -->
  <p>{{ count }}</p>
  <p>{{ user.name }}</p>
</template>

.value가 필요한가? JavaScript에서 원시값은 참조가 아닌 값으로 전달됩니다. 함수에 number를 넘기면 원본과의 연결이 끊어집니다. ref는 원시값을 { value: ... } 객체로 감싸서 참조를 유지합니다.


reactive — 객체 전용 반응형

reactive는 객체를 Proxy로 감싸서 반응형으로 만듭니다.

VUE
<script setup lang="ts">
import { reactive } from 'vue'

// 객체를 직접 Proxy로 감싸기
const state = reactive({
  count: 0,
  user: {
    name: '심정훈',
    skills: ['Vue', 'TypeScript']
  }
})

// .value 없이 직접 접근
state.count++
state.user.skills.push('Pinia')

// 중첩 객체도 자동으로 반응형 (Deep Reactivity)
console.log(state.user.skills) // ['Vue', 'TypeScript', 'Pinia']
</script>

<template>
  <p>{{ state.count }}</p>
  <ul>
    <li v-for="skill in state.user.skills" :key="skill">
      {{ skill }}
    </li>
  </ul>
</template>

ref vs reactive — 핵심 차이

항목refreactive
대상모든 타입객체/배열/Map/Set만
접근 방식.value 필요 (script)직접 접근
재할당가능 (ref.value = newObj)불가 (Proxy 참조 소실)
구조 분해.value로 접근** 반응형 소실 위험**
권장 사용대부분의 경우관련 상태 그룹핑 시

reactive의 함정 — 구조 분해와 재할당

TYPESCRIPT
import { reactive } from 'vue'

const state = reactive({ count: 0, name: 'Vue' })

// 구조 분해 — 반응형 소실!
let { count } = state
count++ // 이 변경은 state.count에 반영되지 않음

// 재할당 — Proxy 참조 소실!
// state = reactive({ count: 1, name: 'Vue' }) // 원래 반응형 연결이 끊어짐

이 문제 때문에 Vue 공식 문서에서도 ref를 기본으로 권장 합니다.


computed — 파생 상태

computed는 다른 반응형 데이터로부터 계산된 값을 캐싱합니다.

VUE
<script setup lang="ts">
import { ref, computed } from 'vue'

const items = ref([
  { name: '사과', price: 1000, quantity: 3 },
  { name: '바나나', price: 500, quantity: 5 },
  { name: '오렌지', price: 800, quantity: 2 }
])

// 읽기 전용 computed
const totalPrice = computed(() => {
  // items가 변경될 때만 재계산됨
  return items.value.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  )
})

// 읽기/쓰기 가능한 computed (writable)
const firstName = ref('정훈')
const lastName = ref('심')

const fullName = computed({
  get: () => `${lastName.value}${firstName.value}`,
  set: (newValue: string) => {
    lastName.value = newValue[0]
    firstName.value = newValue.slice(1)
  }
})
</script>

<template>
  <p>총 금액: {{ totalPrice.toLocaleString() }}원</p>
  <p>이름: {{ fullName }}</p>
</template>

computed vs method — 면접 빈출

항목computedmethod
캐싱의존성이 변할 때만 재계산호출할 때마다 실행
사용법{{ total }}{{ getTotal() }}
용도파생 데이터이벤트 핸들러, 사이드 이펙트
VUE
<script setup lang="ts">
import { ref, computed } from 'vue'

const list = ref([1, 2, 3, 4, 5])

// computed — 캐싱됨, list가 변할 때만 재계산
const evenNumbers = computed(() =>
  list.value.filter(n => n % 2 === 0)
)

// method — 매번 재실행
const getEvenNumbers = () =>
  list.value.filter(n => n % 2 === 0)
</script>

<template>
  <!-- computed: 템플릿에서 여러 번 참조해도 한 번만 계산 -->
  <p>{{ evenNumbers }}</p>
  <p>{{ evenNumbers.length }}개</p>

  <!-- method: 참조할 때마다 계산 -->
  <p>{{ getEvenNumbers() }}</p>
  <p>{{ getEvenNumbers().length }}개</p>
</template>

toRef와 toRefs — reactive에서 안전하게 분해

TYPESCRIPT
import { reactive, toRef, toRefs } from 'vue'

const state = reactive({
  count: 0,
  name: 'Vue'
})

// toRef — 단일 속성을 ref로 변환 (원본과 연결 유지)
const countRef = toRef(state, 'count')
countRef.value++ // state.count도 1로 변경됨

// toRefs — 모든 속성을 ref로 변환
const { count, name } = toRefs(state)
count.value++ // state.count도 2로 변경됨

composable 함수에서 reactive 객체를 반환할 때 toRefs로 감싸는 패턴이 일반적입니다.


readonly — 읽기 전용 반응형

TYPESCRIPT
import { ref, readonly } from 'vue'

const original = ref(0)
const readonlyRef = readonly(original)

// readonlyRef.value++ // 경고 발생! 읽기 전용
original.value++ // 원본 변경은 가능, readonlyRef도 업데이트됨

Props처럼 자식 컴포넌트에서 변경하면 안 되는 데이터에 유용합니다.


면접 팁

  • "ref를 기본으로 쓰고, 관련 상태를 그룹핑할 때만 reactive를 고려한다"가 실무 권장 패턴입니다
  • computed의 캐싱 동작을 설명할 때 "의존성 추적"이라는 키워드를 사용하면 좋습니다
  • reactive의 구조 분해 문제와 해결법(toRefs)을 함께 설명하면 심화 이해도를 보여줄 수 있습니다

요약

ref는 모든 타입에 사용할 수 있는 범용 반응형 래퍼이고, reactive는 객체 전용입니다. computed는 의존성 기반 캐싱이 핵심이며, method와의 차이를 명확히 알아야 합니다. reactive의 구조 분해 시 반응형 소실 문제는 toRefs로 해결할 수 있습니다.

댓글 로딩 중...