개발/개발로그

[React] 간단한 성냥퍼즐 웹 서비스 만들기 7일차 (Undo, Redo, Remove구현)

seungho-dev 2024. 11. 25. 13:41

오늘은 저번시간 구현한 history 스택에 저장되는 데이터를 가지고 Undo, Redo 구현과 지우는 문제를 구현하기 위해

Remove기능을 만들고 퍼즐에 수정 횟수를 제한하여 문제풀이가 가능하도록 만들어보겠습니다.

 

오늘도 가보자~!

 

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

 

💡 핵심 기능 구현

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

💁🏻 Undo, Redo 구현

지금 히스토릴 배열에 변경된 내용을 저장하고 현재 스탭을 currentStep으로 가리키고 있는데 undo()는 currentStep -1의 내용을 가져와 id를 확인해 before에 있는 x, y, angle 값으로 변경하고 redo()는 반대로 구현하였다.

 

  const undo = () => {
    if (currentStep >=0) {
      const { id, before } = history[currentStep]

      setMatchsticks((prev) =>
        prev.map((stick) =>
          stick.id === id ? {...stick , ...before} : stick
        )
      )
      setCurrentStep(currentStep - 1); // 이전 단계로 이동
    }
  }
  const redo = () => {
    if (currentStep < history.length - 1) {
      const { id, after } = history[currentStep + 1]

      setMatchsticks((prev) =>
        prev.map((stick) =>
          stick.id === id ? {...stick, ...after} : stick
        )
      )

      setCurrentStep(currentStep + 1)
    }
  }

 

잘 동작했는데 이미지 포지션 기준점과 회전중심이 달라 회전상태 변경으로 포지션이 달라지기 때문에 현재 변경된 부분만 반영하도록 한 히스토리에서 회전 시 변경된 회전값만 저장하고 있어 다음과 같이 회전 후 Undo 했을 때 포지션이 바뀌는 문제가 생겼다.

이를 해결하려면 회전 시 바뀐 x, y값도 저장하거나 이미지 기준점을 회전 중심축과 동일하게 설정하면 된다고 생각했는데 첫 번째 방법은 비효율적일 것 같아 두 번째 방법인 기준점을 변경하는 방법을 찾아보니 offset 옵션을 통해 설정할 수 있다고 한다.

<Image
      id={stick.id}
      x={stick.x}
      y={stick.y}
      rotation={stick.angle}
      width={18}
      height={150}
      offset={{
        x: 9, // 이미지 폭의 절반
        y: 75, // 이미지 높이의 절반
      }}
      image={image}
      draggable
      onTap={() => onSelect(stick.id)} // 모바일 지원
      onClick={() => onSelect(stick.id)} // 선택
      onDragEnd={(e) => onDragEnd(e, stick.id)}
      onTransformEnd={(e) => onTransformEnd(e.target.rotation(), stick.id)}
    />

 

👨🏻‍💻 Remove 구현

이걸 어떻게 구현할까 고민을 해봤는데 검색을 하기 전에 그냥 떠오르는 생각으로는 지웠다는 상태를 나타내는 방법으로 성냥개비 구조체에 상태를 하나 만들거나 x , y 가 -1로 지워진 상태를 표시하는 것도 가능할 것 같다. 우선 isDeleted 플래그를 사용하는 방식으로 구현했다.

  const remove = () => {
    if (selectedMatchstick) {
      const select = matchsticks.find((stick) => stick.id === selectedMatchstick)

      setMatchsticks((prev) =>
        prev.map((stick) =>
          stick.id === selectedMatchstick ? {...stick, isDeleted: true} : stick
        )
      )
      setSelectedMatchstick(null)
      const before = {...select}
      const after = { ...select, isDeleted: true}
      saveState("remove", selectedMatchstick, before, after)
    }
  }
  ...(생략)
  
  {matchsticks
          .filter((stick) => !stick.isDeleted) // isDeleted 필터
          .map((stick) => (
          <Matchstick
  ...(생략)

 

 

이렇게 했는데 진짜 이상한 게 Undo가 안 되는 문제가 발생했다..

console창에 확인해 보니 undo를 해도 여전히 isDeleted가 true로 되어있었다. 그렇다면 before에 있는 이전에 select에 담아놓은 객체가 변경되었나 싶어 const deep_copy_select = JSON.parse(JSON.stringify(select))나 structuredClone을 사용해 깊은 복사를 해주었는데도 여전히 같은 문제가 있었다. 

그래서 그냥 before에도 isDeleted: false를 넣어주었는데 Undo가 작동했다...

  const remove = () => {
    if (selectedMatchstick) {
      const select = matchsticks.find((stick) => stick.id === selectedMatchstick)
      
      setMatchsticks((prev) =>
        prev.map((stick) =>
          stick.id === selectedMatchstick ? {...stick, isDeleted: true} : stick
        )
      )
      setSelectedMatchstick(null)
      const before = {...select, isDeleted: false}
      const after = { ...select, isDeleted: true}
      saveState("remove", selectedMatchstick, before, after)
    }
  }

 

 

원인은 그냥 Undo과정에서 예전의 값을 덮어쓰는 방식인데 before에 저장된 isDeleted 값 자체가 없어서 기존에 있던 isDeleted: true가 남아 있어서 그런 거였다..

잘 작동하는 것 같다. 다음 주는 여행으로 한 주 쉬고 다녀와서 횟수제한 정답검증과 전체적인 UI를 손봐야겠다.

 

다음주에 봐요~!