
[Vue 3] Composition API - ref, reactive
Vue2는 기본적으로 Options API 문법을 기본으로 사용했습니다. Vue3에서부터는 Composition API를 사용하는 것을 권장하고 있습니다. 대체 Composition API가 무엇이며 어떤 장점이 있는지 살펴보고 어떤 것인지 약간 살펴보도록 하겠습니다.
Options API vs Composition API
Option API
Option API는 data, computed를 이용해 변수, 함수, 생명주기 훅 등 구역을 나눠서 로직을 구현합니다.
export default {
data() {
// 컴포넌트 데이터들을 모아놓는 곳
},
methods: {
// 컴포넌트 함수들을 모아 놓은 곳
},
mounted() {
// 컴포넌트가 마운트되었을 때 실행되는 곳
}
}
하지만 로직이 늘어나면 늘어날수록 오히려 가독성이 떨어질 수 있습니다. 예를 들어, 두 가지 로직을 만든다고 해보겠습니다.

서로 다른 로직이지만 data와 methods에 같이 들어가 있는 것을 볼 수 있습니다. 이렇게 되면 코드가 길어질수록 가독성이 떨어지고 유지보수가 어렵게 됩니다. 물론 컴포넌트를 잘 분리해서 이를 완화할 수 있겠지만 한계가 있습니다.
Composition API
로직이 분리되어있는 Options API의 이러한 단점 때문에 리액트와 유사한 Composition API가 등장했습니다. 공식 사이트에서 동일한 컴포넌트의 로직이 어떻게 나뉘어지는지 색상으로 구분해서 보여줍니다.

