에디터로 pdf 만들기

Juno·2022년 10월 1일
9
post-custom-banner

라이브러리 선정 과정

react 라이브러리 중에서 pdf에 관련된 오픈소스 라이브러리 중 가장 유명한게 아래 두 가지가 있었어요.

먼저 첫 번째의 react-pdf 는 react-app 에서 pdf 미리보기에 적합한 라이브러리 였고, pdf를 로드하는 과정에서 여러 이벤트를 붙일 수 있고, 페이지네이션 등 앱 내에서 pdf를 좀 더 편하게 확인할 수 있는 용도였습니다.

This package is used to display existing PDFs. If you wish to create PDFs using React, you may be looking for @react-pdf/renderer.

두 번째로 언급한 @react-pdf/renderer 가 선택했던 라이브러리 인데요, react-pdf 의 문서 메인에도 나와 있듯이 pdf를 생성하는게 주 목적이기 때문에 해당 라이브러리를 선택하게 되었습니다.

@react-pdf/renderer 의 주요 기능

React-pdf

  1. playground가 있어서 어떠한 기능들을 제공하는지 확인하기 용이합니다.

    https://react-pdf.org/repl

  2. 시멘틱한 태그를 제공하고, 각 태그별로 인라인 스타일을 주입시켜줄 수 있으며 다운로드 링크를 생성해주는 등 자유도가 높은 편입니다.

        const Quixote = () => {
        	return (
        			<Document>
            <Page style={styles.body}>
              <Text style={styles.header} fixed>
                ~ Created with react-pdf ~
              </Text>
              <Text style={styles.title}>Don Quijote de la Mancha</Text>
              <Text style={styles.author}>Miguel de Cervantes</Text>
              <Image
                style={styles.image}
                src="/images/quijote1.jpg"
              />
              <Text style={styles.subtitle}>
                Capítulo I: Que trata de la condición y ejercicio del famoso hidalgo D.
                Quijote de la Mancha
              </Text>
        		</Page>
        </Document>
        	
        	);
        }
        
        Font.register({
          family: 'Oswald',
          src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf'
        });
        
        const styles = StyleSheet.create({
          body: {
            paddingTop: 35,
            paddingBottom: 65,
            paddingHorizontal: 35,
          },
          title: {
            fontSize: 24,
            textAlign: 'center',
            fontFamily: 'Oswald'
          },
          author: {
            fontSize: 12,
            textAlign: 'center',
            marginBottom: 40,
          },
          subtitle: {
            fontSize: 18,
            margin: 12,
            fontFamily: 'Oswald'
          },
          text: {
            margin: 12,
            fontSize: 14,
            textAlign: 'justify',
            fontFamily: 'Times-Roman'
          },
          image: {
            marginVertical: 15,
            marginHorizontal: 100,
          },
          header: {
            fontSize: 12,
            marginBottom: 20,
            textAlign: 'center',
            color: 'grey',
          },
          pageNumber: {
            position: 'absolute',
            fontSize: 12,
            bottom: 30,
            left: 0,
            right: 0,
            textAlign: 'center',
            color: 'grey',
          },
        });
        
        ReactPDF.render(<Quixote />);
  1. 데이터가 바뀔 때 마다 실시간으로 생성되는 pdf를 미리보기 형태로 확인할 수 있습니다.

이렇게 분명 react에서 pdf를 다루기에는 장점이 많은 라이브러리 였지만, 에디터로 본문 내용을 작성받아서 pdf를 생성해야 하는 요구사항을 맞추어 계약서 를 생성해주는 것엔 적합하지 않다고 판단하여 다른 방법을 찾아보게 됩니다.

  • 다운로드 링크가 원하는 시점에 생성되지 않고, 파일이 동적으로 변할 때 마다 계속 생성해주어 부하를 일으키게 됩니다. 요구사항은 에디터를 통해 값을 계속 추가해가는 구조이기 때문에 이 보다는 서버에서 내려주는 데이터를 그대로 pdf 화 시키는데 더 적절하다고 느껴졌습니다. https://github.com/diegomura/react-pdf/issues/736 (다음과 같은 방법으로 라이브러리를 추가하여 특정 시점에 pdf를 생성할 수 있긴 합니다.)
  • 에디터에 입력한 내용을 html로 추출하고 이를 통해 pdf를 생성해주어야 하는데,
	<div dangerouslySetInnerHTML={{ __html: content }} /> 

 와 같은 방법으로 react-pdf의 시멘틱한 태그 사이에 넣어주는 방식으로 넣어주어 구현하려고 했어요. 하지만 실제로 다운로드 받아보면 미리보기와 달리 정상적으로 인쇄되지 않았습니다. (공식적으로도 html 을 포함시키는걸 지원하고 만든건 아니더라구요 🥲)

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

위와 같은 방법으로 html의 각 태그들을 파싱하여 react-pdf의 태그들로 매칭해주어 변환해주는 작업을 하는 유틸함수를 만들어 사용해야 했습니다. (이를 react-pdf-html 로 npm에 배포해주신 분도 있더라구요)

 하지만 이것도 한계가 있었던 것이 매칭하는 과정에서 스타일을 주입하기도 힘들었고, 기존의 react-pdf도 가지고 있던 문제인데 한국어를 지원하는 폰트들고 일부 깨져서 나오고, (몇몇의 폰트만 제대로 나오더라구😂) react-pdf-html을 사용하면 html로 주입해준 부분만 한글이 깨져서 보이는 문제가 있었습니다.

