SprintersSprinters

[React.js] 컴포넌트 저장 기능 구현하기(dom-to-image, FileSaver)

프로젝트를 진행하면서 컴포넌트를 이미지로 저장하는 기능을 추가하고 싶어서 관련 자료를 찾기 시작했다. 처음엔 html2canvas와 jspdf로 기능을 구현하려고 구글링을 하다 dom-to-image, FileSaver 라이브러리를 알게 되었고 이 조합 코드가 좀 더 직관적이고 사용법이 좋은 것 같아서 결정하게 되었다.(그리고 좀 더 기능 구현 목적에 알맞다.)

dom-to-image와 FileSaver 설치


npm install dom-to-image
npm install file-saver --save
또는
yarn add dom-to-image
yarn add file-saver

기능 구현

각 컴포넌트에 다운로드 버튼이 있고 버튼을 클릭하면 해당 컴포넌트를 PNG 이미지로 저장하도록 구현한다.


import React from 'react';
import './card.css';
import domtoimage from 'dom-to-image';
import { saveAs } from 'file-saver';

const Card = () => {
  // 컴포넌트 다운로드 함수
  const onDownloadBtn = () => {
    domtoimage.toBlob(document.querySelector('.card')).then(blob => {
      saveAs(blob, 'card.png');
    });
  };

  return (
    <li className="card">
      <h1>카드 컴포넌트</h1>
      <button className="downBtn" onClick={onDownloadBtn}>
        다운로드 버튼
      </button>
    </li>
  );
};

export default Card;

깃허브 페이지에 사용법이 아주 잘 나와있는데 PNG 이미지로 저장하는 코드는 다음과 같다.


const onDownloadBtn = () => {
  // dom-to-image
  domtoimage
    .toBlob(document.querySelector('.card'))
    .then((blob) => {
      // FileSaver
      saveAs(blob, 'card.png');
    });
};

코드를 보면

  1. toBlob 메서드를 통해 이벤트가 일어날 타겟을 선택한 후

  2. saveAs 메서드로 해당 타겟을 card.png로 저장 시켜주는 것 같다.

이렇게 하면 동작은 하지만 여러 이슈가 존재한다..!

01. 리액트스럽지 않은 DOM 선택

JavaScript를 사용할 땐 getElementById, querySelector같은 DOM Selector 함수로 DOM을 선택하지만 리액트는 useRef라는 Hook 함수를 사용한다. 관련 내용

DOM을 선택하는 JS 방식과 React 방식 (stackoverflow)
DOM을 선택하는 JS 방식과 React 방식 (stackoverflow)

또 하나의 문제는 querySelector로 넣어주게 되면 각 컴포넌트마다 다운로드 기능을 적용할 수가 없다. (방법을 찾지 못했다.)

두번째 컴포넌트 버튼을 클릭 했지만 첫번째 컴포넌트가 저장이 된다.
두번째 컴포넌트 버튼을 클릭 했지만 첫번째 컴포넌트가 저장이 된다.

리액트 방식으로 노드를 선택할 경우 Ref의 .current 값으로 원하는 DOM을 선택하여 해결할 수 있다.


import React, { useRef } from 'react';
import './card.css';
import domtoimage from 'dom-to-image';
import { saveAs } from 'file-saver';

const Card = () => {
  const cardRef = useRef();
  const onDownloadBtn = () => {
    const card = cardRef.current;
    domtoimage.toBlob(card).then(blob => {
      saveAs(blob, 'card.png');
    });
  };

  return (
    <li ref={cardRef} className="card">
      <h1>카드 컴포넌트</h1>
      <button className="downBtn" onClick={onDownloadBtn}>
        다운로드 버튼
      </button>
    </li>
  );
};

export default Card;

02. 이미지에 다운로드 버튼 없애기

이제 다운로드 기능은 제대로 실행이 되지만 저장되는 이미지에 다운로드 버튼을 빼고 싶다. 그렇다면 toBlob 메서드에 힌트가 있지 않을까?

스크린샷 2021-07-07 오후 5.19.00.png
스크린샷 2021-07-07 오후 5.20.42.png

toBlob 메서드를 자세히 들여다보니 두번째 매개변수에 옵션을 넣을 수 있다. 함수도 넣을 수 있는걸 보니 이걸로 버튼 태그를 제외할 수 있지 않을까? 비슷한 사용법이 dom-to-image 깃허브 페이지에 있었다.

dom-to-image 깃허브 페이지
dom-to-image 깃허브 페이지

이걸 응용해서 다음과 같이 해결했다.


const cardRef = useRef();
const onDownloadBtn = () => {
  const card = cardRef.current;
  const filter = (card) => {
    return card.tagName !== 'BUTTON';
  };
  domtoimage
    .toBlob(card, { filter: filter })
    .then((blob) => {
      saveAs(blob, 'card.png');
    });
};
버튼 요소를 제거한 이미지
버튼 요소를 제거한 이미지

03. margin 이슈

마지막으로 컴포넌트에 다양한 경우로 margin을 사용할 때가 있는데 이미지를 저장 시 해당 노드에 margin이 있을 경우 그 margin만큼 이미지가 잘려서 저장이 된다. 해결방법은 여러 방법이 있겠지만 요소를 한번더 감싸서 상위 요소에 margin을 적용하는 방법이 가장 간단하다.

김병연 프로필 이미지

작성자

김병연

기록 164개

안녕하세요, Sprinters를 만든 사람입니다.

시리즈와 댓글을 불러오는 중…