본문 바로가기
개발개발/LifeGraph_인생그래프

[인생 그래프] 이미지 저장하기 (html2canvas + file-saver)

by yelimu 2025. 2. 11.

생성한 그래프를 이미지로 저장하는 방법을 구글링 하다 html2canvas 라는 라이브러리를 알게 되었다.

DOM 요소를 이미지로 저장해주는 라이브러리에는 대표적인 라이브러리가 몇개 더 있다.

그래프를 svg로 그려주고 있는데, 해당 라이브러리가 SVG 요소도 함께 캡쳐할 수 있기에 채택하게 되었다.

 

내가 경험한 바로는 이런 테스트 서비스는 채팅이나 sns로 공유해서 모바일로 실행하는 경우가 많았다.

그래서 모바일에서도 파일 저장하는 로직이 동일한가? 하는 궁금증이 있었다. 

 

몇 개의 블로그를 찾아보던 중 아래 블로그에서 html2canvas 만으로 이미지 다운로드를 구현했을때

아이폰 기준 Safari 브라우저 외에는 작동하지 않는다고 하며 솔루션을 제시해주셨다. 

https://yong-nyong.tistory.com/53

 

[React] 화면(DOM) 캡쳐 및 저장 기능 구현하기 (feat. html2canvas, file-saver)

📖 들어가며 React에서 화면 (DOM)를 캡처, 저장하는 기능에 대해서 알아보려 합니다. 작은 토이 프로젝트를 하면서 여러가지 문제를 맞이 했었는데, 결과적으로 성공한 방법들로 포스팅하게 됩니

yong-nyong.tistory.com

 

바로 file-saver 와 함께 사용하는 것이다. 

file-saver는 클라이언트에서 파일 저장을 도와주는 라이브러리 인데, 웬만한 브라우저와 호환된다고 한다

 

▼ 라이브러리 설치 방법 (React + TypeScript 기준)

npm install html2canvas

npm install file-saver --save

npm install @types/file-saver --save-dev

 

블로그에 적어주신 예시 코드 그대로 적었을 뿐인데 버튼에 빠방! ! 이미지 파일이 저장되었다. 

literally 입틀막

이미지 다운로드 구현을 처음 해보아서 너무 신기했다 

 

  //이미지 저장
  const imageRef = useRef<HTMLDivElement>(null);
  const handleDownload = async () => {
    const image = imageRef.current;
    if (!image) return;
    try {
      const canvas = await html2canvas(image, { scale: 2 });
      canvas.toBlob((blob) => {
        if (blob) {
          saveAs(blob, "life-graph.png");
        }
      });
    } catch (err) {
      console.log("이미지 저장 중 에러 발생", err);
    }
  };
  
  ...
  return (
        <button type="button" onClick={handleDownload}>
        🖼️ 저장
      </button>
      )

ref 영역 내에 캡쳐에서 제외하고 싶은 요소가 있다면?

data-html2canvas-ignore 속성을 추가해주면 된다!

속성을 명시했을때 기본값 true을 갖는다.

버튼을 제외한 영역이 캡쳐되었다

상위 컴포넌트에만 배경색을 지정해놨었는데, 캡쳐할때 포함이 안되어서 스타일에 추가해주었다😎

지피티 안쓰고 구현해서 더 기뿌다.. 

다음에 라이브러리가 제공한 기능을 구현해보는것도 좋을것같다.


클립보드에 복사하는 버튼도 추가해보았다

동일하게 html2canvas 를 사용해서 blob을 생성한다. 

전역 Navigator.clipboard 로 클립보드에 접근할 수 있다!

  //이미지 클립보드에 복사
  const handleCopy = async () => {
    const image = imageRef.current;
    if (!image) return;
    try {
      const canvas = await html2canvas(image, { scale: 2 });
      canvas.toBlob((blob) => {
        if (blob) {
          //클립보드 복사
          navigator.clipboard.write([
            new ClipboardItem({
              "image/png": blob,
            }),
          ]);
          alert("이미지가 클립보드에 저장되었습니다.");
        }
      });
    } catch (err) {
      console.log("이미지 클립보드 복사 중 에러 발생", err);
    }
  };

 

저장 핸들러와 중복되는 코드가 많기때문에 하나의 함수로 합쳐주고, 파라미터에 따라 분기 처리한다.

  const handleImageAction = async (action: "copy" | "save") => {
    const image = imageRef.current;
    if (!image) return;

    try {
      const canvas = await html2canvas(image, { scale: 2 });
      canvas.toBlob(async (blob) => {
        if (!blob) return;

        if (action === "copy") {
          //클립보드 복사
          await navigator.clipboard.write([
            new ClipboardItem({
              "image/png": blob,
            }),
          ]);
          alert("이미지가 클립보드에 저장되었습니다.");
        } else if (action === "save") {
          // 이미지 다운로드
          saveAs(blob, "life-graph.png");
        }
      });
    } catch (err) {
      console.log(`이미지 ${action === "copy" ? "클립보드 복사" : "다운로드"} 중 에러 발생`, err);
    }
  };

 

핸들러 함수에 파라미터를 넘겨줘야하므로 콜백함수 형태로 할당한다.

그렇지 않으면 함수를 실행한 결과값을 할당하게 되므로 버튼을 클릭하기도 전에 실행된다 

return (
<button type="button" onClick={() => handleImageAction("save")} data-html2canvas-ignore>
    🖼️ 저장
  </button>
  <button type="button" onClick={() => handleImageAction("copy")} data-html2canvas-ignore>
    📋 복사
  </button>)

 

이미지를 클릭하면 사이트로 이동합니다.