상황
도면을 띄우고 그 위에 lamp 같은 요소들을 자유롭게 배치하는 캔버스 편집 모드를 만들고 있었다.
요소 이동은 jQuery UI draggable 로 처리하는 레거시 코드 위에서 일하던 중이었다.
편집 모드를 켜고 lamp 를 드래그하면 잠깐 따라오다가 다시 원위치로 튀는 현상이 있었다.
콘솔에 에러는 없고, jQuery UI 자체도 정상 동작하고, Vuex 도 멀쩡했다. 다른 페이지에서 같은 라이브러리로 만든 드래그는 잘 됐는데 이 화면만 이상했다.
CSS transition: none !important 까지 박아봐도 똑같았다. 무언가 외부에서 위치를 계속 덮어쓰고 있다는 감만 있었다.
원인 추적
Vuex DevTools 를 켜고 드래그 중 어떤 mutation이 fire 되는지 봤다.
mousemove 한 번에 setHoveredDeviceId 가 수십 번 호출되고 있었다.
캔버스 편집 모드에는 같은 디바이스에 속한 요소들끼리 hover 시 함께 하이라이트하는 기능이 있다.
마우스가 요소 위를 지나갈 때마다 hoveredDeviceId 가
갱신 → 컴포넌트 재렌더 → :style="{ left: bodyX+'px', top: bodyY+'px' }" 가 다시 적용되고 있었다.
jQuery UI 가 라이브로 변경하고 있던 element.style.left 값을 Vue가 매 프레임 옛 bodyX 로 덮어쓰는 구조였다.
도대체 이 사악한 코드는 왜
[mousemove]
→ setHoveredDeviceId mutation
→ 컴포넌트 재렌더
→ patchStyle (el.style.left = oldBodyX + "px") ← 여기서 jQuery UI 가 만든 위치를 덮음
Vue 3 의 patchStyle 은 reactive 가 트리거되면 inline style 의 키를 다시 적용한다.
props의 bodyX/bodyY는 드래그가 끝나기 전까진 갱신이 안 되니 옛 값이 그대로 박혔다. 즉 Vue 입장에선 "변한 게 없는데?" 라는 게 정답이었지만, 그 "변한 게 없는" 좌표를 매 프레임 다시 박아넣는 게 jQuery UI 의 라이브 위치를 덮어쓰는 부작용을 만들었다.
해결
해결 방향은 두 가지였다.
1. 드래그 중에는 hover mutation 을 막기
2. hover state 를 Vuex 밖으로 빼서 재렌더 자체를 안 일으키게 하기
1번은 드래그 시작/종료를 감지해 mutation 을 일시 중단해야 하고, 다른 reactive 트리거가 또 끼어들면 같은 문제가 재발한다.
2번이 더 깔끔했다. Vue 가 모르는 변화면 재렌더가 안 일어나기 때문이다.
// 기존 — Vuex commit으로 reactive 갱신
store.commit("setHoveredDeviceId", { deviceId });
// 수정 — DOM 속성 직접 토글
document.querySelectorAll(`[device_id="${deviceId}"]`).forEach((el) => {
el.setAttribute("data-device-hovered", "true");
});
CSS 도 [data-device-hovered] 셀렉터로 반응하도록 바꿨다.
.data-box-Item[data-device-hovered] > .point2_Inbox {
outline: 0.75px solid rgba(255, 255, 255, 0.7);
}
DOM 속성 토글은 Vue reactive 시스템 밖이라 컴포넌트가 재렌더되지 않는다. 드래그 중 마우스가 요소 위를 지나가도 :style 이 다시 적용되지 않으니 jQuery UI 가 만든 위치가 그대로 살아남는다.
배운 점
reactive 시스템 안에는 "사용자가 의도한 영속 상태" 만 두는 편이 맞다는 걸 다시 느꼈다.
hover, 임시 마우스 위치, 스크롤 좌표같이 자주 변하지만 영속할 필요 없는 transient state 는 reactive 밖으로 빼야 외부 DOM 조작 라이브러리와 충돌이 안 난다.
같은 패턴은 Sortable.js, d3, Three.js, leaflet 같이 직접 DOM 을 조작하는 라이브러리들 모두에 적용된다.
Vue 의 reactive 가 그 라이브러리가 다루는 속성에 트리거를 거는 순간, 라이브 동작이 깨질 위험이 따라온다.
'Dev > Vue' 카테고리의 다른 글
| [Vue3] canvas에 Figma 같은 정렬 가이드 만들기 - transform 위에서 정확히 그리는 법 (0) | 2026.05.08 |
|---|---|
| [Vue3] ref(객체)에 watch 안 걸어도 되는 이유 (0) | 2026.04.20 |
| #Vue #Generic 안녕하세요 Next에서 넘어왔습니다 (0) | 2026.03.06 |