← Back to list

[Vue3] 반응형(Reactive)상태 선언 딥다이브: ref와 reactive의 모든 것

Vue 3의 reactive는 객체 재할당이나 구조 분해 시 화면 갱신(반응성)을 잃어버리는 치명적인 단점이 있습니다. 따라서 실무에서는 버그 예방과 코드 일관성을 위해 모든 변수를 안전한 ref로 통일하여 사용하는 것을 권장합니다.

야옹캬옹 in HYEONA SONG · 2026-02-25 07:10 · 0 claps · 11.0 min read
#vue-3 #vuejs #reactive #ref
Open on Medium ↗
Wiki topics: 🌐 · Web Development

[Vue3] 반응형(Reactive)상태 선언 딥다이브: ref와 reactive의 모든 것

📌 1. 반응형(Reactive)이란?

반응형이란, 데이터가 변경되었을 때 이를 감지하고 이에 반응하여 DOM이 자동으로 업데이트 되는 성질을 말한다. 즉, 데이터를 변경하면 화면이 변경되는 것이다.

ㅤㅤ

쉬운 이해를 위해 반응형 상태를 생성하는 코드를 살펴보자.

<template>
  <div>
    <button @click="increment"> Click {{state.count}} </button>
  </div>
</template>
<script>
import { reactive } from 'vue'
export default {
  setup() {
    const state = reactive({ count: 0 })
 const increment = () =>{
     state.count++;
    }
    return {
      state, increment
    }
  }
}
</script>

버튼을 클릭하면, state.count의 값이 +1로 증가하며, 데이터 변경을 감지하여 새롭게 바뀐 데이터값이 화면에 자동으로 업데이트 된다.

여기서 중요한 건 ‘화면에 자동으로’이다.

일반 변수는 버튼을 백 번 눌러도 화면에는 여전히 0이다. 메모리 상에서는 값이 바뀌었지만 이걸 HTML이 전혀 모르기 때문이다.

이와 달리 반응형 변수는 버튼을 누를 때마다 화면의 숫자가 1, 2, 3… 실시간으로 휙휙 바뀐다. 변경이 감지되는 순간 Vue 엔진에게 “야! 값 바뀌었어! 빨리 화면 다시 그려!”라고 명령을 내리기 때문이다.

즉, 변경된 데이터 값을 렌더링 해주는 작업을 하지 않아도 되는 것이다. 이것을 ‘반응형 상태’라고 부른다.

📌 2. 반응형 상태의 선언: reactive, ref

반응형 상태를 선언하기 위한 방법에는 reactive, ref가 있다.

Vue 3의 Composition API 환경으로 넘어오면서 개발자들이 가장 먼저, 그리고 가장 많이 헷갈려하는 개념이 바로 반응형 상태를 선언하는 두 가지 방법인 ref와 reactive의 차이이다.

Vue 공식 문서를 보면 단순하게 “원시값은 ref, 객체는 reactive를 쓰세요”라고 안내하고 있지만, 막상 실무에서 코드를 짜다 보면 “이 배열은 도대체 뭘로 선언해야 하지?”, “데이터를 받아왔는데 왜 화면이 갱신 안 되지?” 하며 삽질하는 상황을 수없이 마주하게 된다.

이 포스팅에서는 두 방식의 기본적인 차이를 넘어, 실무에서 마주치는 치명적인 함정(구조 분해 할당, 데이터 재할당 문제)과 어떤 것을 선택해야 하는지 포스팅 했다.

ㅤㅤ

2–1. ref : 만능이지만 .value가 꼬리표처럼 붙는 녀석

  • 객체 타입은 물론 기본형 타입 까지 모든 타입에 대한 반응성을 제공.
  • 내부적으로 value라는 키값에 파라미터를 매핑하는 객체.
  • .value로 접근

ref는 기본형 타입을 반응형 상태로 선언하는데 사용 가능하다.

자바스크립트에서 기본형 타입은 참조가 아니라 값 자체가 복사되어 전달되기 때문에, Vue는 이 기본형 타입의 값을 추적하기 위해 내부적으로 하나의 객체로 한 번 감싸버린다.

