이제 이력서를 쓸 시기가 왔는데 기존 프로젝트는 모두 리액트 기반에 적절한 라이브러리를 사용해가며 만들어 왔습니다. 그러다보니 이력서가 너무 단조로워 지는 느낌이 들었고, 별다른 라이브러리 없이 자바스크립트로 내 이력서를 만들어주는 프로젝트를 하게 되었습니다.
깃헙: https://github.com/Gaoridang/generate-pdf
createElement
라는 함수는 하나의 요소밖에 만들 수 없습니다. 예를 들어 필요한 트리가 아래와 같다면 div, h1, ul, li
네 개의 요소를 만든 뒤 각각 appendChild()
로 묶어줘야 합니다.
<div>
<h1><h1/>
<ul>
<li>
</li>
</ul>
</div>
위 과정을 하나의 함수로 만들어서 해결할 수 있다면 좋겠습니다. 리액트에서는 createElement
라는 클래스를 사용해서 요소를 만드는데 (지금은 jsx라는 함수로 바뀐듯 합니다만) 해당 클래스를 참고하여 함수를 만들었습니다.
제 프로젝트에 필요한 요소가 input
을 제외하면 다 공통으로 쓰이는 props밖에 없었기 때문에 나름 수월했지만 수많은 if
를 보고있자니 좀 답답하긴 합니다. 어쨌거나 이 함수를 사용하면 요소를 생성하면서 class, id, type, data, children 등을 추가할 수 있습니다.
특히 이벤트는 이벤트 타입에 따라 다양한 이벤트를 붙일 수 있도록 만들었습니다. onclick
, oninput
등 한 요소에 여러 이벤트를 붙일 수 있습니다.
export default function createElement(
tagName: keyof HTMLElementTagNameMap,
props?: Partial<ElementProps>
) {
const element = document.createElement(tagName);
if (props) {
if (props.id) element.id = props.id;
if (props.className) element.className = props.className;
if (props.text) element.textContent = props.text;
if (props.attributes) {
Object.entries(props.attributes).forEach(([key, value]) => element.setAttribute(key, value));
}
if (tagName === "input") {
const inputElement = element as HTMLInputElement;
if (props.value) inputElement.value = props.value;
if (props.type) inputElement.type = props.type;
// oninput은 value가 바뀔 즉시 작동
if (props.onChange) inputElement.oninput = props.onChange;
if (props.required) inputElement.required = props.required;
}
if (props.children) {
const children = Array.isArray(props.children) ? props.children : [props.children];
children.forEach((child) => element.appendChild(child));
}
if (props.events) {
Object.entries(props.events).forEach(([event, handler]) => {
element.addEventListener(event, handler);
});
}
}
return element;
}
여러 input, textarea
에 쓴 값들은 모두 value로 취급될 뿐 그 자체로 요소가 아닙니다. 그렇기 때문에 값을 받아서 <p>{value}</p>
와 같은 형식으로 바꾸어줄 필요가 있습니다.
이때 제목, 이메일, 경력 등 각기 다른 값에 대해 각기 다른 output을 만드는 부분이 쉽지 않았습니다.
const sections = {};
const formElements = form.querySelectorAll("input, textarea");
formElements.forEach((element) => {
const inputElement = element as HTMLInputElement | HTMLTextAreaElement;
let value = (element as HTMLInputElement).value.trim();
if (inputElement instanceof HTMLTextAreaElement) {
value = value.replace(/\n/g, "<br>");
}
if (!value) return;
const section = inputElement.dataset.section || inputElement.name;
sections[section] = sections[section] || [];
sections[section].push(value);
});
let formDataHtml = `<div class='form-data'>`;
Object.keys(sections).forEach((sectionName) => {
if (sectionName === "title" || sectionName === "position") {
formDataHtml += `<div class='${sectionName}'><ul>`;
} else {
formDataHtml += `
<div class='${sectionName}'>
<h2>${convertToKorean(sectionName)}</h2>
<ul>
`;
}
sections[sectionName].forEach((value, index) => {
formDataHtml += `<li>${formattedValue}</li>`;
});
formDataHtml += `</ul></div>`;
});
formDataHtml += "</div>";
\n
을 <br>
로 바꾸어 줍니다.data-section
의 값에 따라 sections 객체에 key: value
형태로 저장합니다.기존 라이브러리 없이 구현하고자 했으나, 방법을 찾지 못해 결국 Puppeteer
라는 도구를 사용하게 되었습니다. 아래 코드는 NodeJS로 만든 generatePDF api입니다.
const generatePDF = async (req: Request, res: Response) => {
try {
const validation = pdfDataSchema.safeParse(req.body);
if (validation.success) {
const { htmlContent } = validation.data;
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
await page.setContent(htmlContent);
await page.addStyleTag({ path: path.join(__dirname, '..', 'style.css') });
} catch (styleError) {
console.error('Error loading styles:', styleError);
}
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
res.contentType('application/pdf');
res.status(200).send(pdf);
} else {
res.status(400).send({ message: validation.error.message });
}
} catch (error) {
res.status(500).send({ message: 'Server error' });
}
};
safeParse
zod를 사용해 타입 검사를 진행합니다.puppeteer.launch()
통과하면 puppeteer를 실행시키고 페이지를 생성합니다.page.setContent()
해당 페이지에 request body로 받은 값을 세팅합니다.page.addStyleTag()
해당 페이지에 스타일을 주입합니다.page.pdf()
세팅이 끝났으므로 pdf로 변환해줍니다.리액트가 얼마나 편한 도구인지 깨달았습니다. JSX는 신이야.. 여튼 앞으로도 라이브러리를 쓸 때 이게 왜 편하고, 얼마나 편한 도구인지는 알아야겠다는 생각이 듭니다. 유명하면 무작정 사용하는 경우가 있었는데 이걸 경계함으로써 한 단계 발전할 수 있을 것 같습니다.