치춘짱베리굿나이스

React 컴포넌트 또는 HTML 문서 일부를 pdf로 내보내기 본문

ClientSide/라이브러리

React 컴포넌트 또는 HTML 문서 일부를 pdf로 내보내기

치춘 2023. 8. 17. 14:16

내보내기

리액트 컴포넌트 또는 html 요소를 구성하면 이를 pdf로 만들어주는 기능이 필요했다

라이브러리를 알아보니 크게 두 가지로 갈리는 것 같은데 두 라이브러리를 모두 써보고 느낀 점을 적어보려고 한다

사실 라이브러리 선택하기 위해 팀원 보여주려고? 설득하려고?.. 쓴 글임

최근까지도 업데이트가 활발한 두 라이브러리 케이스를 가져와 보았다

React-pdf

링크

https://github.com/diegomura/react-pdf

https://react-pdf.org

설명

리액트를 이용하여 pdf를 생성해주는 라이브러리

pdf 뷰어는 https://github.com/wojtekmaj/react-pdf 쪽으로 가라고 한다

자체적으로 지원하는 컴포넌트들을 조합하여 pdf 페이지를 구성하고, 이를 바로 pdf 파일로 내보내기 하거나 뷰어를 렌더링할 수도 있다

사용방법

import { Document, Page, Font } from '@react-pdf/renderer';
  • Document
    • pdf 문서 그 자체를 담당한다
    • 후술할 컴포넌트 트리의 가장 루트에 위치해야 한다
  • Page
    • pdf 문서의 각 페이지를 담당한다
    • Prop으로 페이지의 크기를 지정해줄 수 있다 (후술할 예시에서 필자는 A4 사이즈로 지정했다)
  • View
    • UI를 구성하기 위한 컴포넌트로, 일반 html 에서 div랑 비슷한 기능을 한다
  • Text
    • 텍스트를 출력하기 위한 컴포넌트
  • Link
    • 하이퍼링크를 출력하기 위한 컴포넌트
  • Image
    • jpg 또는 png 이미지를 출력하기 위한 컴포넌트

그 외에도 Canvas, Note 등이 있긴 한데 여기서는 생략하도록 한다

 

import { StyleSheet } from '@react-pdf/renderer';

const styles = StyleSheet.create({
    page: {
        flexDirection: 'row',
        backgroundColor: '#ffffff',
    },
    section: {
        marginBottom: 10,
        padding: '10px 20px',
        border: '1px solid black',
    },
    text: {
    fontFamily: "Nanum Gothic",
  },
});

StyleSheet은 pdf의 각 요소의 스타일을 지정할 수 있는 스타일시트를 생성한다

값들이 css와 꽤나 유사하지만 완벽하게 같지는 않다

 

import { Font } from '@react-pdf/renderer';

Font.register({
  family: "Nanum Gothic",
  src: "https://fonts.gstatic.com/ea/nanumgothic/v5/NanumGothic-ExtraBold.ttf",
});

폰트는 Font 컴포넌트를 통해 register 할 수 있다

기본적으로 한국어가 깨지기 때문에 한국어가 지원되는 폰트를 추가해주어야 한다

폰트 지정을 해주었다면, StyleSheet에서 fontFamily 스타일을 설정해주는 것을 잊지 말자

 

const MyDocument = () => (
  <Document>
    <Page size="A4" style={styles.page}>
      <View style={styles.section}>
        <Text>가나다</Text>
      </View>
      <View style={styles.section}>
        <Text>hello</Text>
      </View>
      <View style={styles.section}>
        <Text>hello</Text>
      </View>
    </Page>
  </Document>
);

PDF는 @react-pdf/renderer 에서 제공해주는 컴포넌트들을 조합하여 완성한다

Document는 PDF 문서의 가장 토대가 되는 컴포넌트가 되고, 그 위에 Page 컴포넌트를 이용하여 페이지를 추가하고, 각 페이지 안에 ViewText, Image 등의 조합을 이용하여 페이지 내용물을 구성하는 방식이다

 

<PDFDownloadLink
    document={<MyDocument />}
    fileName="resume.pdf"
>
    <span>다운로드</span>
</PDFDownloadLink>