그러므로 ref는 변이 가능한(반응하는) 객체를 반환하는데, 이 객체 안에는 value라는 단 하나의 속성만 포함한다. value값은 ref() 메서드에서 매개변수로 받은 값을 갖고 있다. 즉, 이 객체는 내부의 value 값에 대한 반응형 참조(reference) 역할을 한다.

그래서 우리가 선언한 값에 접근하거나 값을 변경하려면 반드시 .value 라는 속성을 통해서 접근해야만 한다.

<script>
const count = ref(0)
count.value++
console.log(count.value) // 1
</script>

참고로 ref는 기본타입뿐만 아니라 객체나 배열을 담을 수도 있다.

만약 ref에 객체를 넣으면, Vue는 내부적으로 그 객체를 reactive로 한 번 더 변환해서 처리한다. 즉, ref는 사실상 모든 타입의 데이터를 다 담을 수 있는 만능 주머니인 셈이다.(우리도 후에는 전부 ref로 바꿈)

ㅤㅤ

2–2. reactive: 객체 전용, .value가 없어서 깔끔한 녀석

  • 원본 객체에 대한 Proxy를 제공해서 객체에 대한 반응성을 제공.
  • Object, Array, Map, Set과 같은 타입에 사용.
  • .value를 생략(언래핑)

ㅤㅤ

reactive는 객체나 배열타입의 반응형 상태를 선언하는데 사용된다.

오직 참조 타입 데이터만 반응형으로 만들 수 있다. 내부적으로 자바스크립트의 Proxy 객체를 사용하여 구현되어 있기 때문에, 객체의 속성이 추가되거나 변경되는 것을 깊은 수준까지 완벽하게 감지해 낸다.

가장 큰 장점은 ref처럼 귀찮은 .value를 붙이지 않아도 된다는 것이다. 그냥 일반적인 자바스크립트 객체를 다루듯이 아주 직관적으로 코드를 작성할 수 있다.

<script>
const state = reactive({
    count: 0
})
console.log(state.count); // 0출력
</script>

다만, reactive는 기본형(number, string..)타입의 반응형 상태를 유지하지 못한다. 이것이 ref와 가장 큰 차이점이다.

<script>
let state = reactive('Hello World!');
const add = () => {
    state = state + '!';
}
console.log(typeof state); //string 
</script>

state 변수의 타입을 출력해보면 string으로 나온다. 즉, add함수를 통해 state = state + ‘!’를 연속적으로 실행시켜도 ‘Hello World!’만 출력될 것이다. 데이터 자체를 새롭게 할당하기 때문이다.

‘Hello World!!’가 출력되기 위해서는 template의 {{state}}와 script의 state가 같은 메모리 주소를 참조해야 하는데, 값 자체가 새롭게 할당되므로 기존의 데이터에 ‘!’를 추가할 수 없는 것이다.

reactive에서 String과 같은 기본형 타입의 반응형을 유지하기 위해서는 reacitve({value:’Hello World!’})로 선언한 후, state.value로 같은 메모리 주소에 접근해야 한다.

📌 3. 우리가 ref로 통일한 이유 — 실무에서 마주치는 reactive의 치명적 단점

이 부분이 이 포스팅의 핵심이다. Vue로 전환 작업을 진행하고 있는 개발자들에게 꼭 해주고 싶은 이야기이다.(그대들의 귀한 시간에 삽질하지 않도록…)

Vue 3 초기에는 “기본형 타입은 ref, 객체는 reactive를 쓰자”는 방식이 정석처럼 여겨졌다. 우리도 이 정석을 선택했다.

vue로 전환하며 초기에는 기본타입/참조타입을 나누어 reactive와 ref를 잘 나눠쓰려고 노력했으나…… 이제는 ref로 전면 통일하여 사용하고 있다.

그 이유는 다음과 같다.

① 공통 개발 가이드를 배포해도 기준 없이 혼용되기 시작함.

② reactive 반응형이 깨지는 버그 이슈 발생

우선 개발자들 사이에서 데이터를 다룰 때마다 “이건 ref니까 .value를 붙이고, 저건 reactive니까 그냥 쓰고”를 고민하는 피로도가 상당히 높았다. 유지보수를 할 때도 신경쓰이는 부분이 많았고, 추후에는 기준없이 혼용되며 개발되는 사태가 벌어졌다.

