입사 두달차에 회사 프로젝트 코드를 보던 중 (아직 React가 더 익숙하던 시기) 공식문서에서는 ref, reactive 로 state를 선언할 수 있다던데 회사 코드에서는 reactive가 하나도 보이지 않았다. 일단 React에서는 useState만으로도 상태를 선언하고 다루는데에 불편함을 느껴본 적이 없는데 Vue는 왜 이렇게 구분해놨을까? 뭐가 다르며 언제 뭘 써야할까? 대충 이런 얘기를 잠깐이나마 사수로 계셨던 분과 얘기했던 적이 있다. 지금은 그 대화가 잘 기억이 안나기도하고 사실 당시에는 잘 이해가 되지 않았던 것 같다. 이제서야 Vue 반응형의 원리와 그 분의 큰 그림(?)을 깨달으며 Vue3 (Composition API)를 기준으로 어떻게 반응형을 제공하는지 정리해봤다.
너무나 당연하게도 Vue를 사용하면서 데이터(상태)가 바뀔 때마다 직접 컴포넌트의 DOM을 바꿔주는 코드를 작성할 필요가 없다. Vue가 알아서 자동으로 업데이트를 해주기 때문이다.
그렇다면 Vue는 어떻게 바뀐 데이터를 알아채서 DOM을 업데이트할까?
핵심은 reactivity(반응형)이다.
Vue.js 공식문서를 보다보면 reactivity(반응형, 반응성)에 대한 언급이 정말 많다. 그만큼 reactivity는 Vue가 동작하는 핵심 개념중 하나인데, 내부적으로는 어떻게 동작하는지 궁금해졌다.
대충 proxy 객체를 사용한다 정도로 알고있었는데 자세히 알아보자 !
Reactivity (반응형)이란?
반응형이란, 데이터(상태)가 변경되었을 때 이를 감지하고 반응하여 DOM이 자동으로 업데이트 되는 성질을 말한다.쉽게 말해, 데이터를 변경하면 화면이 변경되는 것이다.
Vue가 제공하는 반응형 API
ref
- 객체 타입은 물론 Primitive Type(number, string, boolean ...)까지 모든 타입에 대한 반응형을 제공한다.
- 내부적으로 value라는 키값에 파라미터를 매핑하는 객체이다.
- reactive 객체를 반환하며, 이 객체 안에는 value라는 속성을 포함하고 있다.
- value값은 ref에서 매개변수로 받은 값을 갖고 있다.
- 이 객체는 내부의 value값에 대한 반응형 참조(reference) 역할을 한다.
reactive
- Object, Array, Map, Set과 같은 타입에 대한 반응형을 제공한다. (기본형 타입에 대한 반응형은 제공하지 않는다.)
- 원본 객체에 대한 Proxy를 제공해서 객체에 대한 반응성을 제공한다.
- .value를 생략(언래핑)한다.
- reactive로 생성한 객체는 내부의 모든 속성이 반응형으로 변환된다.
- 이 객체를 구조 분해할당할 때는 toRefs를 사용해 객체의 모든 속성을 ref로 변환하여 반응성을 유지하면서 구조분해할당하여 사용한다.
ref & reactive
아래 코드부터 보자.
state 변수의 타입을 출력해보면 string으로 나온다.
즉, add함수를 통해 state = state + '!'를 연속적으로 실행시켜도 'Hello World!'만 출력될 것이다.
데이터 자체를 새롭게 할당하기 때문이다.
'Hello World!!'가 출력되기 위해서는 template의 {{state}}와 script의 state가 같은 메모리 주소를 참조해야 하는데, 값 자체가 새롭게 할당되므로 기존의 데이터에 '!'를 추가할 수 없는 것이다.
reactive에서 String과 같은 기본형 타입의 반응형을 유지하기 위해서는 reacitve({value:'Hello World!'})로 선언한 후, state.value로 같은 메모리 주소에 접근해야 한다.
reactive 대신 ref를 사용하는 이유
reactive로 만든 객체에 새로운 객체를 할당하면, 원래의 reactive 객체와의 연결이 끊어진다. 이는 Vue의 반응형 시스템이 제대로 작동하지 않게 만든다.반면, ref를 사용하면 .value를 통해 객체의 값을 변경할 수 있고, ref 객체 자체는 그대로 유지되므로 반응형 상태를 계속 유지할 수 있다.
Vue 반응성의 원리
Proxy
Javascript가 제공하는 Proxy 객체가 있다. 다른 객체의 대리인 역할을 하는 객체이다.
Javascript Object를 만들면 기본적으로 접근자 프로퍼티라고 하는 getter와 setter 함수가 자동으로 포함되어있다. 바로 get(), set() 함수이다. Proxy를 통해 Object의 get() / set() 메소드를 재정의해서 우리가 원하는 동작을 만들 수 있다. 바로 이부분이 Vue 반응성의 핵심이다.
그래서 Vue는 proxy를 어떻게 사용할까?
Vue에서 반응형 상태 객체를 Proxy 객체로 만들어서 값이 바뀌는 동작이 일어나는 set()이 호출되고, set() 핸들러에서 DOM을 업데이트하는 동작을 시킨다.
Proxy를 통해 대상 객체의 set 함수를 Vue에서 만든 반응형 handler로 교체해주는 것이다. 그리고 대상 객체를 직접 조작하는 것이 아닌 Proxy 객체를 통해 상태를 관리한다. Proxy 객체의 값을 변화시키는 코드가 발생하면 set handler에서 DOM을 업데이트시킨다.
📝 그렇다면 반응형 상태값을 바꾸는 작업이 거의 동시에 순차적으로 10번 실행되어야 한다면 어떨까?
10번을 매번 동기식으로 일일히 업데이트해서 렌더링하는 비효율적인 방식으로 동작하지는 않는다.
Vue는 DOM 업데이트를 비동기로 한다.
- 데이터 변경이 발견 될 때마다 큐를 열고 같은 이벤트 루프에서 발생하는 모든 데이터 변경을 버퍼에 담는다.
- 같은 Watcher가 여러번 발생 시 대기열에서 tick 단위로 한 번만 푸시된다.
- 이벤트 루프 “tick”에서 Vue는 대기열을 비우고 실제 (이미 중복 제거 된) 작업을 수행한다.
Vue에서 반응형 상태 객체를 Proxy 객체로 만들어서 값이 바뀌는 동작이 일어나는 set()이 호출되고, set() 핸들러에서 DOM을 업데이트하는 동작을 시킨다.
코드로 보자
Vue core repository의 reactive.ts 파일을 확인해보면, 아래와 같은 reactive 객체를 만드는 함수를 찾아볼 수 있다.
이 함수에서 바로 Proxy 객체를 생성하고 proxyMap을 통해 모든 반응형 객체를 관리하고 있다.
정리
ref나 reactive와 같은 Vue의 반응형 API를 사용하면 변수에 reactivity가 주입되어 해당 변수에 대해서 변경 내용을 추적하고 변화를 바탕으로 Proxy 객체가 반응해서 UI를 변경한다.