1. ag-Grid 데이터 갱신 문제
실무 프로젝트에서 사용하는 데이터 테이블은 단순 조회 기능만 제공하는 경우가 드뭅니다. 특히 관리자 시스템이나 업무용 플랫폼에서는 사용자가 테이블 내부에서 직접 데이터를 수정하거나 상태를 변경하는 경우가 매우 많습니다.
예를 들어 특정 셀의 버튼을 클릭했을 때 다른 행의 계산 결과가 즉시 반영되거나, Pinia/Vuex 기반 전역 상태에 따라 특정 셀의 UI가 실시간으로 변경되는 요구사항이 존재합니다.
프로젝트에서는 이러한 기능 구현을 위해 ag-Grid를 사용하였습니다. ag-Grid는 Virtual Scroll 기반 구조와 고성능 렌더링 엔진을 제공하기 때문에 대규모 데이터 처리에 매우 적합했습니다. 하지만 Vue 3와 함께 사용하는 과정에서 예상하지 못한 렌더링 이슈가 발생하였습니다.
- 이슈 1: 셀 하나를 수정했는데 테이블 전체 Grid가 리렌더링되는 문제
Vue의 ref 또는 reactive 기반 rowData를 직접 수정할 경우 Vue는 배열 전체 변경을 감지합니다. 그 결과 ag-Grid 내부에서도 전체 Row 렌더링이 다시 발생할 수 있었습니다. 특히 데이터가 수천 건 이상인 경우 화면이 순간적으로 멈칫거리는 현상이 발생하였습니다.
- 이슈 2: cellRenderer 내부에서 외부 상태를 참조할 때 최신 값 미반영 문제
setup 내부 변수나 Pinia 상태를 참조하는 경우, ag-Grid는 초기 렌더링 시점의 값을 유지하는 경우가 있었습니다. 이는 Vue의 반응형 시스템과 ag-Grid의 컴포넌트 재사용 로직이 서로 다른 방식으로 동작하기 때문이었습니다.
- 이슈 3: 불필요한 API 호출이 렌더링 시마다 반복되는 현상
특정 셀이 렌더링될 때마다 외부 함수나 getter를 호출하는 구조를 사용하면 스크롤이나 상태 변경만으로도 동일한 API 요청과 연산이 반복 수행되었습니다. 결국 단순 기능 구현만으로는 해결할 수 없는 성능 문제와 구조적 문제가 함께 발생하게 되었습니다.
2. 데이터 이슈가 발생하는 이유
Vue 3는 Proxy 기반 반응형 시스템을 사용합니다. 일반적인 Vue 애플리케이션에서는 매우 효율적인 구조이지만, ag-Grid처럼 자체 렌더링 엔진을 가진 라이브러리와 함께 사용할 경우 문제가 발생할 수 있습니다. ag-Grid는 내부적으로 다음 상태들을 자체적으로 관리합니다.
-
Row Node / Cell State / Scroll State / Selection State / Transaction State
즉, Vue와 ag-Grid 모두 상태를 추적하고 렌더링을 제어하려는 구조를 가지게 되는 것입니다. 특히 rowData를 reactive로 깊게 추적할 경우 Vue는 수천 개 이상의 데이터 객체를 모두 Proxy로 감싸 관리하게 됩니다. 이 과정에서 다음과 같은 문제가 발생하였습니다.
-
메모리 사용량 증가
-
불필요한 렌더링
-
Proxy 생성 오버헤드
-
상태 추적 비용 증가
또한 ag-Grid는 Virtual Scroll 기반 구조를 사용하기 때문에 렌더링 최적화가 매우 중요한데, Vue의 과도한 반응형 추적이 이를 방해하는 상황이 발생하였습니다. 결국 핵심 문제는 ‘어떤 상태를 Vue가 관리하고, 어떤 상태를 ag-Grid에 위임할 것인가’에 있었습니다.
3. 해결 방법
① shallowRef 기반 최적화
첫 번째 해결 방법은 rowData를 shallowRef로 선언하는 것이었습니다. 기존에는 ref 또는 reactive를 사용하여 rowData를 선언하였지만, 이 경우 Vue는 내부 객체까지 모두 추적하게 됩니다. 반면 shallowRef는 객체 내부 속성 변경까지는 추적하지 않습니다. 즉, Vue는 rowData 배열 참조 자체만 관리하고 실제 내부 상태는 ag-Grid가 담당하도록 역할을 분리할 수 있었습니다.

