복잡한뇌구조마냥

PDF 생성 방식에 따른 비교 분석 본문

FE/기타

PDF 생성 방식에 따른 비교 분석

지금해냥 2025. 4. 15. 18:13

Web에서 PDF 생성 방법

Native CSS print rule 활용

  1. 방법: media query의 print 를 활용해서 pdf 생성
  2. 장점: 외부 라이브러리 필요없이 간단하게 pdf를 생성
  3. 단점: 인쇄 버튼을 눌러 저장한 후 pdf를 생성하는 방식으로, 업로드에 필요한 url을 생성할 수 없음. 프린트 화면에서 그 다음 단계로 넘어가야 하는 것이 단점.

DOM Screenshot

  1. 방법: 현재 보이는 화면에 대한 스크린샷을 찍어서 이를 pdf로 변환 (html ⇒ html2canvas ⇒ canvas 에 html의 스냅샷 추가 ⇒ jspdf ⇒ pdf)
  2. 장점: 화면에 보이는 그대로 pdf를 찍어낼 수 있음. 사이즈가 고정되어 있고 어느 정도의 양식이 정해져 있는 문서를 생성하기에 유용
  3. 단점: 화면 사이즈에 의존하고 있어 동적으로 사이즈를 선택하기 어려움. 이미지를 삽입하여 pdf를 만들어내는 것이기 때문에 텍스트를 선택할 수는 없음.

라이브러리 태그 사용

  1. 방법: 라이브러리에서 제공하는 태그 규칙에 맞춰 PDF 양식 작성.(PDFMake, React-pdf 등) 템플릿에다가 데이터를 주입하여 이를 바탕으로 pdf를 추출하는데 특화됨
  2. 장점: 빠른 pdf 변환 속도, 고해상도 가능
  3. 단점
    1. 리포트 양식이 바뀔 때마다 html 뿐만 아니라 pdf 문서 양식도 개발해야 함. 동적인 형태의 문서를 변환하기 어려움
    2. html의 각 태그들을 파싱하여 라이브러리에 맞는 태그들로 매칭해주어 변환하는 함수 작성 가능 => 태그 매칭 과정에서 스타일을 주입하기 힘듦.
    3. 제공하는 인터페이스는 다양하지만(용지 사이즈, 간격, 페이지네이션 등) 동적인 형태의 문서를 다루기는 어려움

서버 API 요청

  1. 방법: 자사 또는 타사의 pdf 생성 API에 html을 전송해 pdf 파일을 제공받는 방식 (convertAPI, DocRaptor 등)
  2. 장점: 빠른 pdf 변환 속도, 고해상도 가능, 라이브러리 사용 불필요
  3. 단점
    1. 타사 서버 API 사용시 환자 정보 유출에 관한 고민 필요. DocRaptor의 경우 SOC2, HIPAA, GDPR 인증을 획득하였으나, 의료 정보를 해외 서버 저장하는 것이 금지된 병원(NCP 서버 사용 병원)에 대한 대안 필요
    2. 자사 서버에서 pdf 생성 API를 개발하는 것이 더 나은 방법으로 판단됨

PDF 생성 라이브러리 적용 결과

1. PDFMake

방법1

📚사용한 라이브러리

  • html-to-pdfmake
  • pdfmake

✨ 장점

  • 생성된 pdf파일의 크기가 작음
  • pdf생성에 적은 소요 시간
  • pdfmake 코드를 빠르고 쉽게 만들어줌
  • pdf 파일 text를 선택할 수 있음
  • 컴포넌트 구조에 상관없이 일정한 PDF를 생성 가능함

❗️문제점

  • html-to-pdfmake에 적합하지 않은 요소가 한개라도 포함되는 경우 라이브러리가 동작하지 않음
  • html-to-pdfmake에 css를 수동적으로 적어줘야하는 문제가 있음.

📝 예시 코드

async htmlToPdfMake() {
  if (this.element.nativeElelement) {
    // html의 string화
    // <div>test</div> => '<div>test</div>'
    const htmlString: string = this.element.nativeElelement.outerHTML;
    // html => pdfmake 내용으로 변환
    const convertedPDFMake = htmlToPdfmake(htmlString);
    
    // pdfmake 요소 생성
    const contents = {
      content: convertedPDFMake,
      style: {
        'page': {
          width: '210mm',
          hight: '297mm',
          'background': 'white',
        }
      }
    };
    
    // pdfmake로 pdf 다운로드
    pdfmake.createPdf(contents).download();
  }
}

방법2

📚사용한 라이브러리

  • pdfmake

✨ 장점

  • 생성된 pdf파일의 크기가 작음
  • pdf생성에 적은 소요 시간
  • pdf 파일 text를 선택할 수 있음
  • 컴포넌트 구조에 상관없이 일정한 PDF를 생성 가능함

