현재 인턴으로 근무하고 있는 회사에서 Vue.js를 사용하고 있어서 Vue.js (version 3) 공부를 시작하게 되었다. 처음엔 Vue.js의 문법이 React와 일대일 대응이 되는 듯했지만, 좀 더 깊게 들어가면 React와 다르게 구현된 부분이 있고, 이로 인해 Vue.js에서만 사용할 수 있는 요소도 있었다. 아직 두 프레임워크에 대한 이해가 완벽하진 않지만, 약 한 달간 Vue.js를 다뤄보면서 느꼈던 주요 차이점들을 기술해보았다.
우선 두 프레임워크 모두 Virtual DOM을 활용한다는 것은 동일하다. Vue.js 공식문서에 따르면, virtual DOM이란 개념은 React에서 처음 등장했고 Vue.js를 포함한 다른 프레임워크들이 따라서 적용했다고 한다.
Virtual DOM 이란?
The virtual DOM (VDOM) is a programming concept where an ideal, or “virtual”, representation of a UI is kept in memory and synced with the “real” DOM.
가상돔은 이상적인, 혹은 가상적인 UI에 대한 표현을 메모리에 저장하고 실제 DOM과 동기화하는 프로그래밍 개념이다.
Compile
Vue templates가 virtual DOM tree를 반환하는 render function으로 컴파일된다. 이 단계는 build 단계에서 수행될 수도 있고, runtime compiler에서 수행될 수도 있다.
Mount
Runtime renderer가 render functions을 실행하여 virtual DOM tree를 만들고, 이로부터 실제 DOM nodes를 생성한다. 각 component가 의존하고 있는 reactive dependencies가 관리되고 추적되며, 해당 상태가 변화하면 그 결과로 mount가 수행된다.
Patch
Mount 시 결정된 의존성(dependency)가 변화하였을 경우, re-render가 발생한다. 이 때 업데이트 된 virtual DOM tree가 생성되고, runtime renderer는 이 tree를 이전 버전과 비교하여 변화한 부분을 실제 DOM에 반영한다.
React와 Vue.js 모두 상태의 변화를 추적하고, 상태가 변화함에 따라 리렌더링이 발생하여 virtual DOM이 업데이트 되며, virtual DOM tree를 그 이전 상태와 비교하여 실제 DOM을 업데이트 하는 것은 동일하다. 하지만 상태의 변화를 추적하는 방식이 서로 다르다.
React는 본질적으로 상태의 변화를 watch하지 않는다. 그 대신 setState 함수를 호출하면 해당 컴포넌트와 그 children가 새로운 상태와 함께 re-render 되어야 함을 알려준다.
setState
enqueues changes to the component state.
즉, setState는 값을 즉각적으로 업데이트하지 않는다. 이는 React의 batching과도 연관이 있는데, 특정 이벤트로 인해 발생하는 각기 다른 상태들의 업데이트를 한 번에 묶어서 업데이트 하는 것을 말한다. (위에서 enqueues changes라고 한 것도 그런 의미에서이다.) 따라서 아래 코드에서 setState 함수를 실행한 뒤 state를 출력해봐도 변화가 바로 반영되지 않는 것을 확인할 수 있다.
function handleClick() {
console.log(this.state.name); // "Taylor"
this.setState({
name: 'Robin'
});
console.log(this.state.name); // Still "Taylor"!
}
React에서 useState를 컴포넌트 바깥에서 사용할 수 없는 이유가 여기에 있다. State는 항상 컴포넌트와 묶여서 존재하며, setState 함수를 실행하여 상태를 변경할 경우 컴포넌트의 리렌더링이 야기된다.
Vue.js는 React와 달리 상태 자체를 watch 한다. 더 정확히는, proxy 객체를 사용하여 객체 타입의 상태에 대한 접근을 가로챈다(intercept). Vue 2 기준으로 reactive objects에는 proxy가 사용되고, refs에는 getter / setters가 사용된다.
우선 Proxy 객체란 무엇인지 살펴보자. Proxy 객체는 javascript 내장 객체로, Object operations인 get, set, 그리고 속성에 대한 정의(defining properties)를 재정의 할 수 있는 객체이다. Proxy 객체는 두 개의 인수를 받는데, 첫 번째는 proxy 하고자 하는 원본 객체이고, 두 번째는 get, set 등의 operations가 정의된 handler 객체이다.
아래의 예시 코드를 살펴보자.
const target = {
message1: "hello",
message2: "everyone",
};
const handler2 = {
get(target, prop, receiver) {
return "world";
},
};
const proxy2 = new Proxy(target, handler2);
console.log(proxy2.message1); // world
target.message1
은 실제로 hello
란 값을 가지고 있음에도 불구하고, handler2
에서 정의한 get 메서드가 실행되어 해당 메서드의 반환값인 world
가 반환되었다.
이처럼 proxy 객체를 사용하면 객체의 속성에 접근하거나 그 속성을 변경할 때 중간에서 그 작업을 가로채서 특정 작업이 함께 실행되거나 지정한 값을 반환하는 등의 작업을 해줄 수 있게 된다.
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
위 pseudo-code에서,
Vue.js가 proxy 객체를 사용함으로 인해 실제 코드에서 상태를 다룰 때 느꼈던 차이점은 다음과 같다.
React의 경우 전역 상태를 사용하기 위해서는 Context API를 사용하거나, Recoil과 같은 전역 상태 라이브러리를 설치해서 사용해야 했다. 하지만 Vue.js에서는 다음과 같은 방식으로 전역 상태를 만들 수 있다.
아래와 같이 reactive를 통해 전역 상태를 만들고 이를 export 한다.
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0
})
해당 상태를 사용할 땐 아래와 같이 import 해서 사용하면 된다.
<!-- ComponentA.vue -->
<script>
import { store } from './store.js'
export default {
data() {
return {
store
}
}
}
</script>
<template>From A: {{ store.count }}</template>
처음에 이 방법을 알았을 땐 ‘이게 된다고..?’라는 생각이 들었다. React의 경우 state를 컴포넌트 외부에서 선언해줄 수 없다. 그도 그럴 것이 React에서는 상태가 컴포넌트와 강결합되어 있고, 컴포넌트 내의 상태가 변화하면 해당 컴포넌트가 리렌더링되도록 하기 때문이다. 반면 Vue.js에서 상태(reactive)는 proxy 객체기 때문에 위와 같이 사용할 수 있는 것이다.
Child component에서 부모의 데이터를 변경할 일이 생길 경우, child component로 부모의 상태를 변경하는 setState를 실행하는 함수를 넘겨주고, child component에서 이를 실행하는 방식으로 사용했었다.
const ParentComponent = () => {
const [state, setState] = useState('');
return(
<ChildComponent changeState={setState} />
)
}
const ChildComponent = ({changeState}) => {
return(
<button onClick={() => changeState('New data')}></button>
)
}
반면 Vue.js에서는 setState 함수가 없다. 그렇다면 props.value를 직접 변경해주면 되지 않는가? 하지만 props는 React에서도, Vue.js에서도 모두 readonly다.
Vue.js는 그 대신 children에서 이벤트를 emit 하여 이런 식으로 업데이트해줘~ 라고 부모 컴포넌트에 전달한다. 아래와 같이 emit의 첫 번째 인수로 event의 key를 넣고, 두 번째 인수로는 부모 컴포넌트에서 실행할 callback 함수에 넘겨줄 인수를 넣어준다.
// MyButton (child component)
<script setup>
const emit = defineEmits(['increase'])
function buttonClick() {
emit('increase', 3)
}
</script>
<template>
<button @click="buttonClick"></button>
</template>
부모 컴포넌트에서는 아래와 같이 사용한다.
// parent component
<MyButton @submit="(n) => count += n" />
위 방법은 개인적으로 익숙해지는데 꽤 걸렸다. 그래도 one-way data flow라는 점에서 setState 함수를 직접 넘겨주는 것보다 부모에서 부모의 상태를 통합적으로 관리할 수 있다는 장점이 있는 것 같다.
아직 Vue.js에 익숙해지고 있는 단계이다. Vue.js가 React와 닮은 부분이 많지만, 상태에 관련한 부분에서 확실한 차이점을 느꼈다. 이를 통해 상태를 어떻게 관리하고 다룰 수 있는지에 대한 시야가 넓어지는 것 같다. 계속 사용해보면서 새롭게 발견한 차이점이 있다면 꾸준히 추가해나갈 예정이다.
참고 자료