Files
sam-docs/dev/standards/pdf-font-policy.md
권혁성 cb77190cd6 docs: [standards] DomPDF 사용 가이드 전면 재작성
- 인스턴스 규칙, setOptions 금지, chroot/symlink 주의사항 추가
- font-weight 800 금지, 서브셋팅 경량화 섹션 병합
- 배포 환경(릴리스/shared) 대응 가이드 포함
2026-03-11 14:00:27 +09:00

10 KiB

DomPDF 사용 가이드

작성일: 2026-03-11 패키지: barryvdh/laravel-dompdf v3.1 (DomPDF v3.1.5) 구현 참조: mng/app/Services/HR/PayrollService.php


1. DomPDF 인스턴스 규칙

1.1 폰트 등록은 반드시 렌더링할 인스턴스에

Pdf::loadView()매번 새 DomPDF 인스턴스를 생성한다. 다른 인스턴스에 폰트를 등록해도 렌더링 인스턴스에는 적용되지 않는다.

// ❌ 인스턴스 불일치 — 폰트가 적용되지 않음
$dompdf = app(\Barryvdh\DomPDF\PDF::class)->getDomPDF();  // 인스턴스 A
$dompdf->getFontMetrics()->registerFont(...);

$pdf = Pdf::loadView('view', $data);  // 인스턴스 B (폰트 없음)
$pdf->output();

// ✅ 동일 인스턴스에 등록
$pdf = Pdf::loadView('view', $data);
$dompdf = $pdf->getDomPDF();  // loadView가 만든 바로 그 인스턴스
$dompdf->getFontMetrics()->registerFont(...);
$pdf->output();

1.2 등록 → 렌더링 순서

Pdf::loadView() → registerFont() → saveFontFamilies() → $pdf->output()

output() 호출 시 내부에서 render()가 실행되므로, 그 전에 폰트 등록이 완료되어야 한다.


2. setOptions() 사용 금지

2.1 문제

->setOptions([...]) 호출 시 DomPDF 내부에서 new Options($options)를 실행한다. 이때 전달한 옵션만 설정되고 나머지는 DomPDF 기본값으로 초기화된다.

// ❌ chroot, font_dir 등 config/dompdf.php 설정이 모두 초기화됨
$pdf = Pdf::loadView('view', $data)
    ->setOptions([
        'font_dir' => storage_path('fonts'),
        'enable_font_subsetting' => true,
    ]);
// 이 시점에서 chroot = vendor/dompdf/dompdf (DomPDF 기본값)

2.2 해결

config/dompdf.php에 모든 설정을 선언하고, 코드에서 setOptions()를 호출하지 않는다.

// config/dompdf.php — 여기에 모든 설정
'options' => [
    'font_dir' => storage_path('fonts'),
    'font_cache' => storage_path('fonts'),
    'enable_font_subsetting' => true,
    'chroot' => array_filter([
        realpath(base_path()),
        realpath(storage_path('fonts')),
    ]),
    // ...
],

// ✅ 코드에서는 setOptions 없이 사용
$pdf = Pdf::loadView('view', $data)->setPaper('a4');

3. chroot와 파일 경로

3.1 chroot 검증 원리

DomPDF의 validateLocalUri()는 폰트 파일 접근 시 다음을 검사한다:

realpath(파일 경로)가 realpath(chroot) 하위인가?

symlink는 realpath()로 해소되므로, symlink 경로가 chroot 밖을 가리키면 차단된다.

3.2 릴리스 기반 배포 환경

mng/current → releases/20260311_134148/   (배포마다 변경)
releases/XXXXX/storage/fonts → ../../shared/storage/fonts/  (symlink)
  • storage_path('fonts')/home/.../releases/XXXXX/storage/fonts (symlink 경로)
  • realpath()/home/.../shared/storage/fonts (실제 경로)
  • base_path()/home/.../releases/XXXXX/ (릴리스 경로)

shared 경로는 릴리스 경로 하위가 아니므로 chroot에 별도 등록이 필요하다.

3.3 chroot 설정

// config/dompdf.php
'chroot' => array_filter([
    realpath(base_path()),           // 릴리스 내부 파일 허용
    realpath(storage_path('fonts')), // shared 폰트 디렉토리 허용
]),

array_filter()realpath()false를 반환할 경우(경로 미존재) 제거하기 위함이다.


4. 폰트 파일 경로 선택

4.1 resource_path() vs storage_path()

항목 resource_path() storage_path()
릴리스 변경 시 경로 변경됨 symlink → shared (불변)
.ufm 캐시 매 배포마다 재생성 유지됨
installed-fonts.json 경로 불일치로 무효화 안정적
Git 포함 O (원본 보관용) X (.gitignore)

결론: 폰트 등록 시 storage_path() 사용. 원본 TTF는 resources/fonts/에 Git으로 관리하고, 최초 실행 시 storage/fonts/로 복사한다.

4.2 폰트 복사 패턴

$fontDir = storage_path('fonts');
$dst = $fontDir.'/Pretendard-Regular.ttf';

// 최초 1회만 복사 (이후 shared에 유지)
if (! file_exists($dst)) {
    $src = resource_path('fonts/Pretendard-Regular.ttf');
    if (! file_exists($src)) {
        return;
    }
    copy($src, $dst);
}

5. 외부 폰트 및 금지 사항

5.1 구글 폰트 금지

