# PDF 생성 시 폰트 정책 > **작성일**: 2026-03-11 > **상태**: 설계 확정 --- ## 1. 개요 ### 1.1 목적 DomPDF로 PDF를 생성할 때 폰트 관련 문제(권한 오류, 외부 의존성, 배포 시 재발, 한글 깨짐)를 방지하기 위한 정책이다. ### 1.2 배경 — 운영서버 장애 사례 급여명세서 PDF 생성 시 500 에러 발생. 원인은 DomPDF가 구글 폰트(Noto Sans KR)를 외부에서 다운로드한 후 캐시 파일(`.ufm`)을 `vendor/dompdf/dompdf/lib/fonts/`에 저장하려는데, `www-data` 유저에 쓰기 권한이 없어서 Permission denied 발생. ``` fopen(.../vendor/dompdf/dompdf/lib/fonts/noto_sans_kr_normal_...ufm): Failed to open stream: Permission denied ``` ### 1.3 핵심 원칙 ``` ❌ PDF 뷰에서 구글 폰트(@import, ) 사용 금지 ❌ DomPDF의 isRemoteEnabled 옵션 사용 금지 ❌ font-family에 'Malgun Gothic' 등 시스템 전용 폰트 단독 사용 금지 (DomPDF가 인식 못함) ✅ NanumGothic을 DomPDF에 등록하여 사용 ✅ 폰트 원본은 resources/fonts/에 프로젝트와 함께 배포 (시스템 설치 의존 금지) ✅ 폰트 캐시는 storage/fonts/에 저장 (vendor/ 아님) ✅ 자동 등록 패턴(ensureKoreanFont) 적용 ✅ font-weight는 normal/bold만 사용 (800 이상 금지 — DomPDF 미매칭으로 한글 깨짐) ✅ 폰트 서브셋팅 활성화 (config/dompdf.php) ``` --- ## 2. 금지 사항 ### 2.1 구글 폰트 외부 로드 금지 ``` ❌ @import url('https://fonts.googleapis.com/...'); ❌ ❌ @font-face src: url('https://...'); ``` ### 2.2 isRemoteEnabled 옵션 금지 ```php // ❌ 금지 — 외부 리소스 다운로드를 활성화하면 안 됨 $pdf = Pdf::loadView('view', $data) ->setOptions(['isRemoteEnabled' => true]); ``` ### 2.3 font-weight 800 이상 사용 금지 NanumGothic은 `normal`(400)과 `bold`(700) 두 가지 weight만 존재한다. `font-weight: 800` 이상을 지정하면 DomPDF가 매칭되는 폰트를 찾지 못해 해당 텍스트의 한글이 `?`로 깨진다. ```css /* ❌ DomPDF에서 한글 깨짐 — 800 weight에 매칭되는 폰트 없음 */ h1 { font-weight: 800; } /* ✅ bold(700)까지만 사용 */ h1 { font-weight: bold; } ``` ### 2.4 DomPDF 미등록 폰트 사용 금지 DomPDF는 시스템 폰트를 자동으로 인식하지 않는다. `Malgun Gothic`, `Apple SD Gothic Neo` 등을 `font-family`에 지정해도 DomPDF가 찾지 못해 한글이 `???`로 깨진다. ```css /* ❌ DomPDF에서 한글 깨짐 */ body { font-family: 'Malgun Gothic', sans-serif; } ``` --- ## 3. 금지 이유 ### 3.1 배포 시 권한 문제 재발 `composer install`이 `vendor/`를 새로 생성하면 폰트 캐시 디렉토리 권한이 초기화된다. `vendor/` 내부 특정 경로에 권한을 거는 것은 안티패턴이다. ``` 배포 → composer install → vendor/ 재생성 → 폰트 캐시 권한 초기화 → PDF 생성 실패 ``` ### 3.2 외부 네트워크 의존성 PDF를 생성할 때마다 `fonts.googleapis.com`에 요청한다. 외부 서버 장애, 네트워크 지연, 방화벽 차단 시 PDF 생성이 실패한다. 내부 ERP에서 외부 서비스에 의존할 이유가 없다. ### 3.3 PDF에서 웹폰트 불필요 DomPDF는 웹 브라우저가 아니다. 구글 폰트를 다운로드 → 파싱 → 캐싱하는 과정이 불필요한 오버헤드다. --- ## 4. 올바른 폰트 사용법 ### 4.1 표준 한글 폰트: NanumGothic SAM 프로젝트의 PDF 한글 폰트는 **NanumGothic**을 사용한다. 폰트 파일은 프로젝트에 포함되어 Git으로 배포된다. 서버에 `fonts-nanum` 패키지를 별도 설치할 필요가 없다. | 항목 | 경로 | 설명 | |------|------|------| | 폰트 원본 (Git 관리) | `resources/fonts/NanumGothic.ttf` | 프로젝트와 함께 배포 | | 폰트 원본 (Git 관리) | `resources/fonts/NanumGothicBold.ttf` | 프로젝트와 함께 배포 | | DomPDF 캐시 | `storage/fonts/` | 런타임에 자동 생성 (.gitignore) | > 이전에는 시스템 폰트(`/usr/share/fonts/truetype/nanum/`)에 의존했으나, 운영서버에 `fonts-nanum` 미설치 시 한글이 깨지는 문제가 발생하여 프로젝트 번들링 방식으로 전환했다. ### 4.2 Blade 뷰 font-family 지정 ```css /* ✅ 올바른 PDF 뷰 폰트 지정 */ body { font-family: 'NanumGothic', 'Malgun Gothic', sans-serif; } ``` > `NanumGothic`이 DomPDF에서 사용되고, `Malgun Gothic`은 브라우저에서 HTML을 직접 볼 때의 fallback이다. ### 4.3 자동 폰트 등록 패턴 (ensureKoreanFont) PDF를 생성하는 서비스에 다음 패턴을 적용한다. 최초 1회만 등록하고 이후는 캐시에서 로드한다. ```php /** * DomPDF에 한글(NanumGothic) 폰트 등록 (최초 1회만 실행) * * 폰트 원본은 resources/fonts/에 프로젝트와 함께 배포된다. * 시스템 폰트(fonts-nanum) 설치 여부에 의존하지 않는다. */ private function ensureKoreanFont(): void { $fontDir = storage_path('fonts'); $installedFile = $fontDir.'/installed-fonts.json'; // 이미 등록되어 있으면 스킵 if (file_exists($installedFile)) { $installed = json_decode(file_get_contents($installedFile), true) ?: []; if (isset($installed['nanumgothic'])) { return; } } // 프로젝트에 포함된 폰트 (Git으로 배포됨) $fontSources = [ 'normal' => resource_path('fonts/NanumGothic.ttf'), 'bold' => resource_path('fonts/NanumGothicBold.ttf'), ]; if (! file_exists($fontSources['normal'])) { return; } if (! is_dir($fontDir)) { mkdir($fontDir, 0755, true); } // storage/fonts/에 복사 후 DomPDF에 등록 foreach ($fontSources as $weight => $src) { $dst = $fontDir.'/'.basename($src); if (! file_exists($dst)) { copy($src, $dst); } } $dompdf = app(\Barryvdh\DomPDF\PDF::class)->getDomPDF(); $fm = $dompdf->getFontMetrics(); $fm->registerFont( ['family' => 'nanumgothic', 'style' => 'normal', 'weight' => 'normal'], $fontDir.'/NanumGothic.ttf' ); $fm->registerFont( ['family' => 'nanumgothic', 'style' => 'normal', 'weight' => 'bold'], $fontDir.'/NanumGothicBold.ttf' ); $fm->saveFontFamilies(); } ``` ### 4.4 PDF 생성 코드 패턴 ```php // ✅ 올바른 PDF 생성 패턴 $this->ensureKoreanFont(); $pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData]) ->setPaper('a4'); $pdfContent = $pdf->output(); ``` ### 4.5 폰트 캐시 구조 ``` storage/fonts/ ├── installed-fonts.json ← DomPDF 폰트 레지스트리 ├── NanumGothic.ttf ← resources/fonts/에서 복사된 원본 ├── NanumGothicBold.ttf ├── nanumgothic_normal_*.ufm ← DomPDF가 생성한 메트릭 캐시 ├── nanumgothic_normal_*.ttf ← DomPDF가 생성한 서브셋 └── nanumgothic_bold_*.* ``` > `storage/fonts/`는 `.gitignore`에 포함되어 있다. 각 환경에서 첫 PDF 생성 시 자동으로 생성된다. --- ## 5. PDF 경량화 설정 ### 5.1 배경 — 텍스트 PDF가 5MB 급여명세서 PDF(텍스트만, 1페이지)가 약 5MB로 생성되는 문제가 발생. 원인은 DomPDF가 NanumGothic 폰트 파일 전체(수천 개 한글 글리프 포함)를 PDF에 임베딩하기 때문이다. ### 5.2 폰트 서브셋팅 (필수) `config/dompdf.php`에서 `enable_font_subsetting`을 `true`로 설정한다. PDF에 실제 사용된 문자의 글리프만 포함하여 용량을 대폭 줄인다. ```php // config/dompdf.php 'options' => [ 'enable_font_subsetting' => true, // ✅ 필수 — 사용 글자만 임베딩 'enable_javascript' => false, // ✅ 권장 — PDF 내 JS 불필요 // ... ], ``` | 설정 | 변경 전 | 변경 후 | 효과 | |------|---------|---------|------| | `enable_font_subsetting` | `false` | `true` | 폰트 전체(~4-5MB) → 사용 글자만(수십KB) | | `enable_javascript` | `true` | `false` | PDF 내 JS 코드 제거 | > 한글 폰트는 11,172개의 완성형 글자를 포함하지만, 급여명세서에 사용되는 글자는 100~200자 수준이다. 서브셋팅으로 99% 이상의 불필요한 글리프를 제거한다. ### 5.3 경량화 체크리스트 - [ ] `config/dompdf.php`의 `enable_font_subsetting` → `true` - [ ] `enable_javascript` → `false` (PDF 내 JS 미사용 시) - [ ] Blade 뷰에 불필요한 이미지/외부 리소스 미포함 - [ ] `font-weight`는 `normal`/`bold`만 사용 (미등록 weight가 fallback 폰트를 트리거하면 용량 증가) --- ## 6. 기존 코드 수정 가이드 구글 폰트를 사용하는 기존 PDF 뷰를 발견하면 다음 절차로 수정한다. ### 5.1 Blade 뷰 수정 ```html ``` ### 5.2 서비스/컨트롤러 수정 ```php // ❌ 수정 전 $pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData]) ->setOptions(['isRemoteEnabled' => true]) ->setPaper('a4'); // ✅ 수정 후 $this->ensureKoreanFont(); $pdf = Pdf::loadView('emails.payslip', ['payslipData' => $payslipData]) ->setPaper('a4'); ``` ### 5.3 적용 사례 | 파일 | 수정 내용 | |------|----------| | `resources/views/emails/payslip.blade.php` | `@import` 구글 폰트 삭제, `font-family` NanumGothic 적용 | | `app/Services/HR/PayrollService.php` | `isRemoteEnabled` 제거, `ensureKoreanFont()` 추가 | --- ## 7. 긴급 복구 — 운영서버 권한 오류 발생 시 이미 구글 폰트를 사용하는 코드가 배포되어 권한 오류가 발생한 경우의 즉시 조치이다. 근본 수정(구글 폰트 제거 + ensureKoreanFont 적용)을 반드시 병행한다. ```bash # Level 2 작업 — 사용자 확인 후 실행 # 운영서버에서 폰트 캐시 디렉토리 권한 수정 sudo chown -R www-data:webservice /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/ sudo chmod -R 775 /home/webservice/mng/current/vendor/dompdf/dompdf/lib/fonts/ ``` > 이 조치는 임시이다. 재배포 시 `vendor/`가 새로 생성되면 다시 발생한다. 반드시 `ensureKoreanFont` 패턴을 적용해야 근본 해결된다. --- ## 8. 체크리스트 ### PDF 뷰 작성 시 - [ ] `@import url('https://fonts.googleapis.com/...')` 미포함 - [ ] `` 미포함 - [ ] `font-family`에 `NanumGothic` 포함 (DomPDF 한글 지원) - [ ] `font-family` fallback에 `Malgun Gothic`, `sans-serif` 포함 (브라우저용) - [ ] `font-weight`는 `normal`/`bold`만 사용 (800 이상 금지) ### PDF 생성 코드 작성 시 - [ ] `isRemoteEnabled` 옵션 미사용 - [ ] `ensureKoreanFont()` 호출 후 PDF 생성 - [ ] `Pdf::loadView()->setPaper()` 패턴 사용 ### 코드 리뷰 시 - [ ] PDF 관련 Blade 뷰에 외부 폰트 URL 없음 - [ ] DomPDF 옵션에 `isRemoteEnabled` 없음 - [ ] 한글 PDF에 `ensureKoreanFont()` 호출 있음 ### 폰트 배포 확인 - [ ] `resources/fonts/NanumGothic.ttf` 존재 확인 (Git 관리) - [ ] `resources/fonts/NanumGothicBold.ttf` 존재 확인 (Git 관리) - [ ] `storage/fonts/` 디렉토리 쓰기 권한 확인 - [ ] 시스템 폰트(`fonts-nanum`)에 의존하지 않음 확인 --- ## 관련 문서 - 서버 운영 매뉴얼: `dev/deploys/ops-manual/README.md` - DomPDF 패키지: `barryvdh/laravel-dompdf` v3.1 - DomPDF 설정: `mng/config/dompdf.php` — 서브셋팅, DPI 등 - 폰트 원본: `mng/resources/fonts/` — NanumGothic TTF (Git 관리) - 구현 참조: `mng/app/Services/HR/PayrollService.php` — `ensureKoreanFont()` --- **최종 업데이트**: 2026-03-11