뷰 프레임워크를 사용하면서 shallowRef에 대해서 궁금한 적이 많았던 것 같다. 그래서 shallowRef의 경험과 개념에 대해서 애기하고 ref와 어떤 차이점이 있는 알아보려고 합니다.
1. Vue의 핵심인 ref
ref는 뷰 프레임워크의 반응성에서 중요한 역할을 하는 함수이다. reactive의 경우 object의 변화만을 감지할 수 있었지만, ref의 등장으로 number, string, boolean타입과 같이 원시타입의 경우에도 변화를 감지 할 수 있게 만들었다.
기본적으로 ref를 사용하게 된다면,
<script setup>
import { ref } from "vue";
const refValue = ref({ val: 0 });
</script>
ref는 Proxy를 활용하여 내부 깊숙히 변화를 관찰할 수 있기 때문에 refValue.value.val로 접근해서 값을 변경하여도 뷰에서는 데이터가 변경되었다는 것을 감지할 수 있다. 이런 원리로 우리가 배열에 push나 pop 메소드를 사용해도 뷰에서는 변화를 감지할 수 있던 것이다.
2. ref가 있는데 굳이 사용할 필요가 있나?
shallowRef를 통해서 대규모 데이터 구조의 성능 최적화를 할 수 있게 된다.
사실 뷰의 반응성의 경우 Proxy를 통해 내부 깊숙하게 변경을 감지할 수 있는 구조이다. 하지만 데이터의 크기가 너무 큰 경우에는 특정 수준의 오버 헤드가 발생하게 된다. 즉, 내부의 모든 속성의 변화를 관찰해야 되기 때문에 엄청난 자원 낭비를 하게 된다는 말이다.
정리하자면, 우리가 변화를 찾기 위해서 모든 내부를 뒤지는 것보단 제일 위에 주소값의 변화만을 확인하는 것이 속도적인 측면에서 더 효율적이기 때문에 shallowRef를 만들고 사용할 수 있게 하였다.
3. shallowRef
shallowRef를 사용한 경우에는 위에 코드에 나타난 ref방식인 refValue.value.val에 접근해서 값을 변경한다면 뷰에서는 값이 변경되었는 지 감지 할 수 없다. refValue.value에 접근해서 값을 업데이트해야지만 값이 변경되었다는 것을 감지하고 새롭게 렌더링을 진행하게 된다. shallowRef의 경우에는 이름에서 볼 수 있듯이 내부 깊숙히 변화를 보는 것이 아닌 얕은 비교를 하게 된다. 어떻게 보면 리액트의 useState, useReducer와 비슷한 방식으로 동작한다고 볼 수 도 있다.
shallowRef사용방법은 ref와 비슷한 편이다.
<script setup>
import { shallowRef } from "vue";
const shallowRefValue = shallowRef({ val: 0 });
</script>
하지만 shallowRef를 사용하는 경우에는 value가 가리키고 있는 주소값 자체를 변경해야 되기 때문에 배열의 push나 객체에 새로운 속성을 추가해도 전혀 반응하지 않는다는 것에 주의해야 된다. 반응이 되었다는 것을 트리거 하기 위해선 shallowRefValue.value에 새로운 객체를 대입해야 된다.
3. 더 성능이 좋으니깐 무조건 shallowRef
성능이 좋다고 무조건 사용하는 것은 좋지 않은 방식이라고 생각한다. 객체가 너무 복잡한 경우에는 shallowRef를 사용해서 성능을 좋게 할 수 있지만, 이러한 장점은 단점으로 바뀔 수 가 있다. 뷰에서는 내부 깊숙히 변화를 체크하였기 때문에 변수에 새로운 값을 대입하는 코드만 작성하여도 되었지만, 이제 우리가 실제로 데이터의 변화에 대해서 안정성을 더 보장할 필요가 생겼다는 말이다. 상위의 주소값의 변경만을 체크해서 새롭게 렌더링 해주기 때문에 우리는 내부의 변화까지도 개발자가 수동적으로 꼼꼼히 체크해줘야 된다.
<script setup>
import { shallowRef } from "vue";
const shallowRefValue = shallowRef({ depth1: { depth2: { a: 1, depth3: 0 } } });
shallowRefValue.value = {
depth1: { depth2: { ...shallowRefValue.value.depth1.depth2, depth3: 1 } },
};
</script>
<script setup>
import { ref } from "vue";
const refValue = ref({ depth1: { depth2: { depth3: 0 } } });
refValue.value = refValue.value.depth1.depth2.depth3 = 1;
</script>
그래서 저는 shallowRef 다음과 같은 상황일 때 많이 사용하는 편입니다. 복잡한 객체를 다룰 때 특히 중첩된 구조를 갖고 있거나, 내부 속성이 많은 경우에 사용합니다. 그리고 서버와 클라이언트 간 데이터 동기화가 필요한 경우에도 많이 사용하는 편입니다. 서버에서 받아온 데이터를 계속 갈아 끼워주기만 한다면 굳이 ref를 사용할 필요는 없기 때문이죠.
4. Remind
뷰와 리액트를 비교하면서 느끼는 부분이지만, 서로의 좋은점과 나쁜점을 보안하면서 서로 발전을 해온 것을 확인할 수 있었다.
리액트의 경우 얕은 비교를 통해서 성능을 우선시 하였지만, 뷰의 경우에는 Proxy의 도입으로 내부 깊숙히 반응성을 체크하면서 직관적인 코드로 변화를 트리거할 수 있게 만들었다. 두개의 방법 중 어떤 것이 더 좋고 나쁜 것은 없다고 다시한번 느낄 수 있었다.
우리는 그냥 이런 방법도 있고 저런 방법도 있구나 하면서 사고를 확장 시킬 수 있었던 것 같다.