행 5천 개짜리 표를 ref에 담았더니 첫 렌더가 눈에 띄게 느려진 이유
문제 발생
API에서 받은 표 데이터를 그대로 담았습니다.
const rows = ref<Row[]>([]); // 5,000행 × 20필드
rows.value = await fetchRows();렌더보다 대입 자체가 느렸고, 지도 라이브러리 인스턴스를 ref에 넣은 화면에서는 동작이 이상해졌습니다.
원인 분석
ref()는 담긴 값을 깊게 반응형으로 만듭니다. 5,000 × 20개 속성을 순회하며 프록시를 만드는 비용이 대입 시점에 한 번에 발생합니다.
그런데 이 표는 셀 하나가 바뀌는 일이 없고 항상 통째로 교체됩니다. 깊은 반응성에 낸 비용이 전혀 쓰이지 않습니다.
Vue 문서는 이 경우를 위한 도구를 명시적으로 제공합니다. shallowRef는 ref()의 얕은 버전으로 .value 접근만 반응형이며, 내부 값은 그대로 저장·노출되고 깊게 반응형으로 만들어지지 않습니다. 용도도 문서에 그대로 적혀 있습니다 — 큰 자료 구조의 성능 최적화, 그리고 외부 상태 관리 시스템과의 통합.
라이브러리 인스턴스 쪽은 다른 문제였습니다. 프록시로 감싸면 내부에서 this나 인스턴스 동일성을 비교하는 코드가 깨집니다. 문서가 markRaw의 용도로 드는 것이 정확히 이것입니다 — 복잡한 서드파티 클래스 인스턴스나 Vue 컴포넌트 객체처럼 반응형이 되면 안 되는 값, 그리고 불변 데이터로 큰 목록을 렌더할 때의 성능 개선.
해결 방안
- 통째로 교체되는 데이터는
shallowRef입니다.
const rows = shallowRef<Row[]>([]);
rows.value = await fetchRows(); // 갱신 O
// rows.value[0].name = "..." // 갱신 X — 이 규약을 팀이 알아야 한다-
얕은 ref의 내부를 직접 바꿔야 한다면
triggerRef로 알립니다. 문서 예시대로 깊은 변경 뒤 수동으로 효과를 발동시킵니다. 다만 이게 자주 필요하다면 애초에shallowRef가 맞는 선택인지 다시 봅니다. -
프록시가 되면 안 되는 객체는
markRaw로 감쌉니다. 문서 설명대로 절대 프록시로 변환되지 않도록 표시하고 객체 자신을 반환합니다.
const map = markRaw(new MapLibreMap({ container }));-
먼저 측정합니다. 수십·수백 개 항목에서는 깊은 반응성이 문제가 되지 않습니다.
shallowRef는 "값이 바뀌어도 화면이 안 바뀌는" 새로운 함정을 들여오므로, 이유 없이 기본값으로 쓰지 않습니다. -
왜 얕은지 주석으로 남깁니다. 다음 사람이 "버그인 줄 알고"
ref로 되돌리면 원래 문제가 그대로 돌아옵니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.