React와의 Vue의 가장 큰 차이점은 바로 상태(state)의 변화를 인식하는 메커니즘입니다.
상태는 프론트엔드에서 렌더링을 제어하는 아주 중요한 개념입니다. 그런데 이 상태의 변화를 인식하는 렌더링 방법이 React와 Vue가 다릅니다. 이 차이를 이해하기 전까지는, React유저였던 저는 의도대로 동작하지 않는 Vue가 매우 불편하게만 느꼈습니다.
간단한 예시를 들어보겠습니다.
React는 useState를 사용해서 빈 배열의 상태를 선언했습니다. 그리고 버튼을 클릭하면 setState로 destructuring을 활용해 'a'
를 추가해 array를 변경시켰습니다.
import { useState } from "react";
export default function App() {
const [list, setList] = useState([]);
const onClick = () => setList([...list, "a"]);
return (
<div className="App">
<h1>List: {list.join(", ")}</h1>
<button type="button" onClick={onClick}>
add
</button>
</div>
);
}
새 array로 상태가 변경되어 재렌더링되는 모습입니다.
그러면 Vue는 어떨까요? Vue의 상태인 reactive를 활용해서 array 상태를 선언한 뒤 똑같이 destructing을 활용해 'a'
를 추가해줍니다.
<script setup>
import { reactive } from 'vue'
const list = reactive([])
const onClick = () => {
list = [...list , 'a']
}
</script>
<template>
<h1> List: {{ list.join(', ') }} </h1>
<button @click='onClick'>
add
</button>
</template>
Vue는 새 array로 상태가 변경되었음에도 반응하지 않습니다.
그렇다면 이번에는 destructing을 해주지 않고 push를 해보겠습니다.
React에서는 버튼을 눌러도 반응이 없습니다.
import { useState } from "react";
export default function App() {
const [list, setList] = useState([]);
const onClick = () => {
list.push("a");
setList(list);
};
return (
<div className="App">
<h1>List: {list.join(", ")}</h1>
<button type="button" onClick={onClick}>
add
</button>
</div>
);
}
이번에는 반대로 Vue에서는 재렌더링이 일어납니다.
<script setup>
import { reactive } from 'vue'
const list = reactive([])
const onClick = () => {
list.push('a')
}
</script>
<template>
<h1> List: {{ list.join(', ') }} </h1>
<button @click='onClick'>
add
</button>
</template>
왜 이런 차이가 일어나는 걸까요?
바로 상태를 관리하는 메커니즘이 두 라이브러리(프레임워크)가 다르기 때문입니다.
React에서는 state를 불변적으로 취급합니다. 즉, 새로운 상태와 이전 상태를 비교해서 값이 다르다면 재렌더링을 시키는 것입니다. 여기서 원시 타입(string,number 등)은 값을 비교해버리고, 객체는 참조값(reference)를 그냥 비교해버립니다.
그래서, destructing을 통해 새로운 array로 바꿔줬을때는 새로운 참조값이 전달되어 렌더링이 일어나고, push를 해서 이전 참조값을 그대로 사용했을 때는 렌더링이 일어나지 않았던 것입니다.
하지만, Vue는 상태 비교에 Proxy를 사용합니다.
Proxy는 특정 객체를 감싸 프로퍼티 읽기, 쓰기와 같은 객체에 가해지는 작업을 중간에서 가로채는 객체입니다. 즉, Vue의 reactive
함수는 Proxy 객체를 반환하여, 이곳에 setter가 발생하는 것을 추적해 재렌더링을 시키고 있는 것입니다.
그래서 destructuring을 통해 새로운 array를 선언해버리면 기존의 Proxy를 덮어써버려서 렌더링이 일어나지 않았던 것이고. push를 통해 기존 array를 변경했을 때는 기존 Proxy로 이 변화를 감지해 재렌더링이 일어났던 것이죠.
한 마디로 정리하면, React는 새로운 bind를 만들고 싶어하고, Vue는 기존 bind를 유지하고 싶어합니다. (개인적으로는 React가 좀 더 함수형 프로그래밍처럼(같은 값이 들어가면 같은 값을 뱉는) 직관적이라고 얘기할 수도 있겠네요.)
그렇다면 Vue에서 destructing을 사용할 수는 없는 걸까요? map,filter,reduce 등의 함수형 프로그래밍 메소드를 사용할 수는 없는 걸까요?
Proxy를 이해하고 난다면 조금의 꼼수를 부릴 수 있습니다. 바로 객체로 감싸줌으로써 Proxy를 덮어쓰지 않도록 하는 것입니다.
<script setup>
import { reactive } from 'vue'
const list = reactive({a:[]})
const onClick = () => {
list.a = [...list.a, 'a']
}
</script>
<template>
<h1> List: {{ list.a.join(', ') }} </h1>
<button @click='onClick'>
add
</button>
</template>
이렇게 하면 Proxy인 list에 값을 직접 변경하지 않아 Proxy가 사라지지 않으면서, Proxy 프로퍼티의 변경을 추적할 수 있습니다. 즉 프로퍼티인 a
의 변경을 추적해 재렌더링이 일어나게 되죠.
실제로 Vue에서는 이러한 꼼수를 활용할 수 있는 함수를 제공하고 있는데요. 이건 바로 다음 글에서 알아보겠습니다.