신입 프로젝트를 하는 중에 Custom Support Report를 pdf로 저장하는 구현이 있었다. 이 문제 해결 과정을 기록해본다.
html 화면을 pdf로 저장하는 것을 생각보다 그렇게 어렵지는 않았다. 많은 블로그를 참고하여 내용을 취합해보면 모두 비슷한 방법으로 pdf 파일 저장 로직을 구현하여서, 나도 그 방법으로 해보니 잘 되었다.
다만, 상용화할 홈페이지에서 사용할 때는 더 세부적인 코드를 구현할 필요가 있을 것 같다. 여러 페이지가 나오는 경우 상하좌우 여백을 준다든지, 머리글/꼬리글에 고정된 디자인을 넣는 등의 커스텀 디자인 시에는 세부적인 구현을 추가할 필요가 있을 것 같다.
내가 구현한 내용은 다음과 같다.
- 지정한 영역을 이미지로 저장(html2canvas) 후, PDF파일로 저장(jspdf)
- textarea 태그가 있을 경우
- 태그 안에 글자가 엔터 없이 inline 형태로 나오는 오류 해결
html 화면을 PDF 파일로 저장하기 위해서는 우선 필요한 npm 패키지를 설치해야 한다.
jspdf
npm i jspdf
html2canvas
npm i html2canvas
나는 Vue.js 라이브러리를 사용하였다.
Vue.js 든 React 든지 javascript 기반 라이브러리임으로
사용법은 거의 비슷하다고 볼 수 있겠다.
<template>
<nav>..</nav>
<div>..</div>
<button v-on:click="generatePdf(this.year, this.month)"></button>
<div id="pdfDiv">
// ... PDF로 변환할 부분
</div>
</template>
<script>
import { jsPDF } from "jspdf";
import html2canvas from "html2canvas";
//...생략
data() {
return {
year: '2022',
month: '05',
}
},
methods: {
generatePdf(year, month) {
// 프린트 하고자 하는 dom 영역을 가져옴
const pdfDiv = document.getElementById("pdfDiv");
html2canvas(pdfDiv).then(function (canvas) {
// 캔버스를 이미지로 변환
let imgData = canvas.toDataURL("image/png");
const imgWidth = 190;
const pageHeight = imgWidth * 1.414;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
const margin = 10;
const doc = new jsPDF("p", "mm");
let position = 0;
// 첫 페이지 출력
doc.addImage(imgData, "PNG", margin, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// 한 페이지 이상일 경우 루프 돌면서 출력
while (heightLeft >= 20) {
position = heightLeft - imgHeight - 28;
doc.addPage();
doc.addImage(imgData, "PNG", margin, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// 파일 저장
// save 안에 들어가는 인자는 저장시 파일명이 됨
doc.save(`CSS Report_${year}년${month}월.pdf`);
});
},
}
</script>
나의 코드에서 div#pdfDiv 안에는
textarea 태그가 있었다.
textarea 태그 안에 여러 줄의 내용이 있을 경우 jspdf와 html2canvas pdf 파일 생성 시 엔터 없이 한 줄로 내용이 나오는 문제가 있었다.
web 화면

생성된 pdf 파일 화면

참고한 html2canvas 깃허브에서 나와 같은 문제를 해결하고자 여러 사람들이 의견을 나누는데 다음과 같은 해결책이 있었다.
textarea 안에 형제 엘리먼트 div를 생성하고, 그 안에 textarea 내용을 넣는 것이다. 그리고 문제가 되는 textarea는 display를 none으로 하는 것이다.
아까 generatePdf 메소드 안에 코드를 추가한다.
methods: {
generatePdf(year, month) {
const pdfDiv = document.getElementById("pdfDiv");
// pdf 파일로 생성하고하 하는 영역에
// textarea 태그가 있는 경우
pdfDiv.querySelectorAll("textarea").forEach((textArea) => {
// textarea 형제 태그를 생성하여
// 그 안에 내용이 들어가게 한다
const div = document.createElement("div");
// remove 할 때를 대비하여 class도 추가
div.classList.add("textAreaDiv");
div.innerText = textArea.value;
div.style.padding = "20px";
div.style.backgroundColor = "#ffffff";
textArea.style.display = "none";
textArea.parentElement.append(div);
});
html2canvas(pdfDiv).then(function (canvas) {
// 여기 내용은 위 "코드 작성" 코드와 동일
});
// 위 코드에서 pdf 파일 저장이 완료된 후
// 생성된 div.textAreaDiv 태그들을 remove해준다
// 🎈 클릭 버튼을 다시 눌러을 때 반복해서 태그가 생기지 않도록 하기 위함
pdfDiv.querySelectorAll("textarea").forEach((textArea) => {
textArea.style.display = "block";
const textAreaDiv = document.querySelector(".textAreaDiv");
textAreaDiv.remove();
});
},
},
주의해야 할 점은 textarea 형제 태그로 생성한 div를 pdf 파일이 만들어지고 난 후, remove()를 해주어야 한다. 그래야 pdf 다운로드 버튼 클릭시 계속적으로 동일한 내용의 div가 재생성 되지 않기 때문이다.
(위 "해결" 코드에서 🎈 이 부분 밑에 코드 구현)
- jsPDF, html2canvas 사용하여 pdf 파일 생성
https://moonsiri.tistory.com/m/71
- textarea 태그 inline text로 보이는 문제
https://github.com/niklasvh/html2canvas/issues/2008