Files
sam-docs/dev/standards/pdf-font-policy.md
김보곤 df19310c2e docs: [standards] PDF 폰트 정책 — 프로젝트 번들링 방식으로 전환
- 시스템 폰트(fonts-nanum) 의존 → resources/fonts/ 번들링으로 변경
- ensureKoreanFont() 코드 예시 resource_path() 방식으로 갱신
- 체크리스트, 관련 문서 경로 업데이트
2026-03-11 11:03:19 +09:00

12 KiB

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, <link>) 사용 금지
❌ 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/...');
❌ <link href="https://fonts.googleapis.com/..." rel="stylesheet">
❌ @font-face src: url('https://...');

2.2 isRemoteEnabled 옵션 금지

// ❌ 금지 — 외부 리소스 다운로드를 활성화하면 안 됨
$pdf = Pdf::loadView('view', $data)
    ->setOptions(['isRemoteEnabled' => true]);

2.3 font-weight 800 이상 사용 금지

NanumGothic은 normal(400)과 bold(700) 두 가지 weight만 존재한다. font-weight: 800 이상을 지정하면 DomPDF가 매칭되는 폰트를 찾지 못해 해당 텍스트의 한글이 ?로 깨진다.

/* ❌ 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가 찾지 못해 한글이 ???로 깨진다.

/* ❌ DomPDF에서 한글 깨짐 */
body { font-family: 'Malgun Gothic', sans-serif; }

3. 금지 이유

3.1 배포 시 권한 문제 재발

composer installvendor/를 새로 생성하면 폰트 캐시 디렉토리 권한이 초기화된다. 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 지정

/* ✅ 올바른 PDF 뷰 폰트 지정 */
body {
    font-family: 'NanumGothic', 'Malgun Gothic', sans-serif;
}

NanumGothic이 DomPDF에서 사용되고, Malgun Gothic은 브라우저에서 HTML을 직접 볼 때의 fallback이다.

4.3 자동 폰트 등록 패턴 (ensureKoreanFont)

PDF를 생성하는 서비스에 다음 패턴을 적용한다. 최초 1회만 등록하고 이후는 캐시에서 로드한다.

/**
 * 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 생성 코드 패턴

// ✅ 올바른 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_subsettingtrue로 설정한다. PDF에 실제 사용된 문자의 글리프만 포함하여 용량을 대폭 줄인다.

// 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.phpenable_font_subsettingtrue
  • enable_javascriptfalse (PDF 내 JS 미사용 시)
  • Blade 뷰에 불필요한 이미지/외부 리소스 미포함
  • font-weightnormal/bold만 사용 (미등록 weight가 fallback 폰트를 트리거하면 용량 증가)

6. 기존 코드 수정 가이드

구글 폰트를 사용하는 기존 PDF 뷰를 발견하면 다음 절차로 수정한다.

5.1 Blade 뷰 수정

<!-- ❌ 삭제 대상 -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700;800&display=swap');
body { font-family: 'Noto Sans KR', sans-serif; }
</style>

<!-- ✅ 수정 후 -->
<style>
body { font-family: 'NanumGothic', 'Malgun Gothic', sans-serif; }
</style>

5.2 서비스/컨트롤러 수정

// ❌ 수정 전
$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 적용)을 반드시 병행한다.

# 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/...') 미포함
  • <link href="https://fonts.googleapis.com/..."> 미포함
  • font-familyNanumGothic 포함 (DomPDF 한글 지원)
  • font-family fallback에 Malgun Gothic, sans-serif 포함 (브라우저용)
  • font-weightnormal/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.phpensureKoreanFont()

최종 업데이트: 2026-03-11