블로그의 콘텐츠가 800개를 넘어서면서 public 디렉토리 크기가 1.8GB에 달했다. GitHub Pages의 저장소 용량 권장 한도는 1GB이고, 빌드 artifact 크기 제한도 존재한다. 이 글에서는 Hugo 정적 사이트의 빌드 산출물 크기를 1,797MB에서 600MB 이하로 줄이고, 태그 페이지 빌드 시간을 17분에서 2분으로 단축한 전체 과정을 정리한다.
문제 인식: public 디렉토리 1.8GB 분석
빌드 후 public/ 디렉토리의 용량을 분석한 결과, 두 가지 주범이 드러났다.
| 구분 | 크기 | 비율 |
|---|---|---|
| 이미지 (PNG 원본) | 1,099 MB | 61% |
| Tag HTML 페이지 | 580 MB | 32% |
| 기타 (JS, CSS, 본문 등) | 118 MB | 7% |
| 합계 | 1,797 MB | 100% |
태그 수가 21,089개에 달했고, 각 태그 페이지가 baseof.html 기반으로 풀 렌더링되어 약 27KB씩 차지했다. 여기에 페이지네이션까지 포함하면 Tag 디렉토리만 580MB였다.
pie title public 디렉토리 구성 (1,797MB)
"이미지 PNG" : 1099
"Tag HTML" : 580
"기타" : 118
1단계: 이미지 최적화 - WebP 변환
Hugo imaging 설정
config/_default/config.toml에 이미지 처리 설정을 추가했다.
| |
resampleFilter는 처음에 Lanczos(고품질)로 설정했다가, 빌드 속도를 우선하여 Box(고속)로 변경했다. EXIF 데이터 제거로 파일 크기를 추가 절감했다.
레이아웃 파일에 WebP 적용
14개 레이아웃 파일에서 .Resize와 .Fill 호출에 webp 포맷을 추가했다.
| |
CI/CD에서 원본 PNG 삭제
Hugo는 WebP 변환 시 원본 PNG도 public/에 함께 출력한다. GitHub Actions에서 빌드 후 원본을 삭제하는 스텝을 추가했다.
| |
Hugo가 생성한 WebP 파일명에는 _hu_ 접두사가 포함되므로, 이를 기준으로 원본과 변환본을 구분할 수 있다.
결과
이미지 최적화만으로 약 500MB 이상 절감했다. 하지만 여전히 Tag HTML이 580MB를 차지하고 있었다.
2단계: Tag 페이지 경량화 - 클라이언트 사이드 렌더링(CSR) 전환
기존 구조의 문제
기존 term.html은 Hugo의 baseof.html을 상속받아 풀 렌더링했다.
flowchart TD
A["기존 구조"] --> B["baseof.html(사이드바, CSS, JS, 위젯 등)"]
B --> C["21,089개 태그 페이지"]
C --> D["페이지당 ~27KB"]
D --> E["총 540MB + 페이지네이션"]
21,089개의 태그 페이지 각각이 사이드바, 위젯, 전체 CSS/JS를 포함한 완전한 HTML을 생성했다. 대부분의 콘텐츠는 페이지마다 동일한 보일러플레이트였다.
CSR 전환 전략
핵심 아이디어는 공통 부분은 공유 JS 1개 파일로, 가변 부분은 최소한의 인라인 JSON으로 분리하는 것이다.
flowchart TD
A["새로운 구조"] --> B["standalone term.html(baseof.html 우회)"]
B --> C["인라인 JSON 데이터(태그명, 글 목록)"]
B --> D["공유 tag-page.js 1개(사이드바, 위젯, 페이지네이션 렌더링)"]
C --> E["21,089개 경량 HTML"]
E --> F["페이지당 ~3KB"]
F --> G["총 63MB"]
새로운 term.html
baseof.html을 완전히 우회하는 standalone HTML을 작성했다.
| |
핵심 설계 결정:
baseof.html우회:{{ define "main" }}블록을 사용하지 않고 완전한 HTML을 직접 출력하여,baseof.html의 사이드바/위젯/footer 보일러플레이트를 제거- 인라인 JSON: 각 태그의 글 목록(제목, URL, 날짜)만 최소한의 JSON으로 인라인
<noscript>SEO 폴백: JavaScript 비활성화 환경(검색엔진 크롤러 포함)을 위한 기본 콘텐츠 제공safeJS파이프: Hugo의 컨텍스트 자동 이스케이핑이<script>태그 내부에서 JSON을 깨뜨리는 문제 방지
tag-page.js: 클라이언트에서 전체 UI 렌더링
228줄의 JavaScript 파일 하나가 사이드바, 메인 콘텐츠, 오른쪽 위젯, 페이지네이션, 다크 모드, 푸터를 모두 렌더링한다.
| |
결과
| 지표 | 변경 전 | 변경 후 | 절감 |
|---|---|---|---|
| Tag HTML 총 크기 | 540 MB | 63 MB | 88% |
| 페이지당 평균 크기 | ~27 KB | ~3 KB | 89% |
| 공유 JS 파일 | 0 | 1개 (7KB) | - |
3단계: 빌드 성능 튜닝
Windows Defender 제외
Hugo 공식 문서에 따르면 Windows Defender의 실시간 검사가 빌드 시간을 400% 이상 증가시킬 수 있다. 빌드 스크립트에 자동 감지 로직을 추가했다.
| |
병렬 워커 수 증가
Hugo의 Go 루틴 기반 병렬 처리를 활용하여 워커 수를 늘렸다.
| |
Resample Filter 변경
이미지 리샘플링 필터를 고품질(Lanczos)에서 고속(Box)으로 변경했다.
| |
세그먼트 빌드
개발 중에는 전체 사이트가 아닌 특정 섹션만 빌드할 수 있도록 세그먼트를 정의했다.
| |
| |
templateMetrics로 병목 식별
Hugo의 --templateMetrics 플래그를 사용하면 각 템플릿의 실행 횟수와 소요 시간을 확인할 수 있다.
| |
cache potential: 100%인 partial은 partialCached로 전환할 수 있는 후보다.
4단계: partialCached로 빌드 시간 17분 → 2분
문제: 21,089회 반복 계산
term.html에서 사이트 데이터(S)와 위젯 데이터(W)를 인라인으로 생성하고 있었다. 이 데이터는 모든 태그 페이지에서 동일한 값이지만, 21,089번 반복 계산되고 있었다.
특히 위젯 데이터(W)는 .Site.Taxonomies.categories.ByCount, .Site.Taxonomies.tags.ByCount, .Site.RegularPages 등 비용이 큰 쿼리를 포함하고 있었다.
flowchart LR
subgraph before ["변경 전: 17분"]
A["term.html"] -->|"21,089회 실행"| B["S 변수 계산(사이트 제목, 아바타 등)"]
A -->|"21,089회 실행"| C["W 변수 계산(카테고리, 태그, 아카이브 쿼리)"]
end
subgraph after ["변경 후: 2분"]
D["site-data.html"] -->|"1회 실행"| E["S 변수 (캐시됨)"]
F["widget-data.html"] -->|"1회 실행"| G["W 변수 (캐시됨)"]
H["term.html"] -->|"21,089회"| I["T 변수만 계산(태그 고유 데이터)"]
end
해결: partialCached로 분리
공통 데이터를 별도 partial로 추출하고 partialCached를 적용했다.
layouts/partials/tag-page/site-data.html:
| |
layouts/partials/tag-page/widget-data.html:
| |
term.html에서의 호출:
| |
partialCached에 "global" 캐시 키를 전달하면, 모든 태그 페이지에서 동일한 캐시된 결과를 재사용한다. 21,089번의 반복 계산이 1번으로 줄어든다.
Hugo 컨텍스트 Auto-Escaping 함정
partialCached의 출력을 <script> 태그 내부에 넣으면, Hugo의 컨텍스트 자동 이스케이핑이 JavaScript 문맥에서 HTML 엔티티로 변환해버리는 문제가 있었다.
| |
해결책은 각 partial이 자체적으로 <script> 태그를 포함하도록 하여, partial의 출력이 HTML 컨텍스트에서 렌더링되게 하는 것이다.
| |
결과
| 지표 | 변경 전 | 변경 후 |
|---|---|---|
| term.html 빌드 시간 | 17분 25초 | ~2분 |
| term.html 평균 실행 시간 | 49.58ms/페이지 | ~5ms/페이지 |
| S, W 변수 계산 횟수 | 21,089회 | 1회 |
5단계: 폰트 FOUT 해결
문제: Flash of Unstyled Text
Hugo Theme Stack은 Google Fonts의 Lato 폰트를 JavaScript로 동적 로딩한다.
| |
이 방식은 페이지가 먼저 시스템 폰트로 렌더링된 후 Lato로 전환되면서 FOUT(Flash of Unstyled Text)가 발생한다.
해결: head에서 직접 로딩
<head>에서 <link> 태그로 직접 로딩하면 브라우저가 렌더링 전에 폰트 다운로드를 시작한다.
layouts/partials/head/custom.html:
| |
layouts/partials/footer/components/custom-font.html (빈 오버라이드):
| |
테마의 원본 파일을 수정하지 않고, Hugo의 레이아웃 오버라이드 메커니즘을 활용하여 로컬 layouts/ 디렉토리에 동일 경로의 파일을 배치했다.
6단계: 기타 최적화
RSS 피드 제한
| |
800개 이상의 글 전체를 포함하던 RSS 피드를 최근 50개로 제한하고, 전문 대신 요약만 포함하도록 변경했다.
Pagefind 검색 인덱스 캐싱
클라이언트 검색 엔진 Pagefind의 인덱스 생성을 조건부로 실행하여, 이미 캐시된 인덱스가 있으면 빌드를 건너뛰도록 했다.
| |
최종 결과 요약
flowchart LR
subgraph before_opt ["최적화 전"]
A["public/ 1,797MB빌드 시간 20분+"]
end
subgraph step1 ["이미지 최적화"]
B["WebP 변환PNG 삭제-500MB"]
end
subgraph step2 ["Tag CSR 전환"]
C["standalone HTML+ 공유 JS-477MB"]
end
subgraph step3 ["partialCached"]
D["S, W 캐싱빌드 17분→2분"]
end
subgraph after_opt ["최적화 후"]
E["public/ ~600MB빌드 시간 ~2분"]
end
before_opt --> step1 --> step2 --> step3 --> after_opt
| 최적화 항목 | 크기 절감 | 시간 절감 |
|---|---|---|
| 이미지 WebP 변환 + PNG 삭제 | -500 MB | - |
| Tag 페이지 CSR 전환 | -477 MB | - |
| partialCached 적용 | - | -15분 |
| 빌드 스크립트 튜닝 | - | 체감 30%+ |
| 폰트 FOUT 수정 | - | UX 개선 |
| RSS 제한 | -수 MB | - |
| 합계 | ~1,200 MB 절감 | 빌드 20분+ → 2분 |
적용 시 주의사항
- WebP 호환성: Hugo Extended 버전이 필요하다. 일반 Hugo에서는 WebP 변환이 동작하지 않는다.
- SEO 폴백: CSR 전환 시
<noscript>태그로 검색엔진 크롤러를 위한 폴백을 반드시 제공해야 한다. - Hugo 컨텍스트 이스케이핑:
<script>태그 내에서jsonify를 사용할 때| safeJS파이프를 빠뜨리면 HTML 엔티티로 변환되어 JavaScript가 깨진다. - partialCached 캐시 키:
"global"같은 고정 키를 사용하면 모든 페이지에서 동일한 결과를 공유한다. 페이지별로 다른 결과가 필요하면.RelPermalink등을 키로 사용해야 한다. - Windows Defender: Windows 환경에서 Hugo를 사용한다면
hugo.exe를 Defender 제외 목록에 추가하는 것만으로 빌드 시간이 극적으로 줄어든다.
참고 링크
- GitHub Pages Limits - GitHub Pages 용량(1GB), 대역폭(100GB/월) 등 공식 제한 사항
- Hugo Image Processing - Hugo 내장 이미지 처리 기능 (Resize, Fill, Fit, WebP 변환 등)
- Hugo partialCached -
partialCached함수로 partial 출력을 캐싱하여 빌드 성능을 개선하는 방법 - Hugo Build Performance -
--templateMetrics플래그를 활용한 빌드 병목 식별 및 최적화 가이드 - Hugo Theme Stack - 이 블로그에서 사용하는 Hugo 테마
- WebP - An image format for the Web - Google의 WebP 포맷 공식 문서 (PNG 대비 26% 감소, JPEG 대비 25-34% 감소)
- Google Fonts CSS API -
display=swap파라미터와preconnect를 활용한 폰트 로딩 최적화 - Pagefind - Hugo 등 정적 사이트를 위한 클라이언트 사이드 검색 엔진
![Featured image of post [Hugo] GitHub Pages 1GB 한계 극복 - 빌드 최적화 실전 가이드](/post/2026-02-24-hugo-build-optimization-github-pages/poster_hu_57f4989091c67cb3.webp)
![[Hugo] 블로그 태그 50개 이상 확장 - Fallback 풀 기반 자동 보강](/post/2026-02-24-tag-improvement-summary/wordcloud_hu_78a356070ab81540.webp)
![[CSS] 모던 CSS 완전 정복: 2015년처럼 CSS 쓰는 시대는 끝났다](/post/2026-02-23-modern-css-stop-writing-like-2015/wordcloud_hu_dd551dfa54409f84.webp)
![[AI] DeepSeek-OCR - 비전-텍스트 압축 기술의 새로운 패러다임](/post/2025-10-24-deepseek-ocr/image_hu_3892839e02628c2e.webp)
![[Hugo] GitHub Pages에서 301 없이 리디렉션 최적화 - Hugo alias 가이드](/post/2025-08-29-github-pages-redirect-301-hugo-alias-seo/wordcloud_hu_b3dfef47c2d994f5.webp)
![[Hugo] Hugo Archetypes 완전 가이드: 콘텐츠 템플릿 시스템 마스터하기](/post/2025-08-06-hugo-archetypes-complete-guide/wordcloud_hu_9f2588c117b9ea36.webp)