이렇게 만들어진 pdf 문서는 PDFDownloadLink 컴포넌트를 이용하여 다운로드 링크를 추가할 수 있고, 이 링크를 클릭하면 pdf를 다운로드받을 수 있다

 

<PDFDownloadLink
    document={<MyDocument data={contents} />}
    fileName="resume.pdf"
>
    {({ blob, url, loading, error }) => (
        <PDFDownloadChildren
            blob={blob}
            url={url}
            loading={loading}
            error={error}
        />
    )}
</PDFDownloadLink>

자식으로는 그냥 컴포넌트를 넘겨도 되는데, 위처럼 function as child 형식으로 값을 받아올 수도 있다

  • loading은 boolean 값으로, 문서가 로딩 중인지 아닌지를 나타낸다
  • blob은 Blob (Binary Large Object) 파일을 담고 있다
  • url은 다운로드 url을 담고 있다
  • error는 에러 발생 시, 해당 에러 객체 (Error 타입) 를 갖고 있다

 

<PDFViewer>
    <MyDocument />
</PDFViewer>

PDFViewer 컴포넌트를 사용하면 만든 pdf 문서를 미리보기까지 할 수 있다

 

html을 열어보면 blob을 임시저장해서 해당 blob 파일을 저장하게끔 해주거나 iframe 태그를 통해 보여주는 것이긴 하다

단점

최대 단점으로… 리액트 컴포넌트나 HTML 그 자체를 똑같이 pdf로 렌더링할 수 없다

필자가 원하는 건 리액트 컴포넌트를 쌓아서 그 컴포넌트를 보이는 그대로 pdf로 만들어주는 것이었는데, 위에 서술했듯 이 라이브러리를 사용하려면 자체 지원하는 컴포넌트를 이용하여 pdf 페이지를 구성해주어야 한다

import { Html } from "react-pdf-html";

const MyDocument = () => (
  <Document>
    <Page size="A4" style={styles.page}>
      <Html>
                {`<div><span>Hello<span></div>`}
            </Html>
    </Page>
  </Document>
);

react-pdf-html 이라는 라이브러리를 이용하면 string 형식으로 된 html 문서를 파싱하여 react-pdf 컴포넌트들로 자동 변환 해주긴 하지만, 이게 또 치명적인 문제가 있다

 

바로 폰트 지정을 해 줘도 한국어가 깨진다는 것…

또한 이 방법을 이용하면 스타일 자유도가 떨어진다는 것도 한 몫 한다

strong 태그, italic 태그 등은 아예 인식하지 못한다

 

또 하나의 문제는, 문서가 조금 변경될 때마다 pdf를 다시 빌드하고 blob 링크를 재생성하기 때문에 동작이 매우 느려진다

필자가 만들고자 하는 것은 사실상 웹에서 pdf 파일을 편집하고 이를 저장할 수 있게 만드는 것인데, 저장 버튼을 눌렀을 때 문서가 만들어지는 것이 아니라 내용물이 변경될 때마다 문서가 만들어지게 되면 매우 무거운 작업이 아닐 수 없다

사실상의 한국어 미지원 + 무거운 동작 콤보로 결국 이 라이브러리의 사용은 잠시 보류하게 된다…

만약 html 파일 그대로 pdf로 출력이 아니라, 자체적인 템플릿을 구성해서 텍스트만 끼워넣으면 되는 형식으로 문서를 작성하려 한다면 react-pdf가 좋은 대안이 될 수 있겠지만, 링크 재생성 문제는 어떻게 해결할런지 모르겠다

HTML2Canvas + jsPdf

링크

https://html2canvas.hertzen.com
https://github.com/parallax/jsPDF

설명

HTML2Canvas는 HTML 요소를 이미지화 해주는 라이브러리로, 사실상 화면 캡쳐를 가능하게 해 준다

HTMLElement 요소를 인자로 넘기면 해당 요소를 캡쳐해서 이미지 파일로 변환해준다

jsPDF는 이렇게 캡쳐한 이미지 파일을 PDF로 변환하는 역할을 맡을 것이다

사실 jsPDF 하나만 써도 pdf 작성이 가능하지만… 우리가 원하는 것은 HTML 요소를 그대로 PDF화 시키는 것이었기 때문에 필요하게 됐다

