[Konva.js] 드래그 앤 드롭과 객체 변형 구현하기
팀 프로젝트에서 Konva.js를 사용하여 사용자가 직접 객체를 편집하는 인터랙티브 UI를 구현했습니다.
실제로 작성한 코드를 바탕으로, 두 가지 핵심 기능인 '드래그 앤 드롭으로 객체 생성'과 'Transformer로 객체 변형하기'를 구현한 방법을 공유하는 글을 작성합니다.
드래그 앤 드롭으로 객체 생성하기
이미지를 캔버스 위로 드래그 앤 드롭하면 마우스를 놓은 위치에 새로운 사각형이 나타나야 합니다.
브라우저가 제공하는 마우스 좌표(clientX, clientY)는 전체 화면 기준입니다. 이 좌표를 Konva 캔버스 내부의 로컬 좌표로 변환해야만 정확한 위치에 객체를 생성해야 합니다.
// EditRoomPage.tsx
const stageRef = React.useRef<Konva.Stage | null>(null);
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (!stageRef.current) return;
const stage = stageRef.current.getStage();
// 1. 캔버스 DOM 요소의 화면상 위치 정보 가져오기
const stageBox = stage.container().getBoundingClientRect();
// 2. 브라우저 좌표를 캔버스 내부 좌표로 변환
const dropX = e.clientX - stageBox.left;
const dropY = e.clientY - stageBox.top;
// 3. 변환된 좌표로 새로운 객체 생성
const newRoom: RoomDto = {
id: `new_${Date.now()}`,
positionX: dropX,
positionY: dropY,
width: 120,
height: 100,
name: '새로운 방',
detail: '',
};
setRooms((prev) => [...prev, newRoom]);
};
- stage.container().getBoundingClientRect(): 이 함수는 Konva 캔버스를 감싸는 <div> 컨테이너의 화면 내 위치 정보(시작 left, top 좌표 등)를 가져옵니다. 이는 좌표 변환의 기준점이 됩니다.
- e.clientX - stageBox.left: 이 코드는 좌표 변환의 핵심입니다. 브라우저 왼쪽 끝에서 마우스까지의 거리(e.clientX)에서, 브라우저 왼쪽 끝에서 캔버스까지의 거리(stageBox.left)를 빼면 캔버스 내부에의 X좌표가 계산됩니다.
- setRooms(...): 계산된 dropX, dropY를 positionX, positionY로 사용하여 새로운 방 객체를 만들고, React의 useState를 통해 rooms 배열 상태를 업데이트하여 화면에 렌더링합니다.
Transformer로 객체 크기 및 위치 조절하기
사용자가 사각형을 클릭하면 크기 조절 핸들이 나타나고, 핸들을 조작하여 변경한 크기와 위치가 화면에 나타나야 합니다.
Konva의 Transformer는 객체의 크기와 위치를 변경합니다. 이 변경 사항을 onTransformEnd 이벤트가 끝나는 시점에 찾아내어 rooms 배열과 동기화 합니다
// EditRoomPage.tsx
const handleTransformEnd = (room: RoomDto, node: Konva.Rect) => {
// 1. Transformer가 변경한 scale 값을 가져온다.
const scaleX = node.scaleX();
const scaleY = node.scaleY();
// 2. 새로운 너비와 높이를 계산하고, 변경된 위치도 가져온다.
const updatedRoom: RoomDto = {
...room,
positionX: node.x(),
positionY: node.y(),
width: Math.max(30, node.width() * scaleX), // 최소 크기
height: Math.max(30, node.height() * scaleY),
};
// 3. 다음 변형을 위해, 노드의 시각적 scale을 1로 초기화한다.
node.scaleX(1);
node.scaleY(1);
// 4. 계산된 최종 값으로 React 상태를 업데이트한다.
setRooms((prev) => {
const updatedRooms = prev.map((r) =>
r.id === room.id ? updatedRoom : r
);
return updatedRooms;
});
};
- node.scaleX(), node.scaleY() : onTransformEnd의 콜백 인자로 들어온 node는 변형이 적용된 Konva 객체입니다. 이 객체의 scale 속성을 통해 얼마나 크기가 변했는지 알 수 있습니다.
- node.width() * scaleX : Transformer는 객체의 width 자체를 바꾸는 것이 아니라 scale을 조절합니다. 따라서 최종 너비는 기존 너비 * 스케일 값으로 직접 계산해주어야 합니다. node.x()와 node.y()를 통해 변경된 최종 위치도 가져옵니다.
- node.scaleX(1) : 상태 업데이트 후 Konva 노드의 scale 값을 다시 1로 초기화해야 합니다. 그렇지 않으면 다음 크기 조절 시, 이전 scale 값에 새로운 scale이 곱해져서 원치 않은 결과가 나타납니다. 시각적 표현은 React 상태에 맡기고, Konva 노드의 scale은 항상 1로 유지해야 합니다.
- setRooms(...): 계산된 updatedRoom 객체로 rooms 배열에서 해당 ID를 가진 요소를 찾아 교체한 후, 새로운 배열로 상태를 업데이트합니다.
정리
위의 구현을 통하여 라이브러리가 제공하는 편리한 기능의 내부 동작 원리를 이해하는 것 또한 중요하다는 것을 배웠습니다.
특히, canvas 환경에서의 좌표계 문제나 상태 동기화 이슈를 직접 겪고 해결하는 것은 지금까지와 다른 구현과는 많이 다른 새로운 경험이였습니다.
'React' 카테고리의 다른 글
| [React] LocalStorage(로컬스토리지) - 저장, 불러오기, 삭제 (1) | 2025.05.29 |
|---|---|
| [React] Props(프롭스)란? (0) | 2025.05.28 |
| [React] 리액트 타입 연결하기 (0) | 2025.05.28 |
| [React] UseState란? (0) | 2025.05.27 |
| [React] UseMemo란? (0) | 2025.05.22 |