Composition API의 장점은 이렇게 동일한 로직끼리 묶어서 관리할 수 있다는 것입니다. 로직을 묶어서 관리할 수 있으니 이후에 다룰 Composable 함수로 코드를 분리해서, 더 효율적이고 깔끔하게 재활용할 수 있습니다.
기존의 Options API도 mixins이 있지만 3가지 단점이 있습니다.
이러한 이유로 Vue 3에서부터는 mixin 대신 Composable 사용을 추천하고 있습니다.
Composition API
이제 Composition API이 무엇인지 자세히 살펴볼까요?
setup hook
Composition API를 다루려면 setup hook을 알아야합니다. Composition API에서만 사용하는 생명 주기 훅(Lifecycle Hook)으로, 여기서 컴포넌트가 필요한 모든 데이터, 함수, computed 등이 들어갑니다.
export default {
setup() {
// ...
}
}
Options API와 달리 이 하나의 속성에 모든 로직이 다 들어갑니다. 그리고 여기서 만들어진 데이터나 함수 등을 template에서 사용하려면 반환해줘야 합니다.
<template>
<p>{{ count }}</p>
<button @click="increase">Increase</button>
</template>
<script>
export default {
setup() {
let count = 0;
function increase() {
count += 1;
}
// 반환해서 template에서 사용합니다.
return {
count,
increase
}
}
}
</script>
하지만 실제로 이 구현에서 Button을 누르면 값이 변하지 않습니다. count가 반응형(reactive)이 아니기 때문입니다.
반응형 상태 정의하기
반응형은 Vue가 추적할 수 있다는 뜻입니다. Vue가 추적할 수 있다는 것은 값이 변하는 것을 감지할 수 있고, 그 값에 따라 DOM을 다시 렌더링할 수 있다는 의미이기도 합니다. 반응형 상태를 정의하기 위해서 Vue에서는 두 가지 API를 제공합니다.
reactive
ref
reactive
는 객체나 배열, Map, Set 등의 객체 타입만 적용되고, ref
는 어떤 타입이든 가능합니다.
import { reactive, ref } from "vue";
export default {
setup() {
// reactive
const obj = reactive({ count: 0 });
const array = reactive([1, 2, 3]);
// ref
const count = ref(0);
const bool = ref(true);
const str = ref("Hello World");
const obj2 = ref({ count: 0 });
const array2 = ref([1, 2, 3]);
},
};
왜 굳이 두 가지나 있을까요? 심지어 ref는 어떤 타입이든 가능한데 그냥 ref를 쓰면되지 않을까요?
reactive vs ref
둘 다 Options API의 data처럼 template에서 사용하기 위한, 반응형 데이터를 만들기 위해 존재합니다. 또한 깊은 반응형(Deep Reactivity)이라 중첩된(nested) 값이 바뀌어도 잘 반응합니다.
import { reactive } from 'vue'
const obj = reactive({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 중첩된 값에도 반응하여 DOM을 다시 렌더링합니다.
obj.nested.count++
obj.arr.push('baz')
}
굳이 깊은 반응형이 아닌 얕은 반응형을 사용해야 한다면 shallowReactive
를 사용하면 됩니다.
둘의 차이는 값을 변경할 때 극명하게 나뉩니다.
값을 변경할 때
reactive
로 만든 상태값을 변화시키는건 일반 객체나 배열을 변경시킬 때와 동일합니다.
import { reactive } from "vue";
export default {
setup() {
// 객체 값 변경하기
const obj = reactive({ count: 0 });
obj.count += 1;
// 배열 값 변경하기
const arr = reactive([1,2,3]);
arr.push(4);
}
}
ref
함수에 값을 넣으면 ref 객체를 생성하고 value에 저장합니다. 따라서 값을 변경 시키려면 .value
에 접근해야합니다.
import { ref } from "vue";
export default {
setup() {
// 객체 값 변경하기
const obj = ref({ count: 0 });
obj.value.count += 1;
// 배열 값 변경하기
const arr = ref([1,2,3]);
arr.value.push(4);
}
}
reactive와 동일하게 이 .value
값은 반응형입니다. 특히 객체를 넣어줄 경우 .value
값은 자동으로 reactive로 변환합니다.
이렇게 ref가 value가 있는 객체로 만드는 이유는 reactive가 가진 한계를 극복하기 위해서입니다.
reactive 한계
reactive
는 일반 객체처럼 다루니 좀 더 편해보이긴 하지만 몇 가지 불편한 점들이 있습니다.
// ❌ 원시 타입은 반응형 상태로 만들 수 없음
let bool = reactive(true);
let num = reactive(3);
let str = reactive('Hello');
// ✅ 개체 타입은 반응형 상태로 만들 수 있음
let obj = reactive({ count: 0 });
let arr = reactive([1, 2, 3, 4]);
let state = reactive({ count: 0 });
// 참조하고 있던 state에 새로 할당해서
// 반응형 연결이 끊긴다.
state = reactive({ count: 1 });
// 객체 내부에 접근하는 다음과 같은 경우 연결이 끊김
// 1. local 변수에 넣기
let n = state.count;
// 2. 구조 분해 할당 (destructuring assignment)
let { count } = state;
// 3. 함수에 넣기
someFunction(state.count);
javascript는 원시타입의 경우 참조값이 아닌 있는 값 그대로 복사하기 때문에 state가 아닌 state 안의 count 값을 복사하면 문제가 발생합니다.
ref 등장
ref
는 이러한 reactive의 한계를 극복하기 위해 나온 것으로 위 단점들을 다음과 같은 방식으로 해결할 수 있습니다.
import { ref } from 'vue';
// 원시 타입도 반응형으로 만들기 가능
const count = ref(0);
// state에 대한 참조 자체는 끊기지 않기 때문에
// value 값 자체를 바꿔도 반응형이 유지된다.
let state = ref({ count: 0 });
state.value = { count: 1 };
그냥 reactive에서 객체를 추가해서 value 값에 넣으면 되는 거 아닌가요?
let state = reactive({
value: { count: 0 }
});
네 그렇기도 합니다. 하지만 원시 타입도 반응형으로 만들 수 있다는 점을 이용해 reactive 단점을 해결할 수 있습니다.
// 원시 타입에도 반응형을 만들 수 있기 때문에
// 하위 속성을 접근할 때 다음 방법으로 반응형을 유지한다.
const obj = {
count: ref(1),
};
// 1. local 변수에 넣기
let n = obj.count;
// 2. 구조 분해 할당 (destructuring assignment)
let { count } = obj;
// 3. 함수에 넣기
someFunction(obj.count);
하지만 여전히 ref에서 value
는 좀 다루기 불편하긴 합니다. 그래서 Vue에서는 이러한 문제를 해결하기 위해 노력중인데요. 그 중 하나가 Unwrapping입니다.
ref 자동 Unwrapping
Vue는 객체나 배열 속의 값이 아니라면 template 내에서는 value를 사용하지 않아도 되도록 했습니다.
<template>
<!-- .value를 쓰지 않아도 됩니다. -->
<p>{{ message }}</p>
<!-- .value를 써야합니다. -->
<p>{{ obj.count.value }}</p>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const message = ref("hello");
const obj = {
count: ref(1),
};
return {
message,
obj,
};
},
};
</script>
value를 생략하려면 count
가 반환되도록 하면 됩니다.
<template>
<p>{{ count }}</p>
</template>
<script>
export default {
setup() {
const obj = {
count: ref(1),
};
const { count } = obj;
// ...
return {
count,
};
}
};
</script>
[실험중] 반응형 변환
Vue에서는 .value
의 불편함에서 완전히 벗어나기 위해 컴파일 단계에서 value를 자동으로 추가해서 동작하도록 하는 기능을 실험하고 있습니다.
<script>
export default {
setup() {
let count = $ref(0)
function increment() {
// 컴파일 시 자동으로 .value를 추가해서 계산합니다.
count++
}
}
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
이는 추후에 좀 더 자세히 다뤄보겠습니다.
결국 ref 위주로…
물론 value 쓰는 것이 불편할 수 있지만 결국 추후에는 ref를 사용하기 편하게 바뀔 것으로 보입니다. 그래서 ref만 사용해서 일관적이게 만드는 것이 헷갈리지 않고 유지보수에도 좋지 않을까 싶습니다. (개인적 의견)
<script setup>
왠만해서는 setup에서 만든 것들은 다 template에서 사용할텐데 반환을 늘 해줘야해서 약간 번거롭습니다. 그래서 이런 불편함을 줄이기 위해 script 옆에 setup
을 추가해주면 됩니다.
<script setup>
import { reactive, ref } from 'vue'
// state, obj 모두 template에서 사용 가능
const state = reactive({ count: 0 })
const obj = ref({ count: 0 })
</script>
이 방법은 Single-File Component(SFC)을 사용할 때만 적용됩니다. script setup 내에서 선언된 import와 변수들은 template 내에서도 사용할 수 있습니다. 하지만 함수나 for문 등 하위 스코프에서 선언된 것은 제외입니다.