Vue의 핵심 기능 중에 하나가 바로 반응성(Reactivity)이다. 반응성이란 상태(State) 변경을 추적하여 DOM을 자동으로 업데이트시켜주는 기능이다. 반응성을 어떻게 활용하는지를 설명하고, 어떤 원리로 이게 가능한지를 설명하겠다.
반응성(Reactivity)
반응성이란 컴포넌트의 상태(State)가 바뀌었을때 자동으로 컴포넌트의 DOM을 변경해주는 것이다. 상태(State)란 쉽게 말해 컴포넌트를 그리는데 연관된 데이터이다. 이 상태 데이터가 변경되면 우리는 컴포넌트의 모습도 바꾸어 줘야 한다. Vue에서는 상태가 바뀔때마다 컴포넌트의 DOM을 우리가 일일이 바꾸어주는 코드를 짤 필요가 없다. Vue가 알아서 자동으로 업데이트를 해주기 때문이다. 이것들 반응성(Reactivity)이라고 하는 것이다. Vue의 중요한 핵심기능으로써 반드시 Vue를 하려면 반드시 알아야 하는 개념이다.
Vue는 결국 모든 것이 컴포넌트를 만드는 일이다. 컴포넌트의 상태와 어떻게 렌더링될지만 선언하고 나서, 상태가 바뀌는 로직만 관리해주면 렌더링은 신경쓸 필요가 없다. Vue는 이렇게 반응성을 이용하여 컴포넌트를 하나하나 만들어 나가면 되는 것이다.
그럼 반응성을 어떻게 구현하는지 쉬운 예제를 활용해서 알아보자. Options API 와 Composition API 2가지 API 스타일을 모두 설명할 것이다. SFC 형태로 코드를 만들었다.
반응형 상태 사용법 (Options API)
컴포넌트의 반응형 상태를 선언해서 사용해보는 코드를 만들어 보자.
아래 코드는 화면에 버튼이 하나 표시되고 버튼안에는 숫자가 표시된다. 처음에 0으로 시작해서 버튼을 클릭할때마다 1씩 증가하는 코드이다.
<script>
export default {
// data: 반응형 상태를 정의
data () {
return {
count: 0
}
},
// methods 선언
methods: {
increment () {
this.count++
}
}
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
아래는 실행화면이다.
See the Pen VueReactivityOptionAPI by JH (@JH-the-reactor) on CodePen.
컴포넌트의 data 옵션을 사용하여 반응형 상태를 선언한다. 그러면 컴포넌트의 인스턴스가 생성될때 data () 가 호출이 되고, data 함수에서 return 하는 객체가 컴포넌트의 반응형 상태가 된다. 그리고 이 값은 컴포넌트 인스턴스에서 this를 통해 접근이 가능하다.
※ 주의
data () 함수에서는 항상 return {} 처럼 객체를 반환하는 함수가 되도록 선언하자. 그렇지 않으면 반응형이 동작하지 않는다.
methods
컴포넌트의 methods를 선언하기 위해서는 methods 옵션에 추가해주면 된다. 주로 상태값을 다루는 함수를 만들때 methods에 추가해서 사용한다.
결국 data와 methods 옵션을 통해 컴포넌트의 상태를 정의하고 상태를 변경하는 함수를 구현하면 되는 것이다.
반응형 상태 사용법 (Composition API)
이번에는 Composition API를 사용해서 반응형 상태를 사용해보자.
반응형 상태를 만들 수 있는 대표적인 2가지 함수를 알아보자. 바로 ref와 reactive 함수이다.
ref 함수
<script setup>
import { ref } from 'vue'
// 반응형 상태 정의
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
동작은 위에 예제와 동일하다. 버튼을 누르면 숫자가 1씩 증가한다.
ref 함수가 바로 반응형 상태를 만들어 주는 역할을 한다. ref 함수를 호출하면 .value 속성을 가지는 객체를 리턴한다. 이 객체가 바로 반응형 상태가 자동으로 동작하게 해주는 역할을 해주는 객체이다.
methods에서는 .value 프로퍼티로 접근을 해야하지만, template에서 접근할 때에는 .value를 붙일 필요가 없다.
ref 함수의 인자로는 모든 타입이 올 수 있다.
reactive 함수
<script setup>
import { reactive } from 'vue'
// 반응형 상태 정의
const state = reactive({ count: 0 })
function increment() {
state.count++
}
</script>
<template>
<div>Counter Page</div>
<button @click="increment">{{ state.count }}</button>
</template>
ref와 사용법은 똑같지만 차이점은 reactive 함수의 인자로는 기본타입(string, number, boolean)이 올 수 없고, object 타입이 와야 한다.
reactive 인자 타입
- objects
- arrays
- collections (Map, Set)
ref 함수도 object 타입에 대해서 내부적으로 reactive를 사용한다. 따라서 ref만 사용해도 된다. 대신 사용성에서 차이가 조금 있다.
ref vs reactive
ref와 reactive를 사용하여 object를 반응형으로 만들면 아래와 같이 객체의 접근법이 달라진다.
<script setup>
import { ref, reactive } from 'vue'
const state1 = ref({ count: 0 })
const state2 = reactive({ count: 0 })
// ref로 만든 반응형 객체
function increment1() {
state1.value.count++
}
// reactive로 만든 반응형 객체
function increment2() {
state2.count++
}
</script>
<template>
<button @click="increment1">{{ state1.count }}</button>
<button @click="increment2">{{ state2.count }}</button>
</template>
ref를 사용하여 만든 반응형 객체의 속성에 접근하려면 .value를 통해 접근해야 한다.
하지만 reactive를 사용한다면 state.count 처럼 바로 접근이 가능하다.
ref만 사용할지 ref와 reactive 모두를 사용할지는 사용자의 선택에 달려있다.
반응성의 원리
Options API의 data() 옵션과 Composition API의 ref(), reactive()를 사용하면 반응형 상태를 만들 수 있고, 상태 값이 바뀌면 DOM이 자동으로 업데이트 된다는 것까지는 이제 알았다.
그런데 이게 어떻게 자동으로 업데이트가 되는 것일까?? 단순히 상태 값만 바꾸는 코드만 넣었는데 DOM이 자동으로 바껴서 렌더링까지 되는게 어떻게 가능한 것일까?
이것을 이해하기 위해 JavaScript의 Proxy라는 개념을 알면 된다.
data() 옵션이 만드는 것과 reactive() 함수가 만드는 것은 결국 JavaScript Proxy 이기 때문이다.
JavaScript Proxy
자바스크립트에서 Proxy라는 객체를 지원한다. Proxy는 다른 객체의 대리인 역할을 하는 객체이다.
그냥 쉽게 말해 해당 객체의 get(), set()과 같은 오브젝트의 기본 프로퍼티 메소드를 가로채서 재정의할 수 있도록 해주는 객체이다.
Proxy는 아래와 같이 정의한다.
new Proxy(target, handler)
- target: Proxy로 만들 대상이 되는 Object
- handler: Object의 기본 프로퍼티 메소드의 동작을 재정의하는 함수를 가지는 객체
아래 예제를 보면 더 쉽게 이해가 될 것이다.
// target object
const obj = { count: 0 }
// handler object
const handler = {
set (obj, prop, value) {
value = value + 1
return Reflect.set(...arguments);
}
}
// Proxy 생성
const p = new Proxy(obj, handler)
p.count = 10
console.log(p.count) // 11
자바스크립트 Object를 만들면 기본적으로 접근자 프로퍼티라고 해서 getter와 setter 함수가 자동으로 포함된다. 바로 get(), set() 함수이다. 바로 Proxy를 통해 Object의 get() / set() 메소드를 재정의해서 우리가 원하는 동작을 만들 수 있다. 바로 이부분이 Vue 반응성의 핵심이다.
Vue 반응형 상태 원리
위에서 반응형 상태를 만드는 방법을 3가지 설명하였다.
- Options API - data () 함수
- Composition API - ref ()함수
- Composition API - reactive ()함수
여기에서 data () 함수와 reactive() 함수는 결국 JavaScript의 Proxy를 사용하는 것이다. ref()도 내부적으로 타입에 따라 나뉘긴 하지만 결국은 Proxy를 사용하거나 Proxy와 동일하게 동작한다. Proxy는 Object에 대해서만 동작하므로 문자열이나 숫자형 같은 기본타입에 대해서는 Proxy와 동일하게 동작하도록 별도로 구현을 해주었다.
Vue에서 반응형 상태 객체를 Proxy 객체로 만들어서 값이 바뀌는 동작이 일어나는 set()이 호출되고, set() 핸들러에서 DOM을 업데이트하는 동작을 시키는 것이다. 핵심 개념은 이렇게 아주 간단하다.
Proxy를 통해 대상 객체의 set 함수를 Vue에서 만든 반응형 handler로 교체해주는 것이다. 그리고 대상 객체를 직접 조작하는 것이 아닌 Proxy 객체를 통해 상태 관리를 하도록 한다. Proxy 객체의 값을 변화시키는 코드가 발생하면 set 핸들러에서 DOM을 업데이트시키도록 하는 것이다.
반응형 상태값을 바꾸는 작업이 거의 동시에 순차적으로 10번이 있다고 하면, 10번을 매번 동기식으로 각각 업데이트해서 렌더링을 하는 비효율적인 방식으로 동작하지는 않는다.
Vue에서는 DOM 업데이트 작업을 적절히 스케쥴링하여 버퍼에 모은 다음에 tick 단위로 한번에 처리한다. 반응형 상태를 변경하는 모든 코드마다 매번 동기식으로 DOM을 업데이트하면 매우 비효율적일 수가 있기 때문이다.
이것을 간단히 도식화하면 아래 그림과 같다.
Vuejs github 코드 분석
아무래도 실제 개발된 코드의 실체를 확인하면 이론적으로만 보던게 실체화되서 지식에 확신이 생기기 때문에 github 코드를 분석해보았다.
vuejs/core 프로젝트의 github 소스코드를 살펴보면 아래와 같은 reactive 객체를 만드는 함수가 있다.
이 함수에서 바로 Proxy 객체를 생성하고 proxyMap을 통해 모든 반응형 객체를 관리하고 있다.
vuejs / core/ package / reactivity / src / reactive.ts
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// ... 중간 생략 ...
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
정리하며
이번 포스팅에서는 Vue의 가장 핵심이자 필수개념인 반응성(Reactivity)에 대해 기본 사용법을 정리해보았다. 그리고 이게 어떻게 가능한지 궁금해할 수도 있을 것 같아서 반응성의 원리에 대해 알아보았고, 그것이 바로 JavaScript의 Proxy 라는 것을 알 수 있었다. 원리를 알고 사용하는 것과 그냥 사용법만 익히고 사용하는 것은 어떤 문제가 닥쳤을때 문제를 바라보는 시각의 깊이가 달라질 수 있으므로, 기회가 된다면 원리를 알고 넘어가도록 하자.
Proxy에 대해 좀 더 설명이 필요하면 아래의 문서를 참고한다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Proxy
'개발' 카테고리의 다른 글
Nuxt3에서 Quasar 사용하기 (2) | 2023.06.23 |
---|---|
Nuxt 3 에서 더이상 Moment.js 말고 Day.js 사용하자 (1) | 2023.06.13 |
Vue Multiple Layout (다중 레이아웃) 적용하기 (2) | 2023.06.07 |
[Vue + Vite] 근데 Vite는 뭔가요? (2) | 2023.06.06 |
Electron 으로 데스크탑 어플리케이션 만들기 기초 (1) | 2023.06.01 |
댓글