Skip to content

기획서: 웹사이트 폰트 – R2 및 로컬 다운로드·설정

목적: Prego 웹사이트에 적용할 폰트의 원본 다운로드 위치, Cloudflare R2 배포, 로컬/개발 환경 설정캐싱 전략을 단계별로 설계. (코드 생성 없이 범위·위치·순서만 정리)

참고: 현재는 Google Fonts 링크(Inter, Noto Sans JP/KR, IBM Plex Sans Arabic)로 폰트를 불러오며, public/locales/{en,ko,ja,ar}.css에서 --locale-font 등 언어별 스타일만 정의되어 있음.


1. 현재 상태 요약

구분내용
폰트 소스Google Fonts CDN (fonts.googleapis.com / fonts.gstatic.com)
선언 위치app/layout.tsx<link href="...Inter&Noto+Sans+JP&...">
언어별 CSSpublic/locales/{en,ko,ja,ar}.css:root:lang(xx), --locale-font, line-height 등
전역 CSSapp/globals.css – Tailwind, :root:lang(xx) body 보조
인프라Cloudflare Zone, Hetzner 서버 등 (별도 IaC/레포). R2 버킷은 해당 IaC 또는 콘솔에서 구성

2. 목표 아키텍처 (설계 방향)

  • 원본 폰트(OTF/TTF): 프로젝트 내 고정 디렉터리에 수급·보관. (저작권·용량 이슈로 자동 다운로드 불가 시 수동 또는 스크립트 1회 실행)
  • 웹용 폰트(WOFF2): Dictionary 기반 서브셋 생성 후,
    • 운영: Cloudflare R2에 업로드, 공개 URL로 서빙. 브라우저·CDN 캐싱으로 재방문 시 로딩 비용·지연 최소화.
    • 로컬/개발: R2 미사용 시 public 또는 빌드 산출물 내 로컬 경로에서 서빙 가능하도록 설계.
  • 캐싱: R2 오브젝트에 Cache-Control: public, max-age=31536000, immutable 적용. 갱신 시 파일명/경로에 버전·해시 부여(Cache Busting).

3. 원본 폰트 다운로드 위치 설계

3.1 저장 경로 (프로젝트 내)

용도제안 경로비고
원본 폰트(다운로드 보관)apps/client-web/static/fonts/raw/OTF/TTF 등 원본만 보관. Git에는 커밋하지 않고 .gitignore에 추가하거나, LFS/별도 저장소로 관리 검토.
서브셋 결과물(WOFF2)apps/client-web/static/fonts/optimized/서브셋 스크립트(pyftsubset) 산출물. 로컬 개발·빌드 및 R2 업로드의 소스 디렉터리.

선택 사항

  • static/fonts/raw 대신 tools/fonts/raw 등 리포지터리 루트 근처에 두고, 클라이언트 앱은 public/fonts만 참조할 수도 있음. 기획 단계에서는 원본 = apps/client-web/static/fonts/raw, 배포용 WOFF2 = apps/client-web/static/fonts/optimized 로 통일하는 것을 권장.

3.2 언어별 원본 파일 (다운로드 대상)

언어폰트 패밀리제안 파일명(원본)다운로드 소스(예시)
ENInterInter-Regular.ttf (및 Weight별)GitHub rsms/inter 릴리즈
KONoto Sans KRNotoSansKR-Regular.otf / .ttfGoogle Fonts / noto-cjk 저장소
JANoto Sans JPNotoSansJP-Regular.otf / .ttfGoogle Fonts / noto-cjk 저장소
ARNoto Sans Arabic (또는 IBM Plex Sans Arabic)NotoSansArabic-Regular.ttf / IBMPlexSansArabic-Regular.ttfGoogle Fonts noto-fonts / ibm-plex-arabic 저장소
VIBe Vietnam ProBeVietnamPro-Regular.ttf (및 Weight별)Google Fonts / be-vietnam-pro 저장소
ZHNoto Sans SCNotoSansSC-Regular.otf / .ttfGoogle Fonts / noto-cjk 저장소 (Simplified Chinese)
  • 버전 고정: 다운로드 스크립트 또는 문서에 릴리즈 버전/URL을 명시하여 재현 가능성(Reproducibility) 확보.

