본문 바로가기
React

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

by Vintz 2021. 7. 7.
반응형

프로젝트를 진행하면서 컴포넌트를 이미지로 저장하는 기능을 추가하고 싶어서 관련 자료를 찾기 시작했다. 처음엔 html2canvasjspdf로 기능을 구현하려고 구글링을 하다 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)

또 하나의 문제는 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 메서드에 힌트가 있지 않을까?

toBlob 메서드를 자세히 들여다보니 두번째 매개변수에 옵션을 넣을 수 있다. 함수도 넣을 수 있는걸 보니 이걸로 버튼 태그를 제외할 수 있지 않을까? 비슷한 사용법이 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을 적용하는 방법이 가장 간단하다.

반응형