❌ @import url('https://fonts.googleapis.com/...');
❌ <link href="https://fonts.googleapis.com/..." rel="stylesheet">
❌ @font-face src: url('https://...');

DomPDF는 웹 브라우저가 아니다. 외부 폰트 다운로드는 네트워크 의존성, 방화벽 차단, 성능 저하를 유발한다.

5.2 isRemoteEnabled 금지

// ❌ 보안 위험 + 외부 의존성
->setOptions(['isRemoteEnabled' => true])

5.3 font-weight 800 이상 사용 금지

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

/* ❌ DomPDF에서 한글 깨짐 — 800 weight에 매칭되는 폰트 없음 */
h1 { font-weight: 800; }

/* ✅ bold(700)까지만 사용 */
h1 { font-weight: bold; }

5.4 시스템 전용 폰트 단독 사용 금지

DomPDF는 OS 시스템 폰트를 자동 인식하지 않는다. registerFont()로 등록된 폰트만 사용 가능하다.

/* ❌ DomPDF가 인식 못함 → 한글 ??? */
body { font-family: 'Malgun Gothic', sans-serif; }

/* ✅ DomPDF에 등록된 폰트 사용 */
body { font-family: 'Pretendard', 'Malgun Gothic', sans-serif; }

Malgun Gothic은 브라우저에서 HTML을 직접 볼 때의 fallback 용도로만 기재한다.


6. PDF 경량화 설정

6.1 폰트 서브셋팅 (필수)

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 폰트 전체(~2-5MB) → 사용 글자만(수십KB)
enable_javascript true false PDF 내 JS 코드 제거

한글 폰트는 11,172개의 완성형 글자를 포함하지만, 급여명세서에 사용되는 글자는 100~200자 수준이다. 서브셋팅으로 99% 이상의 불필요한 글리프를 제거한다.


7. 표준 PDF 생성 패턴

7.1 전체 코드

$pdf = Pdf::loadView('emails.payslip', ['payslipData' => $data])
    ->setPaper('a4');

$this->registerKoreanFont($pdf);  // 동일 인스턴스에 등록

$pdfContent = $pdf->output();

7.2 registerKoreanFont 구현

private function registerKoreanFont(\Barryvdh\DomPDF\PDF $pdf): void
{
    $fontDir = storage_path('fonts');
    $normalDst = $fontDir.'/Pretendard-Regular.ttf';
    $boldDst = $fontDir.'/Pretendard-Bold.ttf';

    // resources → storage 복사 (최초 1회, 이후 shared에 유지)
    if (! file_exists($normalDst)) {
        $src = resource_path('fonts/Pretendard-Regular.ttf');
        if (! file_exists($src)) {
            return;
        }
        if (! is_dir($fontDir)) {
            mkdir($fontDir, 0755, true);
        }
        copy($src, $normalDst);
    }
    if (! file_exists($boldDst)) {
        $src = resource_path('fonts/Pretendard-Bold.ttf');
        if (file_exists($src)) {
            copy($src, $boldDst);
        }
    }

    $dompdf = $pdf->getDomPDF();
    $fm = $dompdf->getFontMetrics();

    $fm->registerFont(
        ['family' => 'pretendard', 'style' => 'normal', 'weight' => 'normal'],
        $normalDst
    );
    if (file_exists($boldDst)) {
        $fm->registerFont(
            ['family' => 'pretendard', 'style' => 'normal', 'weight' => 'bold'],
            $boldDst
        );
    }
    $fm->saveFontFamilies();
}

8. 폰트 캐시 구조

storage/fonts/                        ← shared 디렉토리 (배포 간 유지)
├── installed-fonts.json              ← DomPDF 폰트 레지스트리
├── Pretendard-Regular.ttf            ← resources/에서 복사된 원본
├── Pretendard-Bold.ttf
├── pretendard_normal_*.ufm           ← DomPDF 메트릭 캐시 (자동 생성)
├── pretendard_normal_*.ttf           ← DomPDF 서브셋 (자동 생성)
└── pretendard_bold_*.*

storage/fonts/.gitignore에 포함되어 있다. 각 환경에서 첫 PDF 생성 시 자동으로 생성된다.


9. 체크리스트

PDF 뷰 작성 시

  • 외부 폰트 URL 미포함 (@import, <link>)
  • font-familyPretendard 포함 (DomPDF 한글 지원)
  • font-family fallback에 Malgun Gothic, sans-serif 포함 (브라우저용)
  • font-weightnormal/bold만 사용 (800 이상 금지)

PDF 생성 코드 작성 시

  • setOptions() 미사용 (config/dompdf.php에 선언)
  • Pdf::loadView()동일 인스턴스에 폰트 등록
  • 폰트 경로는 storage_path() 사용 (resource_path() 아님)
  • isRemoteEnabled 미사용

배포 환경 확인

  • config/dompdf.php chroot에 realpath(storage_path('fonts')) 포함
  • storage/fonts/ 디렉토리 쓰기 권한 확인
  • 폰트 TTF 파일이 resources/fonts/에 Git 관리됨
  • enable_font_subsettingtrue

관련 문서

  • DomPDF 설정: mng/config/dompdf.php
  • 구현 참조: mng/app/Services/HR/PayrollService.phpregisterKoreanFont()
  • 폰트 원본: mng/resources/fonts/ — Pretendard TTF (Git 관리)
  • 서버 운영: dev/deploys/ops-manual/README.md

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