앱에서 주문 영수증을 PDF로 내보내야 할 일이 생겼다. 처음엔 서버에서 PDF를 만들어 내려받게 할까 했는데, 데이터가 다 클라이언트에 있는 상황이라 서버 왕복이 아까웠다. 결국 기기에서 바로 PDF를 만드는 쪽으로 갔고, expo-print가 딱 그 일을 한다.
핵심은 하나다. HTML 문자열을 넘기면 PDF 파일로 렌더링해준다. 레이아웃을 CSS로 짜면 되니까 별도의 PDF DSL을 배울 필요가 없다.
설치
npx expo install expo-print expo-sharing
공유까지 하려면 expo-sharing도 같이 넣는다.
HTML을 PDF로
printToFileAsync()에 HTML을 넘기면 임시 파일 경로(uri)를 돌려준다.
import * as Print from 'expo-print';
import * as Sharing from 'expo-sharing';
async function makeReceipt(order) {
const html = `
<html>
<body style="font-family: -apple-system, sans-serif; padding: 32px;">
<h1 style="font-size: 20px;">주문 영수증</h1>
<p>주문번호: ${order.id}</p>
<hr />
${order.items
.map(
(it) => `
<div style="display:flex; justify-content:space-between;">
<span>${it.name} x ${it.qty}</span>
<span>${(it.price * it.qty).toLocaleString()}원</span>
</div>`
)
.join('')}
<hr />
<h3 style="text-align:right;">합계 ${order.total.toLocaleString()}원</h3>
</body>
</html>
`;
const { uri } = await Print.printToFileAsync({ html });
await Sharing.shareAsync(uri, { UTI: '.pdf', mimeType: 'application/pdf' });
}
shareAsync까지 붙이면 카카오톡·메일·파일 앱으로 그대로 내보낼 수 있다. 사용자가 "PDF로 저장"을 고르면 기기에 저장된다.
프린터로 바로 보내기
파일을 만들지 않고 시스템 인쇄 다이얼로그를 띄우고 싶으면 printAsync()를 쓴다.
await Print.printAsync({ html });
iOS는 AirPrint, Android는 연결된 프린터 목록이 뜬다. 실제 종이 출력이 목적이면 이쪽이 맞다.
한글이 깨지거나 잘릴 때
두 가지를 자주 밟았다.
- 웹폰트를
@font-face로 넣지 말자. 렌더링 시점에 폰트를 못 받아 네모(□)로 나온다. 시스템 폰트(-apple-system,sans-serif)를 쓰면 한글이 안전하게 나온다. - 이미지는 base64로. 로고 같은 걸
httpURL로 넣으면 로딩 전에 렌더링돼서 빈칸이 된다.data:image/png;base64,...로 인라인 하면 확실하다.
정리
서버 없이 클라이언트에서 PDF를 뽑아야 한다면 expo-print가 가장 빠른 길이다. HTML/CSS를 그대로 쓰니 레이아웃 조정이 쉽고, printToFileAsync + shareAsync 조합이면 저장·공유가 한 번에 끝난다. 폰트는 시스템 폰트, 이미지는 base64 — 이 두 개만 지키면 깨질 일이 거의 없다.