기획서: 웹사이트 폰트 – 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&..."> |
| 언어별 CSS | public/locales/{en,ko,ja,ar}.css – :root:lang(xx), --locale-font, line-height 등 |
| 전역 CSS | app/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 언어별 원본 파일 (다운로드 대상)
| 언어 | 폰트 패밀리 | 제안 파일명(원본) | 다운로드 소스(예시) |
|---|
| EN | Inter | Inter-Regular.ttf (및 Weight별) | GitHub rsms/inter 릴리즈 |
| KO | Noto Sans KR | NotoSansKR-Regular.otf / .ttf | Google Fonts / noto-cjk 저장소 |
| JA | Noto Sans JP | NotoSansJP-Regular.otf / .ttf | Google Fonts / noto-cjk 저장소 |
| AR | Noto Sans Arabic (또는 IBM Plex Sans Arabic) | NotoSansArabic-Regular.ttf / IBMPlexSansArabic-Regular.ttf | Google Fonts noto-fonts / ibm-plex-arabic 저장소 |
| VI | Be Vietnam Pro | BeVietnamPro-Regular.ttf (및 Weight별) | Google Fonts / be-vietnam-pro 저장소 |
| ZH | Noto Sans SC | NotoSansSC-Regular.otf / .ttf | Google 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-Control | public, max-age=31536000, immutable | 1년 동안 브라우저가 캐시 유지, 재검증 생략. |
| Content-Type | font/woff2 | 올바른 MIME으로 서빙. |
| 적용 주체 | R2 오브젝트 업로드 시 메타데이터로 설정. (구현 시 업로드 스크립트/API에서 해당 옵션 사용.) | |
4.4 Cache Busting (폰트 업데이트 시)
- 전략: 파일 내용이 바뀌면 파일명 또는 경로에 버전/해시 포함.
- 예:
Inter-Regular.woff2 → Inter-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 캐시로 남아도 동작에 문제가 없도록 함. |
| 전략 1 | Pulumi 업로드 단계에서 파일의 해시값을 계산하여 파일명에 포함. 예: Pregoi-Inter-[HASH].woff2, Pregoi-NotoSansKR-[HASH].woff2 등. URL이 바뀌므로 브라우저는 새 리소스로 요청하고, CSS/빌드 산출물에서 해당 해시가 반영된 URL을 참조하도록 함. |
| 전략 2 | Cloudflare 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}.css 및 vi.css, zh.css (VI/ZH 추가 시) | 유지·확장. --locale-font 등은 그대로 두고, 실제 폰트 파일 경로만 @font-face에서 통제. |
아래는 구현 시 반영할 세부 요구사항만 정리. 코드 생성은 하지 않음.
| 항목 | 내용 |
|---|
| 입력 | ./dictionaries/*.json (또는 apps/client-web/dictionaries/*.json) 내 모든 텍스트를 읽어 사용 문자 집합 추출. |
| 도구 | fonttools의 pyftsubset으로 서브셋 생성. |
| 대상 폰트 | 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-face | R2 공개 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-size | line-height |
|---|
| EN | Inter | 1rem (기본) | 1.5 |
| KO | Noto Sans KR | 1px 더 큼 (예: calc(1rem + 1px)) | 1.6 |
| JP | Noto Sans JP | 기본 또는 1px 더 큼 | 1.7 |
| AR | Noto Sans Arabic | 기본 또는 기존 1.125rem 유지 | 1.8 |
| VI | Be Vietnam Pro | 1rem (기본) | 1.6 |
| ZH | Noto 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-face를 app/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 버킷 및 CORS | Cloudflare R2 버킷 prego-static-assets 생성. CORS로 app.pregoi.com, pregoi.com Origin 허용. (업로드 로직은 7단계.) | IaC 또는 콘솔 적용 후 버킷·CORS 존재 확인. |
| 7. R2 업로드 및 Cache-Control | static/fonts/optimized/ 내 WOFF2를 R2에 업로드. 모든 폰트 오브젝트에 Cache-Control: public, max-age=31536000, immutable, Content-Type: font/woff2 적용. | R2 버킷 내 fonts/... 오브젝트 및 응답 헤더 확인. |
| 8. CSS 연동(R2) | @font-face의 url()을 R2 공개 URL로 변경. 환경별로 로컬/R2 전환 가능하게 할지 결정. | 운영 빌드에서 R2 폰트 로딩·캐싱 확인. |
| 9. 검증 및 문서화 | 첫 접속·재접속 시 네트워크 탭으로 폰트 요청 횟수, Cache-Control 응답 헤더 확인. 원본 다운로드 경로·R2 경로·캐시 전략을 기획서 또는 운영 가이드에 반영. | 기획서/README 업데이트. |
7. 사전 준비 사항 (구현 전 체크)
| 항목 | 내용 |
|---|
| Cloudflare API | R2·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_TOKEN | Cloudflare API 호출 인증. R2 버킷 생성·오브젝트 업로드·CORS 설정에 필요. | 셸 export, .env (Git에 커밋하지 말 것) |
CLOUDFLARE_ACCOUNT_ID | Cloudflare 계정(Account) 식별. R2 리소스가 속한 계정 지정. | 셸 export, .env |
- Cloudflare 대시보드: My Profile → API Tokens에서 토큰 생성. R2 읽기·쓰기 권한 포함 권장.
- Account ID는 대시보드 우측 하단 또는 R2 메뉴 진입 시 URL에 포함됨.
선택 (Pulumi 백엔드·클라이언트 앱)
| 변수명 | 용도 | 비고 |
|---|
PULUMI_ACCESS_TOKEN | Pulumi Cloud에 스택 상태 저장 시 인증. | Pulumi Cloud 사용 시에만 필요. |
PULUMI_BACKEND_URL | Pulumi 상태 저장소 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-face에 font-display: swap 필수 적용. FOIT 방지 및 Apple HIG ‘즉각적인 반응성’ 원칙 준수.
- [C] 모니터링: Cloudflare Logpush로 폰트 요청 응답 시간 측정. 특정 지역(예: 중동)에서 지연 시 해당 PoP의 캐시 적중률(Cache Hit Ratio) 점검 및 캐시 규칙·TTL 검토.
이 문서는 기획서이며, 실제 코드·스크립트·Pulumi 리소스 작성은 구현 단계에서 진행한다.