❗️문제점

  • pdfMake를 사용하면 Print, PDF 생성 시 실제 View와 값이 다른 경우가 있음
    (수정 누락, 다른 pipe 사용, 삼항 연산자 또는 Null 병합 연산자 등 코드 차)
  • pdfmake의 디자인 한계
    (폰트, border-radius 등 테이블 관련 디자인, 프린트 후 선이 나옴 등)
  • report가 변경될 때마다 html요소와 pdfmake를 각각 개발하여 작업시간이 오래걸림
    * pdfmake 작업이 적응되기 위힌 러닝 커브가 크다.

📝 예시 코드

async htmlToPdfMake() {
  const contents = {
    pageSize: 'A4',
    content: [
      { 
        table: {
          {
            text: 'test',
            style: 'headerFont',
            bold: false,
            margin: [0, 0, 0, 0],
            border: [false, false, false, false],
          }
        }
      }
    ],
    footer: [],
    style: {
      headerFont: {
        fontSize: 10,
        bold: true,
      }
    }
  }
    
  // pdfmake로 pdf 다운로드
  pdfmake.createPdf(contents).download();
}

2. native css print rules - window.print()

✨ 장점

  • pdf생성에 적은 소요 시간 - 압도적으로 적은 시간 소요
  • 외부 라이브러리를 사용할 필요가 없음

❗️문제점

  • 인쇄 버튼을 통해 PDF를 저장하는 방식임
  • 업로드에 필요한 PDF 관련 데이터를 생성할 수 없음.
    (서버에 저장을 위해서는 사용자가 직접적으로 서버에 저장하도록 해야함 - 치명적인 단점)

📝 예시 코드

Ts 파일

elementPrint() {
    window.focus();
    window.print();
}

css 파일 - 프린트에서 불필요한 요소를 직접적으로 제어

@media print {
  @page {
    margin: 0;
    page-break-after: always;
  }

  .print-invisible {
    display: none;
  }
}

3. jsPDF

📚사용한 라이브러리

  • jsPDF

✨ 장점

  • HTML 요소를 바로 PDF로 만들 수 있음

❗️문제점

  • 한글 깨짐 - 치명적 단점

📝 예시 코드

jsPDF() {
  const doc = new jspdf.jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait', compress: true });
  const containerArray: any = document.getElementsByClassName('page');
  
  doc.html(containerArray[0], {
    callback: (doc: any) => {
      doc.save('test.pdf');
    }
  });
  
  // 사용방법 2
  // doc.html(containerArray[0]);
  // doc.addFileToVFS();
}

4. HTML2PDF

📚사용한 라이브러리

  • html2pdf

✨ 장점

  • 크기가 큰 html 요소를 자체적으로 여러 페이지로 나누어 PDF를 생성해줌
    (최대 크기 제한은 있음)
  • 화질이 좋음
  • PDF 생성을 위한 별도의 파일이 필요하지 않음
  • 컴포넌트를 A4 사이즈 규격으로 만들어야 생성이 용이함
  • class 기준으로 각 페이지를 나눌 수 있음
  • 컴포넌트가 이미지로 생성되어 원본과 동일한 PDF를 생성할 수 있음

❗️문제점

  • 대량 페이지 처리에 있어 소요 시간이 많이 걸림
  • PDF 용량이 큼
  • 만들어낸 pdf의 text를 선택할 수 없음
  • PDF생성 전 이미지에 중간 처리 과정을 할 수 없음
  • svg등 이미지가 많을 수록 느려짐

📝 예시 코드

async htmlToPDF() {
  return new Promise<void>(async resolve => {
    // container는 A4 사이즈의 component가 10개 들어있는 div 요소임
    const containerArray: any = document.getElementsByClassName('container');
    if (containerArray?.length) {
      const startTime = new Date();
      const imgWidth = 210; // mm (A4 width)cd cd
      const pageHeight = 297;
      const scale = 3;
      const dpi = 300;
      const opt = {
        margin: [0, 0],
        filename: `myfile_dpi_${dpi}_scale_${scale}.pdf`,
        image: { type: 'jpeg', quality: 0.98 },
        pagebreak: { after: '.sheet' },
        html2canvas: {
          dpi,
          scale,
          scrollX: 0,
          scrollY: 0,
          useCORS: true,
          ignoreElements: (e: any, i: number) => {
            return e.classList.contains('page-margin');
          },
        },
        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait', compress: true }
      };

      if (containerArray.length > 1) {
        let worker = html2pdf().set(opt).from(containerArray[0]).toPdf();
        for (let i = 1; i < containerArray.length; i++) {
          worker = worker.from(containerArray[i]).toContainer().toCanvas().toPdf().get('pdf').then((pdf: any) => {
            if (i === containerArray.length - 1) {
              // Bump cursor ahead to new page until on last page
              const pageCount = pdf.getNumberOfPages();
              pdf.deletePage(pageCount);
              worker = worker.save();
              const endTime = new Date();
              console.log('html2pdf-duration: ' + (endTime.getTime() - startTime.getTime()));
              resolve();
            }
          });
        }
      } else {
        let worker = html2pdf().set(opt).from(containerArray[0]).toPdf().get('pdf').then((pdf: any) => {
          const pageCount = pdf.getNumberOfPages();
          pdf.deletePage(pageCount);

          worker = worker.save();
          const endTime = new Date();
          console.log('html2pdf-duration: ' + (endTime.getTime() - startTime.getTime()));
          resolve();
        });
      }
    }
  });
}

