개발일지/Next.js
[Konva.js] Next.js + TypeScript에서 그림판 만들기 - #2 (Undo/Redo & 지우개 기능 추가)
ayeongjin
2025. 2. 20. 00:02
🎨 Next.js + TypeScript에서 그림판 만들기 - #2 (Undo/Redo & 지우개 기능 추가)
2025.02.19 - [개발일지/Next.js] - [Konva.js] Next.js + TypeScript에서 그림판 만들기 - #1 (Fabric.js vs Konva.js)
[Konva.js] Next.js + TypeScript에서 그림판 만들기 - #1 (Fabric.js vs Konva.js)
🎨 Next.js + TypeScript에서 그림판 만들기 (Fabric.js vs Konva.js) 1. 프로젝트 배경Next.js에서 그림판 기능을 구현하려고 처음에는 fabric.js를 사용했다.하지만 몇 가지 문제 때문에 리액트 친화적인 konva.j
ayeongjin.tistory.com
지난번에는 Konva.js를 활용한 그림판 구현과 SSR 문제 해결까지 다뤘다.
이번에는 Undo/Redo 기능을 구현하고, 지우개 기능을 추가했다.
💡 추가한 기능
- undo(뒤로가기) redo(되돌리기) 기능 추가 사용자 경험을 위해 지우개를 사용해서 무작위로 선을 지우는 것보다, undo redo 기능을 추가해서 간편한 그림판을 제공하려고 했다.
- Redux에 drawing 상태 추가 undo, redo 기능을 구현하고 펜을 추가하다 보니 props로 받고 추가한 선 관리하는 로직이 복잡해져서 redux로 드로잉 상태를 관리했다.
- 지우개 기능 추가 이것도 사용자 편의를 위해 뭉개명서 지우는 지우개가 아닌 선택한 선 한 줄만 지우도록 했다.
1) Undo/Redo 기능 추가
✅ Undo/Redo 구현 방식
- history 배열: 이전 상태를 저장 → Undo 시, 마지막 상태를 불러옴
- redoStack 배열: Undo 후 다시 실행할 상태를 저장 → Redo 시, 해당 상태 복구
- 상태가 변경될 때마다 history를 업데이트하고, redoStack을 관리한다.
reducers 코드 (drawingSlice.ts)
reducers: {
// 1. 선 추가
addLine: (state, action: PayloadAction<{ points: number[] }>) => {
const newLine = {
points: action.payload.points,
stroke: state.selectedColor,
strokeWidth: state.selectedBrushSize,
};
state.history.push([...state.lines]); // 현재 상태를 저장
state.lines.push(newLine);
state.redoStack = []; // 새로운 선이 추가되면 Redo Stack 초기화
},
// 2. 선 업데이트 (이미 추가된 마지막선을 계속 수정한다)
updateLines: (state, action: PayloadAction<DrawingLine[]>) => {
state.lines = action.payload; // 기존 선 배열을 새로운 상태로 업데이트
},
// 3. 뒤로가기
undo: (state) => {
if (state.history.length > 0) {
const lastState = state.history.pop();
if (lastState) {
state.redoStack.push([...state.lines]); // 현재 상태를 redoStack에 저장
state.lines = lastState; // 이전 상태 복원
}
}
},
// 4. 되돌리기
redo: (state) => {
if (state.redoStack.length > 0) {
const redoState = state.redoStack.pop();
if (redoState) {
state.history.push([...state.lines]); // 현재 상태를 history에 저장
state.lines = redoState;
}
}
},
},
Stage 및 Line 렌더링 코드 (KonvaCanvas.tsx)
// 그림 그리기 시작
const handleMouseDown = (e: any) => {
if (selectedBrush === "eraser") {
handleErase(e); // 지우개 모드일 때는 선 삭제 실행
return;
}
setIsDrawing(true);
const pos = e.target.getStage().getPointerPosition();
dispatch(addLine({ points: [pos.x, pos.y] }));
};
// 그리는중
const handleMouseMove = (e: any) => {
if (!isDrawing || selectedBrush === "eraser") return;
const stage = e.target.getStage();
const point = stage.getPointerPosition();
const newLines = lines.map((line, index) =>
index === lines.length - 1
? { ...line, points: [...line.points, point.x, point.y] }
: line
);
dispatch(updateLines(newLines));
};
2) 지우개 기능 추가
Undo/Redo를 추가한 후, 지우개 기능을 구현했다.
Konva.js에서 기본 제공하는 지우개 도구가 없기 때문에, 클릭한 선을 삭제하는 방식으로 구현했다.
✅ 지우개 기능 구현 방식
- 펜 종류를 "eraser"로 설정하면, 선을 그리지 않고 삭제만 수행
- 클릭한 좌표 근처의 선을 찾아 삭제
- 삭제한 선도 Undo/Redo 가능하도록 관리
Redux 상태 변경 (drawingSlice.ts)
removeLine: (state, action: PayloadAction<number>) => {
state.history.push([...state.lines]); // 현재 상태를 Undo에 저장
state.lines = state.lines.filter((_, index) => index !== action.payload);
},
- removeLine() 실행 시, 삭제하기 전 상태를 history에 저장
- Undo 실행 시, 삭제한 선을 다시 복원할 수 있음
지우개 기능 적용 (KonvaCanvas.tsx)
const handleErase = (e: any) => {
if (selectedBrush !== "eraser") return;
const stage = stageRef.current;
const clickedPosition = stage.getPointerPosition();
// 클릭한 좌표에서 가장 가까운 선 찾기
const clickedLineIndex = lines.findIndex((line) =>
line.points.some(
(_, i) =>
i % 2 === 0 &&
Math.abs(line.points[i] - clickedPosition.x) < 10 && // X 좌표 근접 체크
Math.abs(line.points[i + 1] - clickedPosition.y) < 10 // Y 좌표 근접 체크
)
);
if (clickedLineIndex !== -1) {
dispatch(removeLine(clickedLineIndex)); // Redux에서 해당 선 삭제
}
};
✅ 지우개 기능 테스트
작업 | 결과 |
펜 모드에서 그림 그림 | 정상적으로 선이 추가됨 |
지우개 모드에서 클릭 | 해당 선이 삭제됨 |
Undo 실행 | 삭제한 선이 복원됨 |
Redo 실행 | 다시 삭제됨 |
완성 ~