요소를 이미지화해서 pdf에 붙여넣는다는 게 여간 메롱한 방법이 아니지만 한번 시도해보자

사용방법 (HTML2Canvas)

const docs = document.getElementById("docs");

우선 문서에서 이미지로 캡쳐를 원하는 요소를 가져온다

필자는 처음에 docs 요소를 리액트 루트 바깥에 선언해놓고 해당 요소만 DOM API를 이용해서 건드리는 방식으로 작업했는데 innerHTML을 건드려야 하고 등등등… 이건 아니다 싶어 하단의 방법으로 선회했다

 

export const makePDF = async (ref: RefObject<HTMLElement>) => {
    const docs = ref.current;
    ...
}

리액트에서 작업하는데 getElementById를 사용하는 것이 영 찝찝하다면 useRef을 이용해도 된다

 

const canvas = await html2canvas(docs as HTMLElement);

html2canvas 라이브러리의 인자로 요소를 넘겨주어 이미지 캔버스를 생성한다

본 로직은 모든 요소가 실제 DOM에 반영된 뒤 버튼 클릭으로 실행될 로직이라 반드시 refnull이 아니라는 가정 하에 htmlElement로 형변환 하였다 (!를 붙여줘도 된다)

canvas 요소는 HTMLCanvasElement 타입으로, 하단에도 이미지 크기를 잴 때 사용될 것이다

 

const image = canvas.toDataURL("image/png", 1.0);

그리고 이미지를 다른 곳에서도 사용할 수 있는 데이터 URL화 한다

데이터 타입은 png, 확대 / 축소 스케일은 1.0으로 넣어주었다

이렇게 하면 우리가 출력할 이미지가 준비된다

사용방법 (jsPDF)

const pdf = new jsPDF("p", "mm", "a4"); // A4 size, portrait, mm unit page of PDF

이제 문서를 작성해보도록 하자

portrait (세로) 방향, mm 단위, a4 사이즈의 문서를 하나 생성한다

 

const pageWidth = pdf.internal.pageSize.getWidth(); // page internal width
const pageHeight = pdf.internal.pageSize.getWidth(); // page internal height
const imageHeight = (canvas.height * pageWidth) / canvas.width; // image height
let heightTemp = imageHeight; // image height left
let position = 0;

이미지를 pdf에 붙여넣기 위해서는 이미지의 세로 길이를 페이지 단위로 적절히 잘라주는 과정이 필요하다

  • pageWidth, pageHeight는 위에서 생성한 pdf 문서의 가로, 세로 길이를 가져온다
    • 이 길이는 이미지를 자르는 데에 사용될 것이다
  • imageHeight는 위에서 html2canvas로 생성한 이미지의 세로 길이를 이용하여 계산해준다
    • 실제 이미지 크기는 canvas.height이고, 여기에 (페이지 너비 / 이미지 너비) 값을 곱해 준다
    • 이는 이미지 높이를 페이지 비율에 맞춰 재조정하는 과정이라고 생각하면 된다
    • 이미지 너비를 페이지 너비 (A4 사이즈의 가로 길이) 로 고정시키고, 비율에 맞춰 높이를 재계산하는 방식이다!!!
  • heightTemp는 출력하고 남은 이미지의 높이값이다
    • 하단의 반복문을 돌며 이미지를 출력하면서 점점 줄여나가는 값이다
    • 예를 들어, 20px짜리 이미지에서 4px을 출력했다면 남은 heightTemp는 16px이 되는 것
  • position은 이미지를 출력할 세로 좌표 시작점이다
    • 마찬가지로 반복문을 돌며 이미지를 출력하면서 바꿔나가는 값이다

 

pdf.addImage(image, "PNG", 0, 0, pageWidth, imageHeight);
heightTemp -= pageHeight;

while (heightTemp >= 0) {
    position = heightTemp - imageHeight;
  pdf.addPage();
  pdf.addImage(image, "PNG", 0, position, pageWidth, imageHeight);
  heightTemp -= pageHeight;
}

이미지를 페이지 크기만큼 잘라 페이지에 붙여넣는다