5. HTML2Canvas

📚사용한 라이브러리

  • html2canvas
  • jsPdf

✨ 장점

  • html2canvas로 생성된 이미지를 jpeg로 변경할 수 있음
  • html2canvas로 생성된 이미지를 pdf에 넣기 전 중간 처리 과정을 넣을 수 있음
  • 컴포넌트가 이미지로 생성되어 원본과 동일한 PDF를 생성할 수 있음

❗️문제점

  • 대량 페이지 처리에 있어 소요 시간이 많이 걸림
  • PDF 용량이 큼
  • 만들어낸 pdf의 text를 선택할 수 없음
  • svg등 이미지가 많을 수록 느려짐

📝 예시 코드

async htmlToCanvas() {
  return new Promise<void>(async resolve => {
    // container는 A4 사이즈의 component가 10개 들어있는 div 요소임
    const containerArray: any = document.getElementsByClassName('container');
    if (containerArray?.length) {
      const startTime = new Date();
      const scale = 3;
      const dpi = 300;
      const doc = new jspdf.jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait', compress: true });
      const filename: string = `myfile_dpi_${dpi}_scale_${scale}.pdf`;

      const opt = {
        scale,
        scrollX: 0,
        scrollY: 0,
        useCORS: true,
        logging: true,
        allowTaint: true,
        type: 'view',
        ignoreElements: (e: any) => {
          return e.classList.contains('page-margin');
        },
      };

      if (containerArray.length > 1) {
        for (let i = 0; i < containerArray.length; i++) {
          if (i === containerArray.length - 1) {
            await this.savePDFtoCanvas(doc, containerArray[i], opt, filename, startTime, i).then(() => resolve());
          } else {
            await this.savePDFtoCanvas(doc, containerArray[i], opt, undefined, startTime, i);
          }
        }
      } else {
        await this.savePDFtoCanvas(doc, containerArray[0], opt, filename, startTime, 0).then(() => resolve());
      }
    }
  });
}

async savePDFtoCanvas(worker: any, element: any, opt: any, filename: string, startTime: Date, containerIndex: number) {
  return new Promise<void>(async resolve => {
    await html2canvas(element, opt).then((canvas) => {
      const canvasToJpeg = canvas.toDataURL('image/jpeg', 1.0);
      const imgHeight = (canvas.height * this.imgWidth) / canvas.width;

      const pageCount = Math.round(imgHeight / this.pageHeight);
      for (let i = 0; i < pageCount; i++) {
        if (!(containerIndex === 0 && i === 0)) {
          worker = worker.addPage();
        }

        worker = worker.addImage(canvasToJpeg, 'JPEG', 0, (-this.pageHeight * (i)), this.imgWidth, imgHeight, undefined, 'FAST');
      }

      if (filename) {
        worker = worker.save(filename + '.pdf');
        // window.open(worker.output('bloburl')); // 다운로드 전 미리보기 페이지
        const endTime = new Date();
        console.log('html2canvas-duration: ' + (endTime.getTime() - startTime.getTime()));
      }
      resolve();
    });
  });
}

6. HTML-to-Image

📚사용한 라이브러리

  • html-to-image
  • jsPdf

✨ 장점

  • html2canvas로 생성된 이미지를 jpeg로 변경할 수 있음
  • html2canvas로 생성된 이미지를 pdf에 넣기 전 중간 처리 과정을 넣을 수 있음
  • 컴포넌트가 이미지로 생성되어 원본과 동일한 PDF를 생성할 수 있음

❗️문제점

  • 대량 페이지 처리에 있어 소요 시간이 많이 걸림
  • PDF 용량이 큼
  • 만들어낸 pdf의 text를 선택할 수 없음
  • 한 페이지씩 넣을 때는 화질이 좋으나 10페이지 단위로 이미지 구성시 화질이 떨어짐

📝 예시 코드

