HTML을 PDF로 변환하는 API: 견적서 다운로드의 오류 파일과 중복 생성 줄이기

견적서나 월간 보고서를 웹 화면으로 잘 만들어 놓아도 PDF 다운로드를 붙이는 단계에서 일이 커지는 경우가 있습니다. 출력용 레이아웃, 한글 폰트, 페이지 나눔, 실패한 응답 파일을 따로 처리해야 하기 때문입니다. 다운로드 버튼은 눌렸는데 내용이 오류 메시지인 PDF가 저장되면 사용자는 문서가 망가졌다고 느낍니다.
APICK HTML PDF 변환 API는 HTML 문자열을 받아 PDF 바이너리를 반환합니다. 여기서는 작은 견적서 다운로드 흐름을 예로 들어 요청 규격, 안전한 파일 저장, 중복 생성 관리까지 연결해 보겠습니다. 결과 파일을 만드는 호출 하나를 외부 API에 맡기더라도 문서 데이터의 정확성, 접근 권한, 화면 품질 검수는 애플리케이션에서 관리해야 합니다.
1. 변환 입력은 완성된 HTML 문자열
공식 요청 명세의 엔드포인트는 POST https://apick.app/rest/html_to_pdf입니다. 필수 입력은 html과 pagination이며 본문은 multipart/form-data입니다. html은 변환할 문자열, pagination은 문서상 정수로 0은 번호 미표시, 1은 표시입니다. 0이 기본값으로 설명되어 있지만 입력 명세에서는 필수이므로 예제에서는 항상 명시적으로 전송합니다. 인증은 Bearer 헤더를 사용합니다.
URL만 넘기면 현재 브라우저 화면이 그대로 PDF로 저장된다고 생각하기 쉽지만 이 API의 확인된 입력은 HTML 코드입니다. 견적서 데이터를 서버에서 읽고 전용 템플릿에 넣어 완성한 뒤 전송하세요. 로그인된 브라우저의 상태나 세션 쿠키가 변환 서버에 자동으로 전달된다고 가정하면 안 됩니다. 필요한 문서 내용이 실제 전송할 HTML 안에 있는지 먼저 점검하는 편이 좋습니다.
| 필드 | 형식과 처리 기준 |
|---|---|
html | 필수 문자열. 변환할 HTML 코드입니다. |
pagination | 필수 정수 값 0 또는 1. 폼에서는 문자열 "0" 또는 "1"로 전달합니다. |
응답 본문 | 성공 시 PDF 형식의 바이너리입니다. 일반 JSON 조회 응답과 다릅니다. |
success / cost / ms | 응답 헤더에 안내된 정상 응답 여부, 요금, 응답 시간입니다. |
Content-Type / Content-Disposition | 응답 데이터 타입과 파일명 정보를 담는 헤더입니다. 문서는 파일명을 output 고정으로 설명합니다. |
2. PDF 전용 템플릿은 작고 예측 가능하게
처음부터 웹 서비스 전체 화면을 재사용하기보다 문서에 필요한 제목, 발행일, 항목표, 합계, 유의사항만 포함하는 템플릿으로 시작해 보세요. 툴팁, 버튼, 고정 메뉴 같은 화면 요소는 출력물에서 불필요하거나 레이아웃을 흔들 수 있습니다. 합계 계산은 변환 API에 기대하지 말고 서버의 확정된 값으로 넣습니다. 문서 버전과 입력 데이터 버전을 같이 기록하면 재생성 시 내용이 달라진 이유를 확인하기 쉽습니다.
다음은 가상 항목만 넣은 최소 HTML입니다. 실제 서비스에서는 사용자가 입력한 품목명과 비고를 HTML 이스케이프한 뒤 템플릿에 넣으세요. 아무 HTML이나 그대로 받는 기능이 필요한 것이 아니라면 임의 스크립트나 외부 자원 주소를 허용할 이유도 줄어듭니다. 외부 이미지와 폰트, 복잡한 CSS의 지원 범위는 이 예시에서 검증하지 않았으며 문서 샘플로 확인해야 합니다.
<!doctype html>
<html lang="ko">
<head><meta charset="utf-8"><title>견적서 예시</title>
<style>body{font-family:sans-serif;padding:24px}table{width:100%;border-collapse:collapse}th,td{border:1px solid #ddd;padding:8px}</style>
</head>
<body><h1>견적서 예시</h1><p>문서 ID: DEMO-001</p>
<table><thead><tr><th>항목</th><th>수량</th></tr></thead>
<tbody><tr><td>샘플 상품</td><td>2</td></tr></tbody></table>
<p>출력 품질 확인용 가상 문서입니다.</p></body></html>3. curl: 임시 응답을 검사한 뒤 PDF로 저장하기
위 HTML을 quote.html로 준비한 상황을 가정합니다. curl은 헤더와 본문을 임시 파일에 받고 HTTP 오류 시 다음 저장 단계를 실행하지 않습니다. 이어지는 Python 검사는 JSON 응답과 예상하지 못한 바이트를 걸러낸 뒤 최종 파일을 만듭니다. 예제는 Bash, curl, Python 3가 있는 환경용이며 실제 호출로 실행 검증하지 않았습니다.
curl --fail-with-body --request POST 'https://apick.app/rest/html_to_pdf' \
--header "Authorization: Bearer ${APICK_API_KEY:?Set APICK_API_KEY}" \
--form-string "html=$(cat quote.html)" \
--form-string 'pagination=1' \
--dump-header response.headers --output response.tmp &&
python3 - <<'CHECK'
from pathlib import Path
headers = Path("response.headers").read_text().lower()
body = Path("response.tmp").read_bytes()
if "application/json" in headers or "+json" in headers or body.lstrip().startswith((b"{", b"[")):
raise SystemExit("JSON response received; do not save as PDF")
if not body.startswith(b"%PDF-"):
raise SystemExit("Unexpected response; PDF signature not found")
with open("quote.pdf", "xb") as output:
output.write(body)
print("PDF saved after preliminary checks")
CHECK임시 파일은 최종 문서가 아닙니다. 실패 분석에 필요한 범위에서만 접근을 허용하고 처리 후 정리하는 정책을 두세요. 문서에 개인 정보나 거래 정보가 들어간다면 오류 응답까지 통째로 공유 로그에 올리는 습관을 피해야 합니다. 파일 시그니처 검사는 오류 파일 저장을 줄이는 기본 방어이며 문서의 내용이나 모든 PDF 구조를 검증하는 검사는 아닙니다.
4. Node.js: 상태와 JSON 오류를 확인한 다음 파일 쓰기
아래 예시는 응답 상태를 확인하고 JSON 오류를 거른 뒤 PDF의 시작 바이트를 검사합니다. 서버가 반환한 파일명 대신 애플리케이션이 정한 이름을 사용하고, 기존 파일을 덮어쓰지 않도록 저장 옵션을 넣었습니다. 실제 운영에서는 주문별 권한 확인을 완료한 후 이 함수를 호출하고 결과 저장소의 접근 정책도 함께 적용하세요.
import { readFile, writeFile } from "node:fs/promises";
const apiKey = process.env.APICK_API_KEY;
if (!apiKey) throw new Error("APICK_API_KEY is required");
const html = await readFile("quote.html", "utf8");
const form = new FormData();
form.append("html", html);
form.append("pagination", "1");
const response = await fetch("https://apick.app/rest/html_to_pdf", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: form,
signal: AbortSignal.timeout(120_000)
});
if (!response.ok) throw new Error(`PDF HTTP ${response.status}`);
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
if (contentType.includes("json"))
throw new Error("PDF service returned JSON; inspect through a protected error path");
const bytes = new Uint8Array(await response.arrayBuffer());
const prefix = new TextDecoder().decode(bytes.subarray(0, 64)).trimStart();
if (prefix.startsWith("{") || prefix.startsWith("["))
throw new Error("Possible JSON error body");
if (new TextDecoder().decode(bytes.subarray(0, 5)) !== "%PDF-")
throw new Error("Unexpected PDF response");
await writeFile("quote.pdf", bytes, { flag: "wx" });
const rawCost = response.headers.get("cost");
const cost = rawCost !== null && rawCost.trim() !== "" && Number.isFinite(Number(rawCost))
? Number(rawCost) : null;
console.log({ bytes: bytes.length, cost });공식 SDK의 대응 메서드는 client.htmlToPdf(html, { pagination: true })이며 파일 결과에는 바이트, 크기, 콘텐츠 타입과 메타데이터가 제공됩니다. SDK 가이드는 파일 API의 JSON 오류를 예외로 전달한다고 설명합니다. 직접 REST로 처리할지 SDK로 공통 처리를 줄일지 선택하되, 최종 화면 검수와 문서 접근 제어까지 자동으로 해결된다고 보지는 마세요.
5. 다운로드 버튼 두 번 클릭과 재생성 비용 관리
사용자가 버튼을 두 번 누르거나 프런트엔드가 응답을 기다리다 재요청하면 같은 문서가 여러 번 생성될 수 있습니다. 애플리케이션의 작업 상태를 대기, 생성 중, 완료, 실패로 나누고 동일한 문서 버전이 생성 중일 때 기존 작업을 보여주는 방식을 고려하세요. 이미 완료된 파일을 다시 내려주는 기능과 APICK에 새 변환을 요청하는 기능을 구분하면 비용과 사용자 경험을 함께 관리하기 좋습니다.
이 엔드포인트의 문서에서 중복 요청을 자동으로 한 번만 과금하는 보장을 확인한 것은 아닙니다. 문서 ID나 해시를 자체 작업 키로 사용하는 제안은 우리 서비스에서 중복 생성을 줄이기 위한 설계입니다. 응답을 받지 못했다는 이유만으로 무제한 재시도하지 말고 작업 기록과 사용 내역을 확인하세요. 저장 전에 실패했다면 파일 저장 단계만 다시 할 수 있는지, 실제 변환부터 다시 해야 하는지도 구분합니다.
6. 30P는 변환 건당: 예상 생성 횟수로 예산 잡기
2026-10-11 UTC의 공식 요금표에서 HTML PDF 변환은 건당 30P입니다. 한 달에 100개 문서를 한 번씩 생성한다면 단순 예상은 3,000P, 1,000개라면 30,000P입니다. 확인한 요금 단위는 페이지당이 아닌 변환 건당입니다. 다만 이 가격 표기만으로 무제한 크기나 모든 레이아웃을 지원한다고 추정해서는 안 되며 입력 제한과 지원 범위는 실제 도입 전에 확인해야 합니다.
파일 생성 비용 외에 애플리케이션의 저장소, 다운로드 전송량, 보관 정책에 따른 비용도 별도로 있을 수 있습니다. 같은 버전의 결과를 재사용하는 계획이라면 생성 횟수와 다운로드 횟수를 따로 측정하세요. 문서가 수정되었는데 이전 결과를 그대로 보여주는 오류가 없도록 캐시 키에 템플릿과 데이터 버전을 포함하는 것이 좋습니다. 실제 APICK 사용 포인트는 응답 헤더와 계정 내역으로 확인합니다.
7. 출시 전에는 한글과 긴 표를 눈으로 확인하기
검수 샘플은 짧은 한 장 문서만으로 끝내지 않는 편이 좋습니다. 긴 품목명, 여러 줄 비고, 빈 항목, 페이지를 넘는 표, 한글과 영문 혼합, 큰 숫자, 마지막 페이지의 합계를 포함하세요. 페이지 번호를 켠 결과와 끈 결과도 비교합니다. 이 확인은 폰트와 CSS 렌더링을 추정으로 보장하는 대신 실제 업무 문서에서 필요한 품질을 확인하기 위한 절차입니다.
외부에 공개되지 않은 문서 내용을 변환 API로 전송한다는 점도 설계에 포함해야 합니다. 비밀 값, 불필요한 개인 식별 정보, 브라우저 세션 값을 HTML에 넣지 마세요. 다운로드 주소를 다른 고객이 추측해 접근할 수 없는지, 보관 기간이 끝난 문서가 정상적으로 정리되는지도 확인합니다. PDF 생성 기능의 완성도는 성공 응답을 받는 것뿐 아니라 문서가 필요한 사람에게 올바르게 전달되는 데서 결정됩니다.
자주 묻는 질문
웹페이지 URL을 html 필드에 넣으면 되나요?
확인한 입력은 HTML 코드 문자열입니다. URL 기반 캡처나 페이지 수집과 같은 동작으로 가정하지 마세요.
HTTP 200이면 바로 .pdf로 저장해도 되나요?
JSON 오류가 아닌지와 파일 형식을 먼저 확인하세요. 이후 실제 문서를 열어 내용과 레이아웃을 검수해야 합니다.
한 번 만든 PDF를 다시 다운로드할 때도 변환해야 하나요?
애플리케이션이 보관한 같은 버전의 파일을 재사용하는 설계를 할 수 있습니다. 다시 API를 호출하면 별도 변환 요청이므로 비용 정책을 확인하세요.
다음 단계: 실제 화면 하나에 연결해 보기
연동을 시작할 때는 APICK 공식 개발가이드에서 현재 요청 규격을 확인하고, 요금 안내에서 적용 단가를 다시 확인하세요. 서버 프로젝트에서 SDK를 사용한다면 공식 Node.js 한국어 가이드도 함께 볼 수 있습니다. 테스트 범위를 작게 정한 뒤 입력 검증, 오류 화면, 사용 포인트 기록까지 연결하면 운영 단계에서 바꿀 부분이 선명해집니다.
확인 기준: 2026-10-11 UTC. 이 글의 API 필드와 단가는 위 공식 자료를 확인해 작성했습니다. 코드는 설명용 예시이며 실제 유료 API 호출로 실행 검증하지 않았습니다. 예제의 입력은 가상 데이터 또는 사용자가 직접 설정할 자리표시자입니다. 서비스 정책과 문서는 변경될 수 있습니다.
댓글
댓글 쓰기