개발/개발로그

[React] 간단한 성냥퍼즐 웹 서비스 만들기 6일차 (리팩토링, History 기능)

seungho-dev 2024. 11. 24. 14:28

오늘은 지금까지 코드를 정리해 보고 히스토리 기능을 만들어 보려고 합니다!

 

고고

개인적인 공부를 위해 작성하는 블로그입니다. 혹시라도 잘못되거나 부족한 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

 

💡 핵심 기능 구현

  • 성냥개비 드래그 & 드롭 (완료)
  • 성냥개비 회전 (완료)
  • (new) Match 컴포넌트 분리, History 기능 (오늘 할 것)
  • 퍼즐 정답 검증 시스템

정답 검증 시스템을 만들기 전에 PuzzleCanvas.jsx 가 커져 <Image/> 내용을 Matchsitck.jsx로 분리하였다.

import { Image } from "react-konva";

export default function Matchstick({ stick, image, isSelected, onSelect, onDragMove }) {
  // console.log(isSelected)
  return (
    <Image
      id={stick.id}
      x={stick.x}
      y={stick.y}
      rotation={stick.angle}
      width={18}
      height={150}
      image={image}
      draggable
      onTap={() => onSelect(stick.id)} // 모바일 지원
      onClick={() => onSelect(stick.id)} // 선택
      onDragMove={(e) => onDragMove(e, stick.id)} // 이동
    />
  )
}

 

그리고 배경을 클릭했을 때 선택해제를 해주기 위해 <Stage/> 에 onClick에 setSelectedMatchstick을 null로 해줬는데

무슨 일인지 선택이 안되게 되었다. 

onClick={() => setSelectedMatchstick(null)}

콘솔창에 확인해 보니 성냥을 선택했을 때도 이벤트 버블링으로 인해 Stage에 적용된 onClick 함수가 실행된 것 같아서 다음과 같이 e.target.getStage()로 현재 이벤트 타깃이 Stage일 때만 setSelectedMatchstick(null)이 실행되도록해 해결하였다.

  const handleBackgroundClick = (e) => {
    if (e.target === e.target.getStage()) {
      setSelectedMatchstick(null)
    }
  }

 

💁🏻 History 기능

히스토리 기능을 추가하려고 보니 현재 onDragMove로 움직일 때마다 저장하기에는 비효율 적일 것 같아서 찾아보니 onDrageEnd가 있어서 바꿔주었다.

  const handleDragMove = (e, id) => {
    const newPosition = { x: e.target.x(), y: e.target.y() };
    setMatchsticks((prev) =>
      prev.map((stick) =>
        stick.id === id ? { ...stick, ...newPosition } : stick
      )
    );
    setState(`성냥ID: ${id} 상태: 드래그 중`)
  };

  const handleDragEnd = (e, id) => {
    // 드래그 완료 후 위치 업데이트
    const newPosition = { x: e.target.x(), y: e.target.y() }
    setMatchsticks((prev) =>
      prev.map((stick) =>
        stick.id === id ? { ...stick, ...newPosition } : stick
      )
    )
    // 상태 저장
    setState(`성냥ID: ${id} 상태: 드래그 완료`)
  }

 

이제 history를 기록할 state 배열과 현재 상태를 저장해 둘 currentStep을 만들어 보자

const [history, setHistory] = useState([]); // 상태 기록
const [currentStep, setCurrentStep] = useState(-1); // 현재 상태 포인터

 

다음으로는 변경이 있어났을 때 어떤 방식으로 history를 기록할 건지 생각해 봐야 하는데 아래와 같이 두 가지의 방법이 생각났다

1.  전체를 스냅샷으로 기록하는 방식
2. 변화된 부분만 기록하는 방식

 

1번은 쉽고 간단할 것 같지만 성냥하나의 움직임에 대하여 전체 배열을 저장하는 것은 비효율적일 것 같아 2번으로 정했다.

2번은 작업이 실행할 때 원래 상태로의 복구를 위해 변경 전 상태와 변경 후 상태를 모두 저장해야 해서 다음과 같이 구성해 봤다.

[
  { type: "move", id: "1", before: { x: 50, y: 50 }, after: { x: 100, y: 100 } },
  { type: "rotate", id: "2", before: { angle: 30 }, after: { angle: 45 } },
  ...
]

 

위의 형식에 맞춰서 저장하는 로직과 handleDragEnd()와 handleRoateEnd()를 수정해 주었다.

const handleDragEnd = (e, id) => {
    // 드래그 완료 후 위치 업데이트

    // 정수로 반올림
    const x = Math.round(e.target.x())
    const y = Math.round(e.target.y())

    const newPosition = { x, y }
    const findOne = matchsticks.find((stick) =>  stick.id === id)
    console.log('findOne: ', findOne)

    setMatchsticks((prev) =>
      prev.map((stick) =>
        stick.id === id ? { ...stick, ...newPosition } : stick
      )
    )
    // 상태 저장
    const before = {x: findOne.x, y: findOne.y }
    const after = { x, y }
    saveState("move", id, before, after)
  }

  const handleRotateEnd = (newAngle, id) => {
    // 정수로 반올림
    const roundedAngle = Math.round(newAngle)
    const findOne = matchsticks.find((stick) =>  stick.id === id)

    // 회전 완료 후 각도 업데이트
    setMatchsticks((prev) =>
      prev.map((stick) => 
        stick.id === id ? { ...stick, angle: roundedAngle} : stick
      )
    )
    // 상태 저장
    const before = {angle: findOne.angle}
    const after = {angle: newAngle}
    saveState("rotate", id, before, after)
  }

  const saveState = (type, id, before, after) => {
    const newHistory = history.slice(0, currentStep + 1) // 현재 상태 이후의 기록 삭제
    newHistory.push({ type, id, before, after}) // 새로운 상태 저장
    setHistory(newHistory)
    setCurrentStep(newHistory.length - 1) // 현재 상태를 마지막으로 이동
  }

콘솔 창으로 확인해 보니 잘 저장되는 것 같다. 내일은 undo와 redo를 만들어보고 횟수제한도 어떻게 구현해야 할지 생각해 봐야겠다.

 

 

 

읽어주셔서 감사합니다~!