또 reactive의 재할당 문제와 구조 분해 할당 시 발생하는 예기치 못한 반응성 상실 문제 때문에 “무조건 고민하지 말고 ref만 사용하자.”로 바꾸었다.

객체든 배열이든 문자열이든 묻지도 따지지도 않고 전부 ref로 통일해서 선언하는 것이다. .value를 붙이는 것이 조금 귀찮을 수는 있지만, 재할당 시 반응성이 깨질 걱정을 전혀 안 해도 되고 코드의 일관성이 생겨서 협업할 때 훨씬 쾌적해지기 때문이다.

살펴보니 최근 Vue 커뮤니티와 많은 실무에서도 ref를 주로 쓴다더라. Vue 공식 문서에서도 ref 하나만 사용하는 것을 아주 좋은 대안으로 소개하고 있다.

물론 정답은 없다. 본인 혹은 팀과 합의하여 선택하되, 다음과 같은 이슈는 협의할 때 반드시 체크하길 바란다. 다음은 내가 ref로 통일하자고 밀어 붙이며 작성했던 근거들이다.

reactive 이슈 1. 변수 전체를 재할당하면 반응성을 잃어버린다.

API 통신을 통해 서버에서 새로운 배열이나 객체 데이터를 받아와서 기존 변수에 통째로 덮어씌울 때가 많다. 이때 reactive로 선언된 변수에 새로운 객체를 통째로 할당해 버리면, 기존에 Vue가 걸어둔 반응형 연결 고리(Proxy)가 아예 끊어져 버린다.

import { reactive, ref } from 'vue';

let state = reactive({ count: 0 });
//새로운 객체를 덮어씌우면 반응성이 완전히 깨진다. 화면이 갱신되지 않음.
state = { count: 10 }; 
let list = reactive([]);
// 서버에서 받은 배열로 통째로 갈아끼워도 반응성이 깨진다.
list = [1, 2, 3];

const list = ref([]);
// ref는 .value에 통째로 덮어씌워도 반응성이 유지된다.
list.value = [1, 2, 3];

reactive를 유지하면서 배열을 업데이트하려면 list.push처럼 기존 배열에 요소를 밀어 넣거나, 객체 안에 속성으로 배열을 선언해야 한다.

하지만 이런 제약이 너무 불편하기 때문에, 실무에서는 통째로 데이터를 갈아끼워야 하는 API 응답 데이터 등은 애초에 ref로 선언하는 것이 훨씬 안전하다.

ref는 .value에 새로운 객체를 덮어씌워도 반응성이 그대로 유지되기 때문이다.

reactive 이슈 2. 구조 분해 할당(Destructuring)을 하면 반응성을 잃어버린다.

reactive 객체의 속성이 너무 많으면, 코드를 짧게 쓰기 위해 ES6 문법인 구조 분해 할당을 자주 사용하게 된다. 하지만 이 행위 역시 반응성을 파괴하는 주범이다. 기본형 타입속성들이 객체에서 떨어져 나오는 순간 일반적인 변수가 되어버리기 때문이다.

import { reactive, toRefs } from 'vue';
const user = reactive({
  name: '홍길동',
  age: 25
});
// 이렇게 구조 분해 할당을 하는 순간 name과 age는 일반 문자열/숫자가 되어버림.
let { name, age } = user;

const user = reactive({ name: '홍길동', age: 25 });
// toRefs를 거치면 반응성을 유지한 채로 분해할 수 있다.
let { name, age } = toRefs(user);

이때는 Vue에서 제공하는 toRefs 라는 유틸리티 함수를 써야 한다. toRefs는 객체 안의 속성 하나하나를 모두 ref로 변환해서 떼어내 주기 때문에, 구조 분해 할당을 해도 반응성이 그대로 살아있게 된다.

📌 4. 반응형 상태의 원리 — Proxy

반응형 상태는 어떻게 변경된 데이터를 감지하고 반응하여 업데이트 하는 것일까? 바로, Proxy를 통해서이다.

