[Fabric.js] 3. 실행 취소, 다시 실행 버튼 만들기

chaevivi·2024년 3월 18일
0

Fabric.js

목록 보기
3/3
post-thumbnail

이전 포스트에서는 Fabric.js에서 동적으로 속성 값을 설정하는 방법에 대해서 알아보았습니다. 이번 포스트에서는 캔버스의 그림이나 이미지 등을 실행 취소하거나 다시 실행할 수 있는 버튼을 만들어 보겠습니다.



1. Fabric.js의 단점

Fabric.js는 강력한 캔버스 라이브러리이지만 단점이 한 가지 있습니다. 바로 드로잉 모드일 때 사용할 수 있는 지우개 기능이 없다는 것입니다. 그래서 그림을 그릴 수는 있지만 그림을 지울 수는 없습니다.

이미지 같은 경우에는 삭제 기능을 컨트롤 할 수 있는 deleteControl이 존재하기 때문에 삭제 버튼을 구현하여 이미지를 삭제 할 수 있습니다. (자세한 내용은 공식 문서로 확인하세요.)

이러한 이유로 지우개 기능 대신 실행 취소와 다시 실행 버튼을 만들어 그림 그린 것을 취소하고 그렸던 것을 다시 불러오도록 할 것입니다. 원하는 부분만 지울 수는 없지만 비슷하게 지우개 기능을 구현할 수 있습니다.



2. 실행 취소

먼저 실행 취소 버튼을 만들어 보겠습니다. 버튼을 생성한 후 버튼에 onClick 이벤트를 추가합니다.

const handleUndoClick = () => {
  // ...
};

return (
  <>
  	<button onClick={() => handleUndoClick()}></button>
  </>
);

이제 실행 취소 버튼의 동작 원리를 생각해 봅시다. 실행 취소 버튼을 계속 누르면 가장 최근에 캔버스에 추가된 것부터 삭제됩니다. 그렇다면 캔버스에 추가된 순서와 내용을 모두 알아야 합니다.


const [isLocked, setIsLocked] = useState(false);
const [history, setHistory] = useState<fabric.Object[] | undefined>([]);
  • isLocked
    • isLocked는 다시 실행할 때 필요한 상태값입니다.
  • history
    • 캔버스에 추가되는 것들은 모두 객체로 저장됩니다. 그렇기 때문에 상태 타입을 fabric.Object[]로 설정해 주세요.
    • history실행 취소 버튼을 클릭할때마다 history 배열에 제일 마지막 객체가 저장됩니다.

const saveHistory = () => {
    if (!isLocked) {
      setHistory([]);
    }
    setIsLocked(false);
  };

useEffect(() => {
    if (canvas) {
      canvas.on('object:added', saveHistory);
      canvas.on('object:modified', saveHistory);
      canvas.on('object:removed', saveHistory);
    }
  }, [canvas]);
  • saveHistoy 함수
    • saveHistory 함수는 isLockedfalse일 때 setHistory의 배열을 빈배열로 초기화합니다. isLockedtrue이면 isLockedfalse로 변경합니다.
    • saveHistory 함수는 히스토리에 저장하는 것을 제어합니다.
  • useEffect
    • canvas가 null이나 undefined 일수도 있기 때문에 꼭 확인하는 과정이 있어야 합니다.
    • fabric.js는 on 메서드로 이벤트를 설정할 수 있습니다.
      • object:added: 객체 추가 후 실행
      • object:modified: 객체 수정(이동, 확장, 회전) 후 실행
      • object:removed: 객체 삭제 후 실행
    • canvas에 이벤트가 발생할때마다 saveHistory 함수를 실행합니다.

이제 버튼에 걸어 두었던 이벤트 함수를 작성해 봅시다.

const handleUndoClick = () => {
    if (canvas) {
      if (canvas._objects.length > 0){
        const poppedObject = canvas._objects.pop();
        setHistory((prev: fabric.Object[] | undefined) => {
          if (prev === undefined) {
            return poppedObject ? [poppedObject] : [];
          }
          return poppedObject ? [...prev, poppedObject] : prev;
        });
        canvas.renderAll();
      }
    }
  };
  • 잊지 않고 canvas가 null이거나 undefined가 아닌지 확인합니다.
  • canvas에 추가된 객체들은 _objects에 저장되고 다양한 메서드를 활용하여 접근할 수 있습니다.
  • canvas._objects.length로 캔버스에 추가된 객체의 개수를 확인할 수 있습니다. 객체의 개수가 하나라도 있다면 history 상태 배열에 저장해야 합니다.
    • canvas._objects.pop()으로 제일 마지막 객체를 꺼내 poppedObject 변수에 저장합니다.
    • 그리고 setHistory로 꺼낸 객체를 저장합니다.
  • renderAll() 메서드로 캔버스를 렌더링해줍니다.
    • 이때 canvas._objects에 있던 객체들 중 아까 꺼냈던 마지막 객체를 뺀 나머지 객체들이 캔버스에 다시 렌더링됩니다.

크게 흐름을 다시 정리해봅시다.

객체 추가, 수정, 삭제 이벤트 발생 시 ➡️ saveHistory 함수 실행
'실행 취소' 버튼 누르면 ➡️ _objects의 맨 마지막 객체 꺼냄 ➡️ history에 저장 ➡️ renderAll() ➡️ 캔버스에 마지막 객체 뺀 나머지 객체들이 렌더링



3. 다시 실행

다시 실행은 버튼을 눌렀을 때 실행 취소했던 것들을 다시 불러와야 합니다. 이를 위해 history와 isLocked 상태가 필요하였습니다. 실행 취소와 마찬가지로 버튼을 생성 후 onClick으로 이벤트를 추가합니다.


const handleRedoClick = () => {
    if (canvas && history) {
      if (history.length > 0) {
        setIsLocked(true);
        canvas.add(history[history.length - 1]);
        const newHistory = history.slice(0, -1);
        setHistory(newHistory);
      }
    }
  };
  • canvas가 null이나 undefined가 아닌지 확인하고 history 배열에 값이 있는지 확인합니다.
  • history 배열에 값이 하나라도 있으면
    • isLocked를 true로 바꿔주고
    • add 메서드로 history의 가장 마지막 객체를 canvas에 추가합니다.
    • 그 다음 마지막 객체를 뺀 배열로 history를 다시 업데이트합니다.

흐름을 다시 정리해봅시다.

'다시 실행' 버튼 클릭 ➡️ isLocked = true, 캔버스에 history의 가장 마지막 객체가 추가 ➡️ 가장 마지막 객체를 뺀 나머지 객체 배열을 history에 다시 업데이트



완성하면 이런 모습입니다!




참고
🔗Fabric.js 이벤트: https://github.com/fabricjs/fabric.js/wiki/Working-with-events

profile
직접 만드는 게 좋은 프론트엔드 개발자

0개의 댓글