Vue에서 불변 객체를 활용해 성능 최적화하는 방법과 원리

실무에서 자주 마주치는 문제 상황을 하나 살펴보자. 아래 예제는 단순한 버튼을 포함하고 있으며, 클릭 시 데이터를 로드하고 그 길이를 화면에 표시한다.

템플릿은 다음과 같다. 버튼에 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()를 적극 활용해 리소스 낭비를 방지하는 것이 중요하다.

태그: Vue.js 성능 최적화 Object.freeze 반응성 시스템 JavaScript

6월 7일 22:35에 게시됨