이를 통해 다음과 같은 효과를 얻을 수 있었습니다.
-
불필요한 반응형 추적 제거
-
전체 Grid 리렌더링 감소
-
메모리 사용량 감소
-
렌더링 성능 향상
특히 수천 건 이상의 데이터를 가진 Grid 환경에서 성능 개선 효과가 매우 크게 나타났습니다. 또한 Virtual Scroll 구조 역시 훨씬 안정적으로 동작하였습니다. 이번 경험을 통해 모든 상태를 무조건 Vue 반응형 시스템에 맡기는 것이 정답은 아니라는 점을 체감할 수 있었습니다. 때로는 상태 관리 책임을 라이브러리에 위임하는 것이 훨씬 효율적인 구조가 될 수 있었습니다.
② Custom Cell Renderer와 Transaction API
두 번째 해결 방법은 Custom Cell Renderer와 Transaction API를 활용하는 것이었습니다. ag-Grid는 Cell Component를 재사용하는 구조를 가집니다. 즉, 스크롤 시 모든 컴포넌트를 새로 생성하는 것이 아니라 기존 컴포넌트를 재활용합니다.

문제는 이 과정에서 외부 상태 참조가 최신 값으로 갱신되지 않는 경우가 있었다는 점입니다. 이를 해결하기 위해 프로젝트에서는 refresh 메서드를 명시적으로 구현하였습니다.
특정 데이터 변경 시 해당 Cell만 부분적으로 다시 렌더링하도록 구성한 것입니다. 또한 데이터 갱신 방식 역시 변경하였습니다. 기존에는 다음과 같은 방식으로 데이터를 수정하였습니다
-
rowData.value[0].status = ‘Done’
하지만 이 방식은 Vue와 ag-Grid 모두에게 비효율적인 구조였습니다. 결국 프로젝트에서는 ag-Grid의 applyTransaction API를 직접 사용하는 방식으로 구조를 변경하였습니다.

Transaction API는 다음 작업을 명시적으로 전달할 수 있습니다.
-
add / update / remove
이 구조의 가장 큰 장점은 “부분 업데이트”가 가능하다는 점이었습니다. 즉, 전체 Grid를 다시 렌더링하지 않고 실제 변경된 Row만 갱신할 수 있었습니다. 그 결과 다음과 같은 효과를 얻을 수 있었습니다.
-
렌더링 범위 최소화
-
성능 향상
-
스크롤 안정성 증가
-
API 호출 감소
-
부분 상태 제어 가능
특히 대규모 데이터 환경에서는 이러한 차이가 매우 크게 나타났습니다.
4. 성능 최적화와 설계 관점
이번 트러블슈팅 과정에서 가장 크게 느낀 점은 프론트엔드 성능 최적화는 단순 코드 수정이 아니라 “설계의 문제”라는 점이었습니다. Vue의 반응형 시스템은 매우 강력하고 편리합니다.
하지만 ag-Grid처럼 자체 렌더링 엔진을 가진 라이브러리와 결합될 경우 상태 관리 책임이 중복되며 오히려 성능 문제가 발생할 수 있었습니다. 결국 중요한 것은 다음 질문이었습니다.
-
어떤 상태를 Vue가 관리할 것인가.
-
어떤 상태를 ag-Grid에 위임할 것인가.
-
어디까지 반응형으로 추적할 것인가.
-
어떤 변경을 Transaction으로 처리할 것인가.
즉, 프레임워크와 라이브러리 사이의 역할 분리가 핵심이었습니다. 또한 이번 경험을 통해 단순 기능 구현보다 내부 동작 원리를 이해하는 것이 훨씬 중요하다는 점을 체감할 수 있었습니다.
5. 마무리
이번 ag-Grid 성능 최적화 경험은 단순한 버그 수정 이상의 의미를 가진 경험이었습니다. 처음에는 단순 렌더링 문제처럼 보였지만, 실제로는 Vue 반응형 시스템과 ag-Grid 렌더링 엔진 사이의 구조적 충돌 문제였습니다. 특히 다음 요소들을 깊이 경험할 수 있었습니다.
-
대규모 데이터 렌더링 구조
-
반응형 시스템의 장단점
-
Transaction 기반 상태 관리
-
라이브러리와 프레임워크 역할 분리
-
Virtual Scroll 최적화
무엇보다 “무엇을 프레임워크에 맡기고 무엇을 라이브러리에 위임할 것인가”를 결정하는 설계의 중요성을 크게 느낄 수 있었습니다. 결론적으로 이번 경험은 단순 Vue 또는 ag-Grid 사용 경험을 넘어, 대규모 프론트엔드 환경에서의 성능 설계와 상태 관리 구조를 깊이 이해할 수 있었던 매우 의미 있는 경험이었습니다.
<참고 문헌>
- ag-Grid 공식 문서 (https://www.ag-grid.com/vue-data-grid/getting-started/)
- Vue 공식 문서 (https://ko.vuejs.org/api/reactivity-advanced)
lucy