async exportAsToJpeg() {
  return new Promise<void>(async resolve => {
    let worker = new jspdf.jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait', compress: true });
    const pageArray: any = document.getElementsByClassName('page');
    const containerArray: any = document.getElementsByClassName('container');
    const startTime = new Date();
    const filename: string = `myfile_html-to-image.pdf`;

    const pageSize = pageArray?.length;

    if (containerArray.length > 1) {
      for (let i = 0; i < containerArray.length; i++) {
        if (i === containerArray.length - 1) {
          await this.getJPeg(worker, containerArray[i], (pageSize >= this.containerSize && pageSize % this.containerSize === 0 ? this.containerSize : pageSize % this.containerSize), filename, startTime, i);
        } else {
          await this.getJPeg(worker, containerArray[i], this.containerSize, undefined, startTime, i);
        }
      }
    } else {
      await this.getJPeg(worker, containerArray[0], (pageSize >= this.containerSize && pageSize % this.containerSize === 0 ? this.containerSize : pageSize % this.containerSize), filename, startTime, 0);
    }
    resolve();
  });
}

async getJPeg(worker: any, element: any, pageSize: number, filename: string, startTime: Date, containerIndex: number) {
  return new Promise<void>(async resolve => {
    const scale = 3;
    const pageMargin = 5;
    const imgHeight = (this.pageHeight + pageMargin) * pageSize;
    const filterClass = (node: HTMLElement) => {
      const exclusionClasses = ['page-margin'];
      return !exclusionClasses.some((classname) => node.classList?.contains(classname));
    }
    await htmlToImage.toJpeg(element, { quality: 0.95, backgroundColor: 'white', pixelRatio: 100, canvasHeight: imgHeight, canvasWidth: this.imgWidth, filter: filterClass })
      .then((canvasToJpeg) => {
        for (let i = 0; i < pageSize; i++) {
          if (!(containerIndex === 0 && i === 0)) {
            worker = worker.addPage();
          }

          worker = worker.addImage(canvasToJpeg, 'JPEG', 0, (-this.pageHeight * (i)), this.imgWidth, imgHeight, undefined, 'FAST');
        }

        if (filename) {
          worker = worker.save(filename + '.pdf');
          // window.open(worker.output('bloburl')); // 다운로드 전 미리보기 페이지
          const element = document.getElementById('myIframe');
          if (element instanceof HTMLIFrameElement) {
            // element.attr('src', doc.output('datauristring'));
            element.src = worker.output('datauristring')
          }
          const endTime = new Date();
          console.log('html-to-image-duration: ' + (endTime.getTime() - startTime.getTime()));
        }
        resolve();
      });
  });
}

이미지 기반 라이브러리 비교

✨ 이미지 기반 라이브러리 공통점

  • 이미지 생성하는 시간동안 CPU를 100% 사용하기 때문에 페이지가 많을수록 오래 걸림
  • 1장씩 PDF로 변환하면 걸리는 시간이 상당히 오래 걸림
    - 변환할 수 있는 이미지 최대 크기로 묶을수록 빨라짐 (scale 등 고려 필요)
  • A4크기의 이미지 생성 방식이기 때문에 PDF 용량이 큰 편임
  • 컴퓨터 상태에 따라 편차가 심함

속도 비교

  1. 현재 Livestudio 기준
  HTML2PDF HTML2Canvas HTML-to-Image
속도 19s 17s 430s
용량 10MB 17.5MB 9.5MB
  1. 가상 Report 기준
  HTML2PDF HTML2Canvas HTML-to-Image
5 Page 9s 2s 2s
10 Page 19s 5s 3s
20 Page 30s 10s 10s
30 Page 43s 17s 12s
40 Page 41s 29s 19s
50 Page 75s 41s 21s
60 Page 73s 55s 24s
  1. ECG 그래프 4개 페이지 기준
  HTML2PDF HTML2Canvas HTML-to-Image
10 Page 3.1 4 2.4
20 Page 6.2 8.3 4.7
30 Page 11.8 13.2 6.9
40 Page 14.2 18.8 9
50 Page 19.1 (73MB) 24.3 (91MB) 11.5 (14MB)
  1. ECG 그래프 8개 페이지 기준
 

  HTML2PDF HTML2Canvas HTML-to-Image
10 Page 4.4 6.3 3.8
20 Page 11 13.5 7.1
30 Page 16.2 22.5 10.5
40 Page 23.2 32.4 14.7
50 Page 31.9 (141MB) 42 (174MB) 18 (27MB)

종합 평가 ( : 1 🟡 : 0.5)

 
속도화질용량안정성 (완성도)개발 난이도
PDFMake 🟡 🟡
window.print (평가불가)
jsPDF (평가불가)
HTML2Pdf 🟡
HTML2Canvas 🟡
HTML-to-Image 🟡
LIST