실무에서 자주 마주치는 문제 상황을 하나 살펴보자. 아래 예제는 단순한 버튼을 포함하고 있으며, 클릭 시 데이터를 로드하고 그 길이를 화면에 표시한다.
템플릿은 다음과 같다. 버튼에 loadData 메서드를 바인딩하고, myData 배열의 길이를 출력한다.
<template>
<div id="app">
<button @click="loadData">데이터 로드</button>
<p>총 {{ myData.length }} 개의 데이터가 로드되었습니다</p>
</div>
</template>
스크립트 부분에서는 loadData가 호출되면 getData 함수를 통해 10만 개의 객체로 구성된 배열을 생성하고, 이를 myData에 할당한다.
export default {
name: 'App',
data() {
return {
myData: []
};
},
methods: {
loadData() {
this.myData = this.getData();
},
getData() {
const result = [];
for (let i = 0; i < 100000; i++) {
result.push({
id: i,
name: `my name is ${i}`,
son: {
id: `${i + 1}`,
name: `His name is ${i + 1}`
}
});
}
return result;
}
}
};
버튼을 클릭하면 UI 반응이 지연되는 것을 확인할 수 있다. 브라우저 개발자 도구의 성능 탭으로 분석해보면, 렌더링과 페인팅 시간은 약 6ms로 매우 짧지만, JavaScript 실행 시간이 6635ms에 달한다.
이 중 getData 함수 자체는 약 348ms만 소요되며 전체의 5% 미만을 차지한다. 대부분의 시간은 proxySetter와 같은 Vue의 반응성 시스템 내부 함수에서 소모된다.
특히 proxySetter 내부에서 observe 함수가 반복적으로 호출되며, Vue가 각 객체와 중첩된 속성까지 순회하면서 Object.defineProperty 또는 프록시 기반의 반응성 추적을 설정하기 때문이다. 이 과정은 깊이가 깊고 데이터 양이 많을수록 비용이 크게 증가한다.
하지만 위 예제에서 우리는 myData의 개별 속성이 변경되어도 다시 렌더링될 필요가 없다. 단지 배열의 길이만 표시할 뿐이며, 데이터 자체는 정적이기 때문이다. 따라서 이러한 데이터에 반응성을 부여하는 것은 낭비다.
이 문제를 해결하는 간단한 방법은 JavaScript의 Object.freeze()를 사용하는 것이다. 이 메서드는 객체를 동결시켜 프로퍼티 추가, 삭제, 수정을 방지하며, Vue는 동결된 객체를 감지하여 반응성 처리를 생략한다.
아래처럼 코드를 수정하자.
loadData() {
this.myData = Object.freeze(this.getData());
}
이 변경 후 성능을 다시 측정하면 JavaScript 실행 시간이 약 500ms 이하로 줄어드는 것을 확인할 수 있다. 반응성 추적이 제거되면서 초기화 비용이 급격히 감소하기 때문이다.
이처럼 Vue에서는 불필요한 반응성 추적이 성능 병목을 유발할 수 있으므로, 정적 데이터에는 Object.freeze()를 적극 활용해 리소스 낭비를 방지하는 것이 중요하다.