Skip to content

React:Examples:ExportPdfForHiddenComponents

숨겨놓은 React컴포넌트를 PDF 로 저장하는 예시.

미디어 쿼리 안쓰는 이유

@media print는 브라우저 인쇄 기능(Ctrl+P)을 사용했을 때만 작동한다.

따라서 별도의 프린트 팝업 없이 바로 PDF 다운로드 하고싶다면 아래와 같이 진행해야 한다.

iframe 사용하지 않는 이유

  • iframe은 완전히 독립된 문서예요.
  • React 컴포넌트는 Virtual DOM 안에서 작동하기 때문에, 다른 DOM(DocumentContext)에 직접 삽입하려면 별도의 렌더링 방식이 필요해요.
  • iframe은 CSS 격리가 되기 때문에, 앱의 전역 스타일이나 테마가 iframe에 적용되지 않아요.
  • 따로 CSS를 로딩하거나 인라인으로 전달해줘야 함.
  • props, context, 상태 등을 iframe으로 넘기기 위해선 postMessage, JSON serialization 등 추가적인 작업 필요.
  • 간단하게 컴포넌트를 넘길 수 없음.

숨겨 놓은 HTML 요소 만들기

<div
  ref={componentRef}
  style={{
    position: "fixed",
    top: 0,
    left: 0,
    width: "210mm",
    height: "297mm",
    zIndex: -9999,
    opacity: 0,
    pointerEvents: "none",
    overflow: "hidden",
  }}
>
  <OtherPageComponent />
</div>

스타일 속성

이유

position: fixed

스크롤과 무관하게 화면 고정

z-index: -9999

화면 제일 뒤로 (보이지 않게)

opacity: 0

눈에 안 보이게

pointer-events: none

클릭 등 이벤트 차단

overflow: hidden

혹시 내부 넘침 방지

width/height

PDF 출력에 맞는 정확한 크기

Export PDF 아이콘 다운로드

https://icones.js.org/ 에서 적당한 아이콘 다운로드.

import {SVGProps} from 'react';

export function BiFileEarmarkPdf(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="1em"
      height="1em"
      viewBox="0 0 16 16"
      {...props}
    >
      <g fill="currentColor">
        <path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"></path>
        <path d="M4.603 14.087a.8.8 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102c.198-.307.526-.568.897-.787a7.7 7.7 0 0 1 1.482-.645a20 20 0 0 0 1.062-2.227a7.3 7.3 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136c.075-.354.274-.672.65-.823c.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538c.007.188-.012.396-.047.614c-.084.51-.27 1.134-.52 1.794a11 11 0 0 0 .98 1.686a5.8 5.8 0 0 1 1.334.05c.364.066.734.195.96.465c.12.144.193.32.2.518c.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416a.86.86 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.7 5.7 0 0 1-.911-.95a11.7 11.7 0 0 0-1.997.406a11.3 11.3 0 0 1-1.02 1.51c-.292.35-.609.656-.927.787a.8.8 0 0 1-.58.029m1.379-1.901q-.25.115-.459.238c-.328.194-.541.383-.647.547c-.094.145-.096.25-.04.361q.016.032.026.044l.035-.012c.137-.056.355-.235.635-.572a8 8 0 0 0 .45-.606m1.64-1.33a13 13 0 0 1 1.01-.193a12 12 0 0 1-.51-.858a21 21 0 0 1-.5 1.05zm2.446.45q.226.245.435.41c.24.19.407.253.498.256a.1.1 0 0 0 .07-.015a.3.3 0 0 0 .094-.125a.44.44 0 0 0 .059-.2a.1.1 0 0 0-.026-.063c-.052-.062-.2-.152-.518-.209a4 4 0 0 0-.612-.053zM8.078 7.8a7 7 0 0 0 .2-.828q.046-.282.038-.465a.6.6 0 0 0-.032-.198a.5.5 0 0 0-.145.04c-.087.035-.158.106-.196.283c-.04.192-.03.469.046.822q.036.167.09.346z"></path>
      </g>
    </svg>
  );
}

export default BiFileEarmarkPdf;

툴팁이 적용된 아이콘 버튼 적용:

<div
  className="tw-group tw-relative tw-flex tw-justify-center tw-items-center"
  onClick={e => onClickExportPdf(e, d)}
>
  <BiFileEarmarkPdf className="tw-text-center tw-text-lg tw-m-1" />
  <span className="group-hover:tw-opacity-70 tw-transition-opacity tw-bg-gray-500 tw-py-1 tw-px-2 tw-text-xs tw-text-gray-100 tw-rounded-md tw-absolute tw-left-1/2 -tw-translate-x-1/2 tw-translate-y-full tw-opacity-0 tw-m-4 tw-mx-auto">
    PDF
  </span>
</div>

적당한 위치에 버튼과 이벤트 핸들러 추가

import BiFileEarmarkPdf from '../icons/BiFileEarmarkPdf';
import styles from './DailyWorkStatus.module.scss';

// ...
export default function DailyWorkStatus() {
  // ...
  const pdfLayoutRef = useRef<HTMLDivElement>(null);
  const onClickExportPdf = (
    e: React.MouseEvent<SVGSVGElement, MouseEvent>,
    data: DailyInfoData
  ) => {
    e.stopPropagation();
  };
  // ...
  <BiFileEarmarkPdf onClick={e => onClickExportPdf(e, d)} />
  // ...
  <div ref={pdfLayoutRef} className={styles.pdfLayout}></div>
  // ...
}

See also