# Papermake: 콘텐츠 주소로 관리하는 Typst PDF 렌더링 레지스트리

- 게시: 2026.07.15
- 원문(HTML): https://trendbrief.news/articles/papermake-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%A3%BC%EC%86%8C%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EB%8A%94-typst-pdf-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%8A%B8%EB%A6%AC.html
- 발행: [트렌드브리프](https://trendbrief.news/)

> Papermake는 Typst 템플릿을 HTTP로 발행해 서버에서 PDF로 렌더링하는 오픈소스 레지스트리다. 템플릿·데이터·결과를 SHA-256 해시로 저장해 중복을 없애고 모든 렌더를 추적한다.

## Papermake는 무엇을 하는 도구인가

Papermake는 Typst로 작성한 문서 템플릿을 서버에 올려두고 HTTP 요청 한 번으로 PDF를 받아오는 오픈소스 레지스트리다. Typst는 문서를 코드처럼 작성하는 마크업 조판 시스템으로, LaTeX를 대체할 목적으로 나온 [오픈소스 프로젝트](https://typst.app)다. Papermake 공식 저장소는 이 도구를 "Typst 템플릿을 PDF API로 바꾼다. 한 번 발행하고 어디서나 렌더링한다"라고 소개한다.

개발자 Erik Steiger가 만들었고 [GitHub 저장소](https://github.com/rkstgr/papermake)에 Apache-2.0 라이선스로 공개돼 있다. 코드는 대부분 Rust이며 관리 UI 쪽에 TypeScript가 섞여 있다. 2026년 7월 기준 GitHub 릴리스는 0.3.0이고, 프로젝트 문서는 아직 초기 단계라 API와 기능이 바뀔 수 있다고 밝힌다. 이 글은 그 0.3.0 시점의 저장소를 기준으로 콘텐츠 주소 저장과 서버 렌더링 구조를 개발자 관점에서 정리한다. 클라이언트 SDK 사용법이나 Typst 문법 자체는 다루지 않는다.

## 콘텐츠 주소 저장이 왜 중복을 없애나

파일을 이름이 아니라 내용의 해시로 식별하기 때문이다. Papermake는 템플릿과 입력 데이터, 결과 PDF를 전부 SHA-256으로 해시해 저장한다. 같은 내용이면 같은 해시가 나오므로 한 번만 저장하고 나머지는 그 해시를 가리킨다. 인보이스 템플릿 100개가 같은 회사 로고를 쓰면 로고 파일은 저장소에 하나만 남고 100곳이 이를 참조한다. Git과 Docker 레지스트리가 쓰는 방식과 같다.

참조에는 두 층이 있다. 해시로 직접 가리키는 불변 참조는 언제 요청해도 같은 바이트를 돌려주고, 태그로 가리키는 가변 참조는 시점에 따라 다른 버전을 가리킨다.

| 참조 방식 | 예시 | 특성 |
| --- | --- | --- |
| 불변(내용 주소) | invoice@sha256:abc123 | 항상 같은 바이트, 재현 보장 |
| 가변(태그) | invoice:latest | 배포 시점마다 다른 버전을 가리킴 |

덕분에 "지난달에 나간 그 인보이스를 그대로 다시 뽑아 달라" 같은 요구를 해시 하나로 재현할 수 있다. 버전 고정과 편의를 동시에 챙기는 구조다.

## 템플릿 발행과 렌더링 요청

흐름은 발행과 렌더 두 단계다. Papermake 저장소 설명에 따르면 템플릿은 POST /templates/{name}/publish?tag={tag} 로 올리고, 렌더링은 POST /render/{name}:{tag} 에 JSON 데이터를 실어 요청하면 PDF 바이트가 돌아온다. 템플릿은 단일 파일뿐 아니라 메인 Typst 파일에 헬퍼 모듈, 로고, 폰트를 묶은 번들도 지원하고, 이들을 가상 파일시스템으로 서로 import한다.

Typst 템플릿은 데이터를 참조하는 문서 코드로 짠다.

```
= 인보이스 #data.number
*수신:* #data.customer.name
*금액:* #data.amount 원
```

발행한 뒤에는 데이터만 바꿔 요청하면 된다. 도커 컴포즈로 띄우면 서버가 localhost:3000에 뜬다.

```
curl -X POST "http://localhost:3000/render/invoice:v1.0.0" \
 -H "Content-Type: application/json" \
 -d '{"number":1024,"customer":{"name":"김데브"},"amount":50000}' \
 -o invoice.pdf
```

서버는 태그를 해시로 풀고, 가상 파일시스템을 구성한 뒤 Typst 엔진에 JSON 데이터를 주입해 문서를 컴파일한다. 결과는 PDF 바이트와 render_id다. 로컬에 Typst를 깔 필요가 없다는 점이 서버 렌더링의 핵심이다.

## 저장 구조와 렌더 추적

저장은 두 백엔드로 나뉜다. Papermake 저장소는 템플릿 파일과 입력 데이터, 출력 PDF 같은 실제 바이트를 S3 계열 블롭 스토리지에 두고, 렌더 이력 메타데이터는 ClickHouse(대용량 분석용 데이터베이스)에 기록한다고 설명한다. 서버는 Rust로 만든 단일 바이너리라 여기에 S3와 ClickHouse만 붙이면 자체 호스팅이 완성된다.

렌더 한 건마다 템플릿 해시, 입력 데이터 해시, 출력 PDF 해시, 오류 정보가 남는다. "이 PDF는 어떤 템플릿과 어떤 데이터로 나왔나"를 해시로 역추적할 수 있어 감사와 재현에 쓸 수 있다. 이력은 GET /renders?limit=N 으로 조회하고, 개별 결과물은 GET /renders/{id}/pdf 로 다시 받는다.

코드베이스는 역할별 크레이트(러스트의 패키지 단위)로 나뉘어 있다.

| 크레이트 | 역할 |
| --- | --- |
| papermake | Typst 렌더링 코어(가상 파일시스템) |
| papermake-registry | 템플릿 저장(S3)·렌더 이력(ClickHouse) |
| papermake-server | Axum 기반 HTTP API |
| papermake-worker | 큐 기반 렌더 워커(실험적) |

## 직접 실행하고 도입을 판단하려면

가장 빠른 방법은 도커 컴포즈다. 저장소 안내대로 컴포즈를 올리면 서버와 MinIO(S3 호환 스토리지), ClickHouse가 함께 뜨고 서버는 localhost:3000에서 응답한다. 서버만 단독으로 돌리려면 cargo run -r -p papermake-server 로 실행한다.

성능은 만든 사람이 직접 벤치마크를 공개했다. Erik Steiger는 자신의 블로그에서 최적화 후 PDF 한 장을 약 35밀리초에 렌더링했고, 1,000건 요청을 11초에 처리해 초당 91장을 냈다고 적었다. AWS Lambda의 256MB, ARM64 환경에서 100만 장을 프리 티어 없이 계산하면 0.35달러라고 밝혔다. 이 렌더러를 서버리스 함수 위에 올린 사례인데, 함수 단위로 과금되는 방식이 낯설다면 [서버리스 컴퓨팅의 요금 구조를 다룬 글](https://trendbrief.news/articles/%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EC%BB%B4%ED%93%A8%ED%8C%85%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%ED%95%A8%EC%88%98-%EC%9A%94%EA%B8%88%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A7%A4%EA%B2%A8%EC%A7%80%EB%82%98.html)을 먼저 보면 이해가 빠르다.

도입을 저울질한다면 이렇게 나눠 보면 된다. 인보이스, 계약서, 리포트처럼 같은 양식을 데이터만 바꿔 대량으로 찍어내고 어떤 버전으로 무엇을 뽑았는지 추적해야 하는 서비스라면 콘텐츠 주소 방식이 잘 맞는다. 반대로 PDF가 어쩌다 한 번 필요하거나 S3와 ClickHouse를 새로 운영할 여력이 없다면 부담이 더 크다. 무엇보다 릴리스가 0.3.0이고 API가 바뀔 수 있다고 저장소가 못 박은 만큼, 프로덕션에 넣기 전에는 버전을 고정하고 충분히 검증하는 편이 안전하다.

## 관련 글

- [이해가 새로운 병목이다, AI 코딩 시대 개발자의 역할](https://trendbrief.news/articles/%EC%9D%B4%ED%95%B4%EA%B0%80-%EC%83%88%EB%A1%9C%EC%9A%B4-%EB%B3%91%EB%AA%A9%EC%9D%B4%EB%8B%A4-ai-%EC%BD%94%EB%94%A9-%EC%8B%9C%EB%8C%80-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%97%AD%ED%95%A0.md)
- [Vapor로 Swift 서버 만들기, 라우팅부터 DB까지](https://trendbrief.news/articles/vapor%EB%A1%9C-swift-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0.md)
- [Leiden 알고리즘, Louvain의 끊긴 커뮤니티를 고치는 법](https://trendbrief.news/articles/leiden-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-louvain%EC%9D%98-%EB%81%8A%EA%B8%B4-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0%EB%A5%BC-%EA%B3%A0%EC%B9%98%EB%8A%94-%EB%B2%95.md)
- [Article 구조화 데이터로 저자·발행일·이미지 마크업하기](https://trendbrief.news/articles/article-%EA%B5%AC%EC%A1%B0%ED%99%94-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EC%A0%80%EC%9E%90-%EB%B0%9C%ED%96%89%EC%9D%BC-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85%ED%95%98%EA%B8%B0.md)
- [MinIO 유지보수 종료 이후, S3 호환 스토리지 다시 고르기](https://trendbrief.news/articles/minio-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98-%EC%A2%85%EB%A3%8C-%EC%9D%B4%ED%9B%84-s3-%ED%98%B8%ED%99%98-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EB%8B%A4%EC%8B%9C-%EA%B3%A0%EB%A5%B4%EA%B8%B0.md)