3.3 원본 다운로드 실행 주체

  • 수동: 위 저장소에서 직접 다운로드 후 static/fonts/raw/에 배치.
  • 자동(선택): 셸 스크립트 예) scripts/download_fonts.sh 또는 tools/fonts/download_fonts.sh에서 curl/wget으로 위 소스에서 받아 static/fonts/raw/에 저장. 코드 생성은 하지 않음 – 기획서에서는 “이 스크립트가 채울 경로”만 위와 같이 정의.

4. R2 및 캐싱 설계

4.1 R2 버킷

항목내용
버킷 이름prego-static-assets (Pulumi·build_fonts.py와 동일. Runbook: docs/runbook/cloudflare-step-by-step-check.md §0)
관리 위치Cloudflare R2 버킷은 IaC(별도 레포) 또는 Cloudflare 콘솔에서 구성.
오브젝트 경로fonts/{언어 또는 폰트명}/{파일명}.woff2 예) fonts/en/Inter-Regular.abc123.woff2. 업로드 소스는 ./static/fonts/optimized/ 내 WOFF2.

4.2 캐싱 메커니즘 (요약)

  • 최초 접속: 브라우저가 @font-face의 URL(R2 공개 URL)로 요청 → R2(및 Cloudflare 에지)에서 응답 → 브라우저가 로컬 캐시에 저장 후 렌더링.
  • 재접속: 동일 URL이면 브라우저 캐시에서 사용(네트워크 요청 없음). R2에 설정한 Cache-Control에 따라 재검증 주기 제어.

4.3 Cache-Control 및 메타데이터

설정목적
Cache-Controlpublic, max-age=31536000, immutable1년 동안 브라우저가 캐시 유지, 재검증 생략.
Content-Typefont/woff2올바른 MIME으로 서빙.
적용 주체R2 오브젝트 업로드 시 메타데이터로 설정. (구현 시 업로드 스크립트/API에서 해당 옵션 사용.)

4.4 Cache Busting (폰트 업데이트 시)

  • 전략: 파일 내용이 바뀌면 파일명 또는 경로에 버전/해시 포함.
    • 예: Inter-Regular.woff2Inter-Regular.v2.woff2 또는 Inter-Regular.a1b2c3d4.woff2
  • 결과: URL이 바뀌어 브라우저가 새 리소스로 인식하고 재다운로드. 기존 URL은 계속 immutable로 캐시되어도 문제 없음.

4.5 CORS (R2 공개 접근 시)

  • R2 버킷에 CORS 정책 설정: 다음 Origin만 허용.
    • https://app.pregoi.com
    • https://pregoi.com
    • (개발/스테이징) http://localhost:* 등 필요 시 추가.
  • 잘못된 CORS 시 브라우저에서 폰트 요청이 차단되므로, 실제 서비스 도메인과 반드시 일치시키도록 구현.

4.6 [A] 캐시 무효화 및 버전 관리 (Cache Busting)

폰트 파일이 업데이트될 때 브라우저가 예전 서브셋을 캐시에서 사용하는 것을 방지해야 한다.

항목내용
목표배포된 새 폰트가 즉시 반영되고, 이전 버전 URL은 immutable 캐시로 남아도 동작에 문제가 없도록 함.
전략 1Pulumi 업로드 단계에서 파일의 해시값을 계산하여 파일명에 포함. 예: Pregoi-Inter-[HASH].woff2, Pregoi-NotoSansKR-[HASH].woff2 등. URL이 바뀌므로 브라우저는 새 리소스로 요청하고, CSS/빌드 산출물에서 해당 해시가 반영된 URL을 참조하도록 함.
전략 2Cloudflare R2의 ETag를 활용. 업로드 후 오브젝트 ETag를 버전 식별자로 사용하거나, url?etag=... 형태의 쿼리 파라미터로 Cache Busting(선택). 구현 시 R2 API에서 반환하는 ETag를 빌드/배포 파이프라인에 전달하여 CSS 또는 manifest에 반영.
구현 시위 둘 중 하나 또는 병행. 해시 기반 파일명이면 디렉터리 구조는 예) fonts/en/Inter-Regular.[HASH].woff2. CSS는 빌드 시점에 생성된 해시가 들어간 URL을 사용하도록 함.

