반응형 상태의 두 기둥: ref와 reactive
Vue 3에서 변수 값이 변경될 때 화면이 자동으로 갱신(반응형, Reactivity)되게 하려면 ref 또는 reactive 를 사용해야 합니다.
둘은 역할이 비슷해 보이지만 담을 수 있는 데이터 타입과 접근 방식(.value의 유무)에서 치명적인 차이를 가집니다. 실무에서는 보통 원시값과 객체를 모두 아우를 수 있는 ref를 만능 상자 처럼 더 선호하는 경향이 있습니다.
📦 ref() vs reactive() 비교
ref() (추천! 👍)
문자열, 숫자, 배열, 객체 (모두 가능)
.value
0
count.value = 1
단점: .value 를 꼭 붙여야 함
reactive()
객체(Object) 전용 (문자, 숫자 불가)
Proxy Object
{ age: 25 }
user.age = 26
단점: 구조 분해 할당 시 반응성 파괴
특징
ref() (추천)
reactive()
허용 타입
원시값(String, Number), 객체, 배열 등 모든 타입
오직 객체(Object), 배열, Map, Set 만 가능
스크립트 접근
반드시 .value를 붙여야 함 (예: count.value)
.value 없이 프로퍼티에 직접 접근 (예: user.name)
재할당 안정성
전체 객체를 통째로 바꿔치기(재할당) 해도 반응성 유지됨
객체를 통째로 교체하면 반응성이 끊어짐 (치명적 단점)
💡 초보자를 위한 실전 요약: 언제 뭘 써야 할지 헷갈리신다구요? 무조건 ref()만 쓰시면 됩니다! Vue 공식 문서에서도 초보자에게는 모든 데이터 타입에 일관성 있게 사용할 수 있는 ref의 사용을 강력히 권장하고 있습니다.
<!-- ========================================== -->
<!-- 📂 StateManagement.vue (ref와 reactive 비교 예제) -->
<!-- ========================================== -->
<script setup>
import { ref, reactive } from 'vue';
// ------------------------------------------------
// 1. ref()의 사용 (강력 추천!)
// 원시값(숫자, 문자)부터 객체, 배열까지 다 담을 수 있는 만능 상자.
// ------------------------------------------------
const message = ref('안녕하세요!'); // 문자열
const counter = ref(0); // 숫자
const updateRef = () => {
// 스크립트 내부에서 값을 읽거나 바꿀 때는 상자 뚜껑을 열어주는 '.value'를 반드시 적어야 합니다.
message.value = '반갑습니다! (ref)';
counter.value++;
console.log('✅ ref 업데이트됨:', message.value, counter.value);
};
// ------------------------------------------------
// 2. reactive()의 사용
// 오직 "객체(Object)나 배열"만 담을 수 있습니다. .value가 필요 없는 대신 제약이 많습니다.
// ------------------------------------------------
let user = reactive({
name: '김민수',
age: 20
});
const updateReactive = () => {
// .value 없이 속성에 직접 접근합니다.
user.age++;
console.log('✅ reactive 업데이트됨:', user.age);
};
const breakReactive = () => {
// 🚨 [주의] 아래처럼 통째로 갈아끼우면 반응성(화면 갱신)이 영원히 박살납니다!
console.warn('🚨 경고: user 객체를 통째로 교체합니다. 이제 반응성이 끊어집니다!');
user = { name: '이영희', age: 30 };
};
</script>
<template>
<div class="flex-container">
<!-- ref 템플릿 -->
<div class="card ref-card">
<h2>ref()</h2>
<p>메시지: <strong>{{ message }}</strong></p>
<p class="counter">{{ counter }}</p>
<button @click="updateRef">ref 업데이트</button>
</div>
<!-- reactive 템플릿 -->
<div class="card reactive-card">
<h2>reactive()</h2>
<p>이름: <strong>{{ user.name }}</strong></p>
<p class="counter">{{ user.age }}세</p>
<button @click="updateReactive">나이 먹기 (+1)</button>
<button class="btn-danger" @click="breakReactive">
🚨 통째로 교체 (반응성 파괴)
</button>
</div>
</div>
</template>
<style scoped>
.flex-container { display: flex; gap: 2rem; max-width: 800px; margin: 2rem auto; }
.card { flex: 1; padding: 2rem; border-radius: 12px; }
.ref-card { border: 2px solid #3b82f6; background: #eff6ff; }
.ref-card h2 { color: #1d4ed8; font-weight: 800; }
.ref-card .counter { font-size: 2.5rem; font-weight: 900; color: #2563eb; }
.ref-card button { background: #3b82f6; color: white; width: 100%; padding: 0.75rem; border-radius: 8px; border: none; font-weight: bold; cursor: pointer; }
.reactive-card { border: 2px solid #ec4899; background: #fdf2f8; }
.reactive-card h2 { color: #be185d; font-weight: 800; }
.reactive-card .counter { font-size: 2.5rem; font-weight: 900; color: #db2777; }
.reactive-card button { background: #ec4899; color: white; width: 100%; padding: 0.75rem; border-radius: 8px; border: none; font-weight: bold; cursor: pointer; margin-bottom: 0.5rem; }
.reactive-card .btn-danger { background: #475569; }
</style>