캔버스의 이미지는 각 노드에도 적용할 수 있고, Layer, Stage 단위 등 다양하게 이미지로 export 하는 기능이 잘 되어있다.

react-konva같은 경우는 각 요소에 접근하여 프로토타입 메서드를 사용하는 방식이 useRef 를 사용하는 방법이기 떄문에 useRef를 통해 먼저 stage를 가져올 필요가 있다.

interface StageStore {
  stage: null | RefObject<Konva.Stage>;
  registerStageRef: (stage: RefObject<Konva.Stage>) => void;
}
export const useStageStore = create<StageStore>((set) => ({
  stage: null,
  registerStageRef: (stage) =>
    set({
      stage: stage,
    }),
}));

먼저 캔버스 요소와 헤더의 export button은 분리되어 있는 컴포넌트이기 때문에 context API 혹은 전역 상태 관리로 등록해줄 필요가 있었다.

useRef의 경우 ref를 렌더링 된 요소에 등록해준다는 개념이기 때문에, useEffect를 통해 등록하면서 stage가 바뀔 때마다 이러한 부분이 반영되어야 한다고 생각했다. 또한 context api를 활용하기에는 보다 넓은 범위로 provider를 감싸야 했고, 굳이 실질적으로 렌더링에 영향을 끼치지 않는 요소가 context API로 관리되기보다는 zustand를 통한 전역 상태관리를 하는 것이 낫다고 판단했다.

export default function MindMapView() {
  const { data, undoData: undo, redoData: redo, updateNode, overrideNodeData, saveHistory } = useNodeListContext();
  const { dimensions, targetRef, handleWheel, zoomIn, zoomOut } = useDimension(data);
  const registerLayer = useCollisionDetection(data, updateNode);
  const stageRef = useRef();
  const { registerStageRef } = useStageStore();

  useEffect(() => {
    registerStageRef(stageRef);
  }, [stageRef]);

  const commandKeyMap = {
    z: undo,
    y: redo,
  };
  function handleReArrange() {
    const savedData = JSON.stringify(data);
    saveHistory(savedData);
    overrideNodeData(initializeNodePosition(JSON.parse(savedData)));
  }

  useWindowKeyEventListener("keydown", (e) => {
    if (e.ctrlKey || e.metaKey) {
      commandKeyMap[e.key]();
    }
  });

  return (
    <div ref={targetRef} className="relative h-full min-h-0 w-full min-w-0 rounded-xl bg-white">
      <Stage
        ref={stageRef}
        className="cursor-pointer"
        width={dimensions.width}
        height={dimensions.height}
        scaleX={dimensions.scale}
        scaleY={dimensions.scale}
        x={dimensions.x}
        y={dimensions.y}
        draggable
        onWheel={handleWheel}
      >
        <Layer ref={registerLayer}>
          {Object.keys(data).length >= 1 && <DrawMindMap data={data} root={data[1]} depth={1} />}
        </Layer>
      </Stage>
      <ToolMenu dimensions={dimensions} zoomIn={zoomIn} zoomOut={zoomOut} />
      {!Object.keys(data).length ? <NoNodeInform /> : <CanvasButtons handleReArrange={handleReArrange} />}
    </div>
  );
}

이런 식으로 stage가 바뀔 때마다 stage를 전역 상태에 등록해 놓을 수 있도록 하였으며, 이를 통해 헤더에서도 stage를 가져올 수 있었다.

downloadURI(stage.current.getStage().toDataURL({ mimeType: "image/png", quality: 1 }), "demo");

export function downloadURI(uri, name) {
  let link = document.createElement("a");
  link.download = name;
  link.href = uri;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

헤더에서는 konva에서 제공하는 api인 toDataURL을 통해 데이터를 URL 타입으로 바꾸어 주었다. toImage로도 바꿀 수 있긴 하지만 굳이 toDataURL을 사용하는 이유는 해당 이미지를 다운로드 해야하기 때문이다. 받은 이미지를 다운로드하기 위해서는 data에 대한 URL이 필요했기 떄문에 toDataURL을 사용하게 되었다.

간단하게 이미지 형식과 비율을 정해준 뒤, downloadURI 라는 함수를 만들었다.

이 함수의 역할은 클릭했을 때 다운로드가 가능할 수 있도록 해주는 역할이다. 다운로드는 a태그의 프로퍼티에 download attribute를 설정하여 구현할 수 있다.

하지만 이 다운로드 링크의 경우 한번 사용한 뒤 재사용되지 않으므로 링크는 일회성을 사용되기 위해서 DOM API를 사용하여 a태그를 만들고, 클릭 이벤트를 실행시킨 뒤 다시 지워주는 작업을 거쳤다.