@react-pdf/renderer 는 정해진 템플릿에다가 데이터를 주입하여 이를 바탕으로 pdf를 추출하는데 특화되어 있는 라이브러리라고 생각됩니다. 제공되는 인터페이스는 다양하지만(용지 사이즈, 간격, 페이지네이션 등) 동적인 형태의 문서를 다루기에는 어려워 보입니다.

더 쉬운 방법으로, html ⇒ pdf

 React 진영에 있는 pdf를 편하게 만들어주는 라이브러리라는 점에서 그 관점에 꽂혀 어떻게 이리저리 이슈를 해결해 가면서 구현하고자 하였으나, 한계점이 명확해서 결국 다른 방법을 찾아보게 되었어요.

How to generate PDF from HTML in React / Node.js app

위의 문서에 고민하던 방법들이 여러가지 담겨 있었는데요, 클라이언트에서 pdf를 생성하는 입장으로 생각해보면 아래의 세 가지 방법으로 정리해볼 수 있었어요.

  1. native css print rules 활용
  2. making a screenshot from the DOM
  3. using react-pdf (using pdf libraries)-(위에서 삽질한 결과.. 요건 제외했어요)

native css print rules 활용

media query의 print 를 활용해서 pdf를 생성하는 방법입니다.

 외부 라이브러리도 필요없고 간단하게 pdf를 생성할 수 있지만, 인쇄버튼을 통해서 저장하여 pdf를 생성하는 방식이기 때문에 업로드에 필요한 url을 생성할 수 없고, 프린트 화면에서 넘어가야 한다는 점 때문에 적절하지 않았어요.

making a screenshot from the DOM

 말 그대로 현재 보이는 화면에 대한 스크린샷을 찍어서 이를 pdf로 변환해주는 것 입니다.

 react-pdf 는 pdf에 특화되어 있기 때문에 이를 위한 인터페이스가 잘 제공되어 있는 반면, 거기에 제한되어 있던 반면, 이 방법을 이용하게 되면 페이지네이션과 같은 부분만 로직으로 추가를 해주면 화면에 보이는 그대로 pdf를 찍어낼 수 있었습니다.

 그리고 브라우저에 보이는 그대로 찍어내기 때문에 해당 화면 사이즈에 의존하고 있어 동적으로 선택하긴 어렵지만, 계약서의 특성 상 사이즈가 고정되어 있고 어느정도의 양식도 갖춰져 있기 때문에 문제되지 않았던 것 같아요.

 하지만, 말 그대로 이미지를 삽입하여 pdf를 만들어내는 것이기 때문에 텍스트를 선택할 수는 없습니다 🥲

html ⇒ html2canvas ⇒ canvas 에 html의 스냅샷 추가 ⇒ jspdf ⇒ pdf

이 과정에서 html2canvas, jspdf 의 두 가지 라이브러리를 사용하였습니다.

 먼저 계약서의 형태대로 퍼블리싱을 해두고, 계약서의 본문 내용만 에디터의 html 파일을 활용하는 형태고, 필드들을 모두 채운 뒤 미리보기 형태로 제공합니다.

const Document = ({ html }) => {
	const docRef = useRef<jsPDF>(null);

	return(
		<div ref={docRef}>
			{html}
		</div>
	);
}

export const html2pdf = async (element: HTMLElement) => {
  const canvas = await html2canvas(element);
  const imgData = canvas.toDataURL('image/png');
  /*
  Here are the numbers (paper width and height) that I found to work.
  It still creates a little overlap part between the pages, but good enough for me.
  if you can find an official number from jsPDF, use them.
  */
  const imgWidth = 210;
  const pageHeight = 295;
  const imgHeight = (canvas.height * imgWidth) / canvas.width;
  let heightLeft = imgHeight;

  const doc = new jsPDF('p', 'mm', 'a4', true);
  let position = 0;

  doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight, undefined, 'FAST');
  heightLeft -= pageHeight;

  while (heightLeft >= 0) {
    position = heightLeft - imgHeight;
    doc.addPage();
    doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight, undefined, 'FAST');
    heightLeft -= pageHeight;
  }
	// doc.save();
  const blob = new Blob([doc.output('blob')], { type: 'application/pdf' });

  return blob;
};

미리보기 상태에서 해당 화면을 html2canvas 로 canvas로 변환한 뒤, 이미지 형태로 추출합니다.

해당 이미지를 jspdf 를 이용하여 pdf 문서에 페이지별로 적당하게 잘라서 이미지를 추가하고 이를 저장하거나 blob 형태로 저장할수도 있습니다.

실제로 구현할 때는, 먼저 스냅샷을 가져오기 위해 미리보기를 제공하고 이를 pdf로 생성이 완료된 후에 서버쪽으로 업로드 하는 식으로 구현하였습니다.

profile
사실은 내가 보려고 기록한 것 😆
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 5월 5일

잘 봤습니다!

답글 달기