Canvas 기반 편집기를 Konva로 개발하면서 가장 큰 기술적 난관 중 하나는 Canvas 요소와 HTML 요소 간 좌표 차이였다.
사용자가 보는 위치와 실제 Konva Canvas에서 인식하는 위치가 달라 툴바, 입력창 등의 HTML 오버레이가 엉뚱한 위치에 렌더링되는 문제가 반복적으로 발생했다.
이 글에서는 이 좌표 오차가 왜 발생하는지, 그리고 이를 어떻게 해결했는지에 대해 실제 경험을 바탕으로 정리한다.
Canvas는 Canvas 픽셀 좌표계를, HTML은 CSS 픽셀 좌표계를 기준으로 한다.
브라우저의 확대/축소나 디바이스의 devicePixelRatio 설정에 따라 좌표 오차가 누적될 수 있다.
Konva의 scale은 Canvas 내부 transform으로 적용된다.
반면 HTML은 CSS의 transform 속성으로 처리되므로, 서로 다른 좌표 변환 방식을 사용한다.
<Stage>의 부모 요소에 padding, border, flex 설정이 있으면 좌표 보정이 반드시 필요하다.getClientRect()는 DOM 기준, getAbsolutePosition()은 Konva 내부 기준이다.
따라서 스크롤이 있는 페이지에서는 툴바 위치가 어긋나는 원인이 될 수 있다.
텍스트나 도형을 클릭하면 요소 하단 중앙에 툴바를 띄우는 구조를 구현 중이었다.
초기에는 getAbsolutePosition()으로 좌표를 받아 다음과 같이 툴바 위치를 설정했다:
const position = node.getAbsolutePosition();
setToolbar({ x: position.x, y: position.y });
그러나 실제 렌더링된 툴바는 완전히 다른 위치에 표시되었다.
이는 getAbsolutePosition()이 scale, 회전, 그룹화 변형을 반영하지 않는 논리 좌표만 반환하기 때문이다.
보다 정확한 위치 계산을 위해 getClientRect()를 사용한 코드로 교체했다:
const handleUpdateToolbarNode = (node: Konva.Node) => {
requestAnimationFrame(() => {
const rect = node.getClientRect();
setToolbar({
x: rect.x + rect.width / 2 - (TOOLBAR_WIDTH * zoom) / 2,
y: rect.y + rect.height + 8,
});
});
};
getClientRect()는 실제 렌더링 기준의 x, y, width, height 정보를 포함한다.
이를 기반으로 요소의 하단 중앙에 툴바가 정확히 위치하게 된다.
반환값: { x, y } (논리 좌표)
장점: 변형 전 순수 좌표 확인 가능
단점: 스케일, 회전, 그룹 트랜스폼 미반영
반환값: { x, y, width, height }
장점: 실제 렌더링된 bounding box 조회 가능
단점: CSS와 Canvas 스케일 차이에 대한 보정 필요
Zoom 기능을 추가한 이후, 좌표가 또다시 어긋나는 문제가 발생했다.
Konva 좌표는 확대된 상태로 계산되므로, HTML 요소는 Zoom을 나누어 보정해야 한다:
const textPosition = textNode.getAbsolutePosition();
const areaPosition = {
x: textPosition.x / zoom,
y: textPosition.y / zoom,
};
setupTextareaStyles({ textarea, textNode, areaPosition });
HTML과 Canvas는 본질적으로 좌표계가 다르다.
이 차이를 정확히 이해하고, getClientRect()와 zoom 보정을 적절히 적용한 결과, 툴바 및 입력창 같은 HTML 오버레이 요소들이 사용자 기준 위치에 정확하게 렌더링되도록 구현할 수 있었다.
이 경험을 통해 좌표계 문제를 단순 보정이 아닌 시스템적 차이로 인식하고 대응하는 사고력을 키울 수 있었다.

에서
