어느 순간부터 앱이 느려졌다. 원인 찾느라 한참 걸렸다.


증상

  • 드래그가 버벅거림
  • 메모리 사용량 계속 증가
  • CPU 100%

원인

Konva Stage를 ref로 감쌌다:

const stage = ref<Konva.Stage | null>(null);

Vue의 ref()는 객체를 깊은 반응성으로 감싼다.

Konva Stage는 내부에 수많은 프로퍼티가 있음. children, attrs, parent, 이벤트 핸들러…

이걸 전부 Proxy로 감싸버리니까 느려진 거다.


해결: shallowRef

// 안 좋음
const stage = ref<Konva.Stage | null>(null);

// 좋음
const stage = shallowRef<Konva.Stage | null>(null);

shallowRef는 최상위만 반응성. 내부 프로퍼티는 건드리지 않음.


Layer, Node도 마찬가지

const transformerRef = shallowRef<Konva.Transformer | null>(null);
const layerRef = shallowRef<Konva.Layer | null>(null);

Konva 객체는 전부 shallowRef.


데이터와 노드 분리

Konva 노드를 직접 저장하지 말고, 데이터만 저장:

// 안 좋음
const items = ref<Konva.Rect[]>([]);

// 좋음
const itemsData = ref<FurnitureData[]>([]);
// Konva 노드는 렌더링 시 생성됨

스토어에는 순수 데이터만. Konva 노드는 vue-konva가 관리.


computed 주의

// 문제 가능
const selectedNode = computed(() => {
  return stage.value?.findOne(`#${selectedId.value}`);
});

매번 findOne 호출. 캐싱 안 됨.

// 개선
const selectedNode = shallowRef<Konva.Node | null>(null);

watch(selectedId, (id) => {
  selectedNode.value = id 
    ? stage.value?.findOne(`#${id}`) 
    : null;
});

watch의 deep 옵션

// 주의: 성능 문제 가능
watch(store.items, () => { ... }, { deep: true });

배열 안 객체 하나 바뀌어도 콜백 실행됨. 필요한 경우만 deep 쓰기.


markRaw

절대 반응성 필요 없는 객체:

import { markRaw } from 'vue';

const konvaStage = markRaw(new Konva.Stage({ ... }));

markRaw로 감싸면 Vue가 무시함.


정리

상황사용
Konva 노드 참조shallowRef
순수 데이터ref
절대 반응성 불필요markRaw
깊은 감시 필요할 때만watch + deep

결과

shallowRef로 바꾸니까 다시 빨라졌다.

Vue + Canvas 라이브러리 조합할 때 반응성 범위 조심해야 한다.


시리즈 끝

20개 글에 걸쳐 Floor Planner 개발 과정을 정리했다.

요약:

  • vue-konva로 Canvas 앱 만들기
  • 좌표계, 줌/팬, 그리드
  • 가구/문 시스템
  • 키보드 단축키, Undo/Redo
  • 저장/불러오기, 이미지 내보내기
  • 테스트 (Vitest + Playwright)
  • 성능 최적화

직접 써보기: Floor Planner


#1 - 왜 만들게 됐나로 돌아가기