Proxy는 특정 객체를 감싸 프로퍼티 읽기, 쓰기와 같은 객체에 가해지는 작업을 중간에서 가로채는 객체로, 가로채진 작업은 Proxy 자체에서 처리되기도 하고, 원래 객체가 처리하도록 그대로 전달되기도 한다.

ㅤㅤ

자바스크립트 Object를 만들면 기본적으로 접근자 프로퍼티라고 해서 getter와 setter 함수가 자동으로 포함된다. 바로 get(), set() 함수이다. Proxy를 통해 Object의 get() / set() 메소드를 재정의해서 우리가 원하는 동작을 만들 수 있다. 바로 이부분이 Vue 반응성의 핵심이다.

즉, Vue에서 반응형 상태 객체를 Proxy 객체로 만들어서 값이 바뀌는 동작이 일어나는 set()이 호출되고, set() 핸들러에서 DOM을 업데이트하는 동작을 시키는 것이다. 핵심 개념은 이렇게 아주 간단하다.

다음 그림을 통해 과정을 살펴보자.

  1. getter와 setter는 속성에 접근(get)하거나 수정(set)할 때, Vue가 종속성 추적 또는 변경 알림을 수행할수 있다.
  2. 모든 컴포넌트 인스턴스에 해당 watcher 인스턴스가 있으며, 이 인스턴스는 컴포넌트가 종속적으로 렌더링 되는 동안 수정된 모든 것을 기록한다.
  3. 나중에 종속적인 setter가 트리거가 되면 watcher에 전달하고 컴포넌트가 다시 렌더링 된다.

ㅤㅤ

💡 여기에서, 반응형 데이터는 모든 변경사항을 적용 시킬까? 그렇지 않다.

Vue는 DOM 업데이트를 비동기로 한다.

  1. 데이터 변경이 발견 될 때마다 큐를 열고 같은 이벤트 루프에서 발생하는 모든 데이터 변경을 버퍼에 담는다.
  2. 같은 Watcher가 여러번 발생 시 대기열에서 한 번만 푸시된다.
  3. 이벤트 루프 “tick”에서 Vue는 대기열을 비우고 실제 (이미 중복 제거 된) 작업을 수행한다.

ㅤㅤㅤ

🐱 한 줄

ㅤㅤ

우리는 Nexacro에서 Vue3로 전환 작업 중에 있다.

바로 반응형 웹을 위해서다. 반응형을 원한 것은 고객이다. 앞으로도 더 많은 영역에서 반응형 웹에 대한 요구가 많아질 것으로 예상한다.

Vue 전환 작업에 있어 팀 내부에서 어떤 고민과 좌절(..)을 겪었는지 어떤 근거로 어떤 선택을 했는지 지속적으로 포스팅을 남길 예정이다.

ㅤㅤ

전환 작업을 하며 참 많은 분들의 글과 경험들에서 도움을 받았다.

반응형의 개념과 ref와 reactive에 대한 차이를 고민하는 개발자, 전환 작업을 시작하는 개발자들 모두에게 이 포스팅이 도움이 되길 바란다.

ㅤㅤㅤ

ㅤㅤㅤ

참고문헌

https://v2.ko.vuejs.org/v2/guide/reactivity.html


메타데이터
post_id
82ea6362dffb
slug
nexacro-vue3-전환기-반응형-reactive-상태-선언-딥다이브-ref와-reactive의-모든-것-82ea6362dffb
url
https://medium.com/dancingcat/nexacro-vue3-%EC%A0%84%ED%99%98%EA%B8%B0-%EB%B0%98%EC%9D%91%ED%98%95-reactive-%EC%83%81%ED%83%9C-%EC%84%A0%EC%96%B8-%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C-ref%EC%99%80-reactive%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-82ea6362dffb
canonical_url
https://medium.com/dancingcat/nexacro-vue3-%EC%A0%84%ED%99%98%EA%B8%B0-%EB%B0%98%EC%9D%91%ED%98%95-reactive-%EC%83%81%ED%83%9C-%EC%84%A0%EC%96%B8-%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C-ref%EC%99%80-reactive%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-82ea6362dffb
author_url
https://medium.com/@falling_star3
status
ok
fetched_at
2026-06-26 12:24:55