4.7 [B] 폴백 지연(Fallback Latency) 관리

서브셋 폰트가 로드되는 동안 **텍스트가 보이지 않는 현상(FOIT, Flash of Invisible Text)**을 막아야 한다.

항목내용
목표폰트 로딩 중에도 시스템 폰트로 텍스트를 먼저 표시하여 가독성·체감 속도를 확보.
설정CSS의 모든 @font-face 선언에서 font-display: swap;반드시 사용.
동작브라우저가 폰트를 다운로드하는 동안 fallback 폰트로 텍스트를 즉시 그리며, 폰트 로드 완료 후 스왑.
참고Apple HIG(Human Interface Guidelines)의 ‘즉각적인 반응성(Immediate Responsiveness)’ 원칙과 일치. 사용자가 빈 화면을 보는 시간을 최소화.

4.8 [C] 모니터링 (Instrumentation)

폰트 리소스의 응답 시간지역별 성능을 파악하기 위한 관측을 설계에 포함한다.

항목내용
수단Cloudflare Logpush를 통해 R2(및 에지)에 대한 요청·응답 로그를 수집. 폰트 요청의 **응답 시간(지연)**을 측정할 수 있도록 로그 필드 또는 메트릭 활용.
분석특정 국가/지역(예: 중동)에서 폰트를 R2에서 가져오는 속도가 느리다면, 해당 PoP(Point of Presence)캐시 적중률(Cache Hit Ratio) 을 점검. 에지 캐시 미적중 시 원본(R2)까지의 round-trip으로 지연이 발생할 수 있음.
대응Cache Hit Ratio가 낮은 PoP는 Cloudflare 대시보드 또는 설정에서 캐시 규칙·TTL·프리웜 등 검토. 필요 시 폰트 오브젝트에 대한 캐시 규칙을 명시하여 에지 캐싱을 강화.

5. 로컬 다운로드 및 로컬 서빙

5.1 로컬 개발 시나리오

  • 옵션 A: R2 미사용. 서브셋 WOFF2를 public/fonts/ 등에 두고, CSS의 @font-face/fonts/... 상대 경로를 참조.
  • 옵션 B: R2 사용. 동일한 @font-face URL을 쓰되, 개발 시에는 R2 공개 URL 또는 로컬 프록시로 R2를 가리키게 설정.

기획 단계에서는 **“원본은 static/fonts/raw에만 두고, 서브셋 결과는 static/fonts/optimized에 두며, 로컬에서는 이 경로(또는 public/fonts로 복사/심볼릭)를 우선 사용할 수 있도록 설계”**만 명시.

5.2 설정 정보가 들어갈 파일(설계만)

