[Django+WeasyPrint] PDF 템플릿만 CSS가 인라인인 이유
궁금했던 것
일반 화면 템플릿들은 다 base.html을 상속하고 <link href="{% static '...css' %}">로 외부 CSS 파일을 불러오는데, PDF 생성용 템플릿(quotation_pdf.html 같은) 하나만 <style> 태그 안에 CSS가 통째로 인라인되어 있었다. 왜 이 파일만 다른 방식을 쓰는지 궁금해서 물어봤다.
확인
grep -c "<style>" templates/quotation/quotation_detail.html templates/brands/brand_form.html
# 결과: 둘 다 0
grep -rn "static.*\.css\|{% static" templates/base.html
# base.html은 <link rel="stylesheet" href="{% static 'css/design-system.css' %}" /> 로 외부 CSS를 불러옴
일반 페이지 템플릿들은 예상대로 <style> 없이 base.html이 불러오는 공통 CSS를 그대로 쓰고 있었다. PDF 템플릿만 유별났다.
원인 — 렌더링 경로 자체가 다르다
PDF 템플릿은 {% extends "base.html" %}을 아예 안 쓰고 <html>부터 완전히 독립된 문서로 만들어져 있다. 이유는 누가 이 HTML을 렌더링하는지가 다르기 때문이다.
- 일반 페이지: 브라우저가 서버에 HTTP 요청을 보내서 HTML을 받고, <link href="{% static ... %}">로 걸린 CSS도 브라우저가 별도 요청으로 가져와서 적용한다
- PDF: 브라우저를 아예 거치지 않는다. Django가 render_to_string()으로 만든 HTML 문자열을 그대로 WeasyPrint(HTML(string=...).write_pdf())에 넘겨서, 서버 프로세스 안에서 바로 PDF로 그려낸다
이 방식에서 만약 <link rel="stylesheet" href="{% static '...' %}">처럼 외부 CSS 파일을 참조하면, WeasyPrint가 그 CSS를 가져오기 위해 자체적으로 별도 요청을 해야 한다 — 그 시점에 정적 파일 서빙이 정상 동작해야 하고, base_url 설정도 정확히 맞아야 한다. (같은 문서 안에서 로고/직인 이미지를 file:// 절대경로로 넣는 것도 같은 이유다 — 외부 리소스 접근이라는 추가 의존성 자체를 없애는 게 안전하기 때문.)
CSS를 통째로 인라인해두면, WeasyPrint 입장에서는 외부 요청 없이 그 자리에서 바로 렌더링할 수 있어서 PDF 생성 파이프라인이 훨씬 안정적이다.
정리
"이 파일만 유별나게 인라인"인 게 아니라, 브라우저 없이 서버에서 직접 PDF로 렌더링하는 문서라서 외부 리소스 참조 자체를 피하는 게 원칙이다. base.html을 상속하는 일반 화면 템플릿들과는 애초에 성격이 다른 파일이다.
같은 맥락에서 정리하면:
일반 화면 템플릿 PDF 템플릿
| 렌더링 주체 | 브라우저 | 서버(WeasyPrint) |
| CSS | 외부 파일 링크 ({% static %}) | <style> 인라인 |
| 이미지 경로 | 웹 URL (/media/...) | 파일시스템 절대경로 (file://...) |
| base 템플릿 상속 | 함 (base.html) | 안 함, 독립 문서 |
서버 사이드에서 곧바로 PDF/이미지 등을 렌더링하는 템플릿을 만들 일이 있으면, "브라우저가 나중에 채워줄 거라고 가정하는 리소스"(외부 CSS, 웹 경로 이미지 등)는 전부 그 자리에서 바로 해결 가능한 형태(인라인 CSS, 절대경로)로 바꿔야 한다는 걸 이번에 확실히 배웠다.