개발일지/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에서 기본 제공하는 지우개 도구가 없기 때문에, 클릭한 선을 삭제하는 방식으로 구현했다.

 

지우개 기능 구현 방식

  1. 펜 종류를 "eraser"로 설정하면, 선을 그리지 않고 삭제만 수행
  2. 클릭한 좌표 근처의 선을 찾아 삭제
  3. 삭제한 선도 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 실행 다시 삭제됨

 

 

완성 ~