구분파일/위치역할
폰트 선언app/globals.css 또는 public/locales/*.css 또는 전용 fonts.css@font-face에서 url(...)을 R2 공개 URL 또는 로컬 경로(/fonts/...)로 지정.
환경별 URL환경 변수 또는 빌드 시 치환R2 베이스 URL을 빌드/환경별로 바꿀 수 있도록 설계(선택).
언어별 스타일기존 public/locales/{en,ko,ja,ar}.cssvi.css, zh.css (VI/ZH 추가 시)유지·확장. --locale-font 등은 그대로 두고, 실제 폰트 파일 경로만 @font-face에서 통제.

5.3 풀 배포 파이프라인 상세 (Fonttools 서브셋 · Pulumi · 전역 스타일)

아래는 구현 시 반영할 세부 요구사항만 정리. 코드 생성은 하지 않음.

(1) Fonttools 서브셋 스크립트 (Python)

항목내용
입력./dictionaries/*.json (또는 apps/client-web/dictionaries/*.json) 내 모든 텍스트를 읽어 사용 문자 집합 추출.
도구fonttoolspyftsubset으로 서브셋 생성.
대상 폰트Inter(EN), Noto Sans KR(KO), Noto Sans JP(JP), Noto Sans Arabic(AR), Be Vietnam Pro(VI), Noto Sans SC(ZH). 원본은 ./static/fonts/raw/에 있다고 가정.
출력최적화된 .woff2 파일을 ./static/fonts/optimized/ 에 저장.
경로스크립트 실행 컨텍스트(프로젝트 루트 vs apps/client-web)에 맞춰 ./dictionaries./static/fonts/raw·./static/fonts/optimized 경로를 코드베이스 기준으로 정확히 맞출 것.

(2) Pulumi 인프라

항목내용
리소스Cloudflare R2 버킷 prego-static-assets 를 Pulumi 프로젝트에 정의.
CORS다음 Origin 허용: app.pregoi.com, pregoi.com. (프로토콜·포트는 실제 환경에 맞게.)
업로드./static/fonts/optimized/.woff2 파일을 R2 버킷에 업로드하는 로직(Pulumi 리소스 또는 CI 스크립트).
메타데이터 (필수)업로드되는 모든 폰트 오브젝트에 다음 적용: Cache-Control: public, max-age=31536000, immutable, Content-Type: font/woff2.

(3) 전역 스타일 연동 (CSS)

항목내용
@font-faceR2 공개 URL을 가리키는 @font-face 선언. (로컬/개발 시에는 /fonts/... 또는 상대 경로 fallback 검토.)
font-display성능을 위해 font-display: swap 적용하여 FOIT(Flash of Invisible Text) 방지.
경로app/globals.css, public/locales/*.css, 또는 전용 fonts.css코드베이스 내 실제 경로를 분석하여 일관되게 적용.

언어별 타이포그래피 기준 (CSS 반영)

언어폰트font-sizeline-height
ENInter1rem (기본)1.5
KONoto Sans KR1px 더 큼 (예: calc(1rem + 1px))1.6
JPNoto Sans JP기본 또는 1px 더 큼1.7
ARNoto Sans Arabic기본 또는 기존 1.125rem 유지1.8
VIBe Vietnam Pro1rem (기본)1.6
ZHNoto Sans SC기본 또는 1px 더 큼1.6
  • 위 수치는 :root:lang(xx)public/locales/{en,ko,ja,ar,vi,zh}.css--locale-line-height, --locale-font-size 등과 통일하여 적용.
  • AR은 RTL 및 dir="rtl" 지원은 기존 app/layout.tsx 등과 동일하게 유지.

6. 단계별 진행 순서 (기획)

아래는 작업 단계만 정의. 코드·스크립트는 구현 단계에서 작성.

단계내용산출물/확인
1. 원본 폰트 위치 확정apps/client-web/static/fonts/raw/static/fonts/optimized/ 디렉터리 구조와 .gitignore 규칙 확정.디렉터리 구조 문서 또는 README 반영.
2. 원본 폰트 수급위 3.2 표에 따라 Inter, Noto Sans KR/JP/Arabic, Be Vietnam Pro, Noto Sans SC 원본을 수동 또는 스크립트로 static/fonts/raw/에 배치.raw/ 내 OTF/TTF 파일 존재.
3. Fonttools 서브셋 스크립트Python 스크립트: ./dictionaries/*.json 에서 모든 텍스트를 읽어 문자 집합 추출 → pyftsubset으로 Inter, Noto Sans KR, Noto Sans JP, Noto Sans Arabic, Be Vietnam Pro, Noto Sans SC에 대해 WOFF2 서브셋 생성 → ./static/fonts/optimized/ 에 출력. 코드베이스 기준 경로 분석하여 정확히 적용.static/fonts/optimized/.woff2 파일 생성.
4. 서브셋 실행 및 로컬 배치서브셋 스크립트 실행 → WOFF2를 static/fonts/optimized/에 출력. 파일명에 버전/해시 포함 여부 결정.로컬에서 /fonts/... 또는 해당 경로로 접근 가능한 상태.
5. CSS 연동(로컬)@font-faceapp/globals.css 또는 locale CSS에서 정의, url('/fonts/...') 또는 상대 경로로 WOFF2 참조. font-display: swap 적용. 언어별 타이포그래피(EN 1.5, KO 1px↑·1.6, JP 1.7, AR 1.8, VI 1.6, ZH 1.6) 반영. 기존 Google Fonts <link> 제거 또는 로컬 우선 분기.로컬 빌드에서 자체 호스팅 폰트·line-height 적용 확인.
6. R2 버킷 및 CORSCloudflare R2 버킷 prego-static-assets 생성. CORS로 app.pregoi.com, pregoi.com Origin 허용. (업로드 로직은 7단계.)IaC 또는 콘솔 적용 후 버킷·CORS 존재 확인.
7. R2 업로드 및 Cache-Controlstatic/fonts/optimized/ 내 WOFF2를 R2에 업로드. 모든 폰트 오브젝트Cache-Control: public, max-age=31536000, immutable, Content-Type: font/woff2 적용.R2 버킷 내 fonts/... 오브젝트 및 응답 헤더 확인.
8. CSS 연동(R2)@font-faceurl()을 R2 공개 URL로 변경. 환경별로 로컬/R2 전환 가능하게 할지 결정.운영 빌드에서 R2 폰트 로딩·캐싱 확인.
9. 검증 및 문서화첫 접속·재접속 시 네트워크 탭으로 폰트 요청 횟수, Cache-Control 응답 헤더 확인. 원본 다운로드 경로·R2 경로·캐시 전략을 기획서 또는 운영 가이드에 반영.기획서/README 업데이트.

7. 사전 준비 사항 (구현 전 체크)

항목내용
Cloudflare APIR2·Pulumi 사용 시 CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID 등 필요.
원본 폰트 라이선스Inter, Noto, IBM Plex, Be Vietnam Pro, Noto Sans SC 등 사용 폰트의 라이선스 및 재배포·서브셋 허용 범위 확인.
도구서브셋 단계에서 fonttools, Brotli(WOFF2) 등 Python 환경 준비.
R2 공개 접근R2 버킷을 공개 읽기로 쓸 경우, 버킷 정책 또는 Custom Domain(선택) 설계.

7.5 실행 전 설정해야 할 환경 변수

기획서 내용을 실행하기 전에 아래 환경 변수를 미리 설정해야 한다. (코드 생성 없이 목록만 정리.)

필수 (Pulumi로 R2 버킷 생성·업로드 시)

변수명용도설정 위치 예시
CLOUDFLARE_API_TOKENCloudflare API 호출 인증. R2 버킷 생성·오브젝트 업로드·CORS 설정에 필요.export, .env (Git에 커밋하지 말 것)
CLOUDFLARE_ACCOUNT_IDCloudflare 계정(Account) 식별. R2 리소스가 속한 계정 지정.export, .env
  • Cloudflare 대시보드: My Profile → API Tokens에서 토큰 생성. R2 읽기·쓰기 권한 포함 권장.
  • Account ID는 대시보드 우측 하단 또는 R2 메뉴 진입 시 URL에 포함됨.

선택 (Pulumi 백엔드·클라이언트 앱)

변수명용도비고
PULUMI_ACCESS_TOKENPulumi Cloud에 스택 상태 저장 시 인증.Pulumi Cloud 사용 시에만 필요.
PULUMI_BACKEND_URLPulumi 상태 저장소 URL.기본값 외 Self-hosted 백엔드 사용 시.
NEXT_PUBLIC_FONTS_BASE_URL (또는 동일 목적 변수)클라이언트(Next.js)에서 @font-face의 베이스 URL. 로컬(/fonts) vs R2 공개 URL 전환.구현 시 환경별(dev/staging/prod)로 다른 URL 지정할 때 사용.

선택 (폰트 다운로드 스크립트)

변수명용도비고
HTTP_PROXY / HTTPS_PROXY회사 방화벽 등으로 외부 다운로드 시 프록시.curl·wget이 참조. 필요 시에만 설정.
NO_PROXY프록시 제외 호스트.내부/로컬 주소 제외 시.

선택 (Cloudflare Logpush · 모니터링)

변수명용도비고
Logpush용 API 토큰Cloudflare Logpush 작업 생성·관리.대시보드 또는 API로 Logpush 설정 시. 별도 토큰 권한(Logs Edit 등) 필요. 환경 변수명은 구현 시 정함.

설정 방법 요약

  • 로컬/CI: export CLOUDFLARE_API_TOKEN=... 또는 프로젝트 루트 .env (Git에 커밋하지 말 것).
  • 문서화: 팀용 README 또는 운영 가이드에 “폰트 파이프라인 실행 전 위 변수 설정 필요”라고 명시.

8. 요약

  • 원본 폰트 다운로드 위치: apps/client-web/static/fonts/raw/ (고정). 원본 수급은 수동 또는 전용 다운로드 스크립트 1회 실행.
  • 서브셋 결과물: static/fonts/optimized/ 에 WOFF2 보관. Python 스크립트가 ./dictionaries/*.json 텍스트를 읽어 pyftsubset으로 Inter, Noto Sans KR/JP/Arabic, Be Vietnam Pro, Noto Sans SC 서브셋 생성 후 이 경로에 출력. R2 업로드 소스로 사용.
  • R2: Cloudflare R2 버킷 prego-static-assets, CORS는 app.pregoi.com, pregoi.com 허용. 업로드 시 모든 폰트 오브젝트Cache-Control: public, max-age=31536000, immutable 적용. 갱신 시 파일명/경로에 버전·해시로 Cache Busting.
  • 전역 스타일: @font-face는 R2 공개 URL 참조, font-display: swap 적용. 언어별 타이포그래피: EN line-height 1.5 (Inter), KO 1px 더 큼 + line-height 1.6 (Noto Sans KR), JP line-height 1.7 (Noto Sans JP), AR line-height 1.8 (Noto Sans Arabic), VI line-height 1.6 (Be Vietnam Pro), ZH line-height 1.6 (Noto Sans SC). app/globals.css·public/locales/*.css 등 코드베이스 경로 분석하여 일관 적용.
  • 진행 순서: 원본 위치 확정 → 원본 수급 → Fonttools 서브셋 스크립트(dictionaries → optimized) → 로컬 CSS 연동(타이포그래피·font-display) → R2 버킷·CORS → R2 업로드·Cache-Control → R2 URL로 CSS 연동 → 검증·문서화.
  • [A] 캐시 무효화: Pulumi에서 파일 해시를 계산해 파일명에 포함(예: Pregoi-v[HASH].woff2)하거나 R2 ETag 활용. 구현 시 CSS/빌드가 해시된 URL을 참조하도록 함.
  • [B] 폴백 지연: 모든 @font-facefont-display: swap 필수 적용. FOIT 방지 및 Apple HIG ‘즉각적인 반응성’ 원칙 준수.
  • [C] 모니터링: Cloudflare Logpush로 폰트 요청 응답 시간 측정. 특정 지역(예: 중동)에서 지연 시 해당 PoP의 캐시 적중률(Cache Hit Ratio) 점검 및 캐시 규칙·TTL 검토.

이 문서는 기획서이며, 실제 코드·스크립트·Pulumi 리소스 작성은 구현 단계에서 진행한다.

Help