사전에 계산한 imageHeight (페이지 너비와 실제 그림 너비, 높이를 이용하여 리사이징한 이미지 개별 높이) 을 이용하여 이미지를 계속 잘라주고, 자른 이미지의 높이를 이용하여 이미지의 시작점 (position) 을 조정한 뒤 pageWidth * imageHeight 크기의 이미지를 해당 position에 붙여넣는 방식이라고 생각하면 된다

heightTemp를 전부 소모할 때까지 = 이미지 전체 높이만큼 출력할 때까지 작업을 반복한다

 

const file = new File([pdf.output("blob")], "test.pdf", {
    type: "application/pdf",
});
// file creation

return file;

blob 파일을 이용하여 새로운 파일을 생성하고, 이름을 붙이고 반환한다

반환받은 파일을 함수 반환값을 이용하는 곳에서 자유롭게 (다른 페이지에서 출력하기, 저장하기 등…) 사용하면 된다

 

window.open(pdf.output("bloburl"));

blob 파일 대신 blob url을 이용하면 브라우저의 새 탭에서 파일을 열 수 있다

 

단점

이미지를 생성해서 붙여넣는 것이기 때문에 텍스트나 링크 등 pdf에 추가할 수 있는 다양한 자료 형태들이 전부 플랫한 이미지 형태로 들어가 버린다

만약 텍스트를 복사 붙여넣기 해야하는 상황이 온다면…? 링크를 클릭해야 한다면…?

텍스트를 pdf에 직접 적은 것이 아니라 이미지를 붙여넣은 것이기 때문에 pdf를 확대하면 글자의 픽셀이 깨진다는 단점도 있다…

또한 이미지를 생성한 뒤 일일이 잘라서 붙여넣는 방식이기 때문에 HTML 요소가 싹둑 다음 페이지로 잘려버리는 현상이 있을 수 있다

 

https://html2canvas.hertzen.com/features

html 요소를 이미지화하는 과정에서 위와 같은 css 속성들이 적용이 되지 않는다는 문제도 있다

대부분의 속성은 적용되기 때문에 크게 불편함을 못 느낄 수도 있으나..

결론

두 라이브러리 모두 장단점이 매우 극명해서 오히려 혼란만 가중되었다

차라리 html 문서를 그대로 pdf화 하는 것은 포기하고 템플릿만 비슷하게 가져가서 새로 빌드하는 것이 나을 지도 모르겠다는 생각이 들었다 흠…

React-Pdf

장점

  • 자체 지원하는 컴포넌트를 사용하여 pdf 페이지를 쉽게 빌드 가능
  • 이미지로 만들어서 넣는 것이 아니라, 텍스트를 그대로 옮겨넣는 것이기 때문에 픽셀 깨짐 이슈가 없음
  • 사용이 쉽고 직관적이다

단점

  • A4 사이즈로 만들 경우, 페이지가 어디서 잘릴지 가늠하기 힘듦
  • 자체 컴포넌트로 문서를 만들 때, 한 곳에서 변화가 발생하면 pdf를 다시 빌드하기 때문에 성능 이슈가 발생
  • HTML 문서 그대로 pdf화 하기엔 무리가 있음
    • react-pdf-html을 사용하면 되긴 하지만, 이 경우 한국어 지원이 되지 않고 문서 일부분이 깨질 수 있음

html2canvas + jsPDF

장점

  • html 문서를 이미지화해서 그대로 pdf에 넣는 방식이기 때문에 html을 pdf로 변환하기에 최적
  • html 문서를 그대로 캡쳐하기 때문에 한국어가 깨지지 않는다

단점

  • 이미지화한 html 문서를 높이 단위로 난도질 (?) 하기 때문에, 이미지가 어디서 잘릴 지 몰라 문서가 원치 않는 곳에서 깨질 위험이 있음
  • 이미지를 붙여넣는 방식이기 때문에 pdf 문서를 확대할 경우 픽셀이 깨진다

참고 자료

https://devmemory.tistory.com/98

https://tjddnjs625.tistory.com/22

https://curryyou.tistory.com/487

https://velog.io/@juno7803/html-to-pdf

https://ironpdf.com/blog/pdf-tools/javascript-pdf-generator-tutorial/

https://html2canvas.hertzen.com/features

https://react-pdf.org/components

Comments