birchholt 직접 해보고 남기는 기록

Hugo static/에 둔 이미지는 원본 그대로 나갑니다 — 빌드 때 줄이고 WebP로 바꾸기

마크다운에 ![설명](/images/글/00.png) 한 줄을 쓰면 Hugo가 알아서 줄여서 내보낼 거라고 생각하기 쉽습니다. 재보니 그렇지 않았습니다. 이미지를 어디에 두느냐에 따라 Hugo가 아예 손을 안 댑니다. 2026-10-04에 작은 테스트 사이트를 따로 만들어 세 가지 방법을 나란히 재봤습니다.

커다란 액자를 그대로 우편함에 밀어 넣으려는 사람 옆에서 작은 엽서 묶음을 건네는 이웃

테스트 조건은 이렇습니다. 글 8편에 이미지를 1장씩 넣었고 이미지는 가로 2560px PNG 8장(합계 7,151,706바이트, 약 7.2MB)입니다. 기존 삽화를 macOS의 sips로 키워서 PNG로 바꾼 것이라 실제 사진보다는 단순한 그림입니다. Hugo는 v0.165.0 extended, 컴퓨터는 Apple M2 Max입니다.

static/ 에 둔 이미지는 Hugo가 그대로 복사만 합니다

창고 문 앞에 쌓인 상자들을 열어보지도 않고 그대로 트럭에 옮겨 싣는 짐꾼

가장 흔한 배치부터 봤습니다. static/images/posts/p1/00.png 에 두고 본문에서 /images/posts/p1/00.png 로 부르는 방식입니다. 빌드 출력은 이랬습니다.

 Static files     │  8
 Processed images │  0

Processed images(Hugo가 처리한 이미지 수)가 0입니다. 8장이 그대로 복사됐고 결과물 이미지는 7,151,706바이트, 원본과 같았습니다. 나온 HTML에는 width·height도 없습니다.

<img src="/images/posts/p1/00.png" alt="테스트 1" loading="eager" decoding="async">

Hugo 문서에도 이렇게 나와 있습니다. 이미지를 처리하려면 파일을 page resource(글 폴더 안의 파일), global resource(assets/ 안의 파일), remote resource(외부 URL) 중 하나로 가져와야 합니다 (Hugo · Image processing). static/ 은 이 셋 어디에도 들지 않습니다. 템플릿에서 resources.Get 으로 찾으면 아무것도 돌아오지 않습니다.

그래서 static/ 에 둔 채로 줄이고 싶으면 빌드 전에 직접 변환해야 합니다.

$ cwebp -q 78 -resize 1280 0 00.png -o 00.webp

8장을 이렇게 바꿨더니 185,298바이트가 됐습니다. 이미지 하나에 화질 하나, 크기 하나로 끝나는 사이트면 이 방법으로도 충분합니다. 다만 원본을 고칠 때마다 다시 돌려야 하고 화면 폭에 맞춰 여러 크기를 내보내려면 그만큼 명령을 더 돌려야 합니다.

assets/ 로 옮기고 render-image 훅에서 줄이기

원목 판재를 작업대에 올려 세 가지 폭으로 재단해 나란히 세워두는 목수

원본을 assets/images/posts/p1/00.png 로 옮기고 본문 마크다운은 한 글자도 바꾸지 않았습니다. 대신 render-image 훅(마크다운 이미지 한 줄을 HTML로 바꿀 때 Hugo가 부르는 템플릿, layouts/_markup/render-image.html)을 고쳤습니다.

{{- $u := urls.Parse .Destination -}}
{{- $img := "" -}}
{{- if not $u.IsAbs -}}
  {{- with .Page.Resources.Get $u.Path -}}
    {{- $img = . -}}
  {{- else -}}
    {{- with resources.Get (strings.TrimPrefix "/" $u.Path) -}}{{- $img = . -}}{{- end -}}
  {{- end -}}
{{- end -}}
{{- with $img -}}
  {{- $set := slice -}}
  {{- $main := . -}}
  {{- range slice 640 960 1280 -}}
    {{- if le . $img.Width -}}
      {{- $r := $img.Resize (printf "%dx webp q78" .) -}}
      {{- $set = $set | append (printf "%s %dw" $r.RelPermalink $r.Width) -}}
      {{- $main = $r -}}
    {{- end -}}
  {{- end -}}
<img src="{{ $main.RelPermalink }}" srcset="{{ delimit $set ", " }}"
     sizes="(max-width: 760px) 100vw, 720px"
     width="{{ $main.Width }}" height="{{ $main.Height }}" alt="{{ $.PlainText }}">
{{- else -}}
  {{- warnf "render-image: %q 를 리소스로 못 찾았습니다 (%s)" $.Destination $.Page.File.Path -}}
<img src="{{ .Destination | safeURL }}" alt="{{ .PlainText }}">
{{- end -}}

핵심은 Resize "1280x webp q78" 한 줄입니다. 가로 1280px로 줄이고 높이는 비율대로, 형식은 WebP, 화질은 78이라는 뜻입니다. 화질을 적지 않으면 Hugo 기본값이 들어가는데, hugo config 로 보니 [imaging.webp] quality = 75 였습니다. 원본보다 큰 폭은 만들지 않도록 le . $img.Width 로 걸렀습니다.

빌드하면 이미지 한 장에서 세 가지 크기가 나오고 HTML은 이렇게 됩니다.

<img src="/images/posts/p1/00_hu_1b9308e35ad72f41.webp"
     srcset="/images/posts/p1/00_hu_e764dab69b4f76f0.webp 640w, /images/posts/p1/00_hu_c5404b2cbd0d4062.webp 960w, /images/posts/p1/00_hu_1b9308e35ad72f41.webp 1280w"
     sizes="(max-width: 760px) 100vw, 720px" width="1280" height="715" alt="테스트 1" ...>

srcset(화면 폭에 맞는 파일을 브라우저가 고르도록 후보를 나열한 속성)과 width·height 가 같이 나왔습니다. 원본 PNG는 결과물에 아예 없었습니다. 이 훅은 줄인 WebP의 주소(RelPermalink)만 꺼내고 원본의 주소는 꺼내지 않습니다. 아래에서 보듯 원본의 주소를 꺼내는 훅이면 PNG가 그대로 나갑니다.

마지막 warnf 는 덤입니다. 같은 훅을 static/ 에 둔 이미지에 걸어보니 빌드 때 이렇게 8줄이 찍혔습니다.

WARN  render-image: "/images/posts/p1/00.png" 를 리소스로 못 찾았습니다 (posts/p1.md)

이 줄이 없으면 처리가 빠진 이미지가 경고 없이 원본 그대로 나갑니다.

세 가지 방법을 나란히 재봤습니다

같은 짐을 커다란 궤짝, 작은 상자, 크기별 상자 세 개에 나눠 담아 저울 위에 나란히 올려둔 장면
① static 원본② 수동 cwebp③ Hugo 자동 변환
결과물 이미지8개, 7,151,706 B8개, 185,298 B24개, 396,384 B
1280px 파일만 합치면—185,298 B184,404 B
width·height없음없음있음
srcset없음없음640·960·1280
빌드 시간(첫 빌드)0.05~0.06초0.04~0.05초0.93~0.96초
빌드 시간(두 번째부터)0.05초0.04~0.05초0.05초

빌드 시간은 /usr/bin/time -p 로 세 번씩 잰 real 값입니다. 수동 cwebp 8장 변환은 따로 0.65초가 걸렸습니다. ③의 합계가 ②보다 큰 것은 640·960px 파일까지 더한 값이라서입니다.

1280px끼리만 놓고 보면 cwebp와 Hugo가 거의 같았습니다. 장마다 차이는 2바이트에서 1,662바이트 사이였고, 8장 중 4장은 cwebp가, 4장은 Hugo가 더 컸습니다. 예를 들어 첫 장은 cwebp 13,398바이트, Hugo 13,758바이트였고 마지막 장은 45,588바이트와 43,926바이트였습니다. 용량만 보면 ②와 ③은 같다고 봐도 됩니다. (화질 비교는 하지 않았습니다.) 차이는 손이 얼마나 가느냐와 srcset·width·height 가 자동으로 붙느냐에서 납니다.

assets/ 에 두되 훅이 처리를 안 하는 경우도 재봤습니다. 이미지 크기만 읽어 width·height 를 박는 훅이면 PNG 8장이 그대로 나가고 width="2560" height="1430" 이 붙습니다. assets/ 로 옮기는 것만으로 줄어들지는 않습니다. Resize 같은 처리 메서드를 불러야 줄어듭니다.

첫 빌드만 1초인 이유 — resources/_gen

공방 선반에 미리 잘라둔 조각들을 쌓아두고 다음 날 그대로 꺼내 쓰는 목수

③은 첫 빌드가 0.9초대였는데 두 번째부터는 0.05초였습니다. 처리 결과가 resources/_gen/images/ 에 남기 때문입니다. hugo config 로 기본값을 보면 이렇게 나옵니다.

[caches.images]
  dir = ':resourceDir/_gen'
  maxage = -1

maxage = -1 은 만료가 없다는 뜻입니다(Hugo · Configure file caches). 24장을 만든 뒤 이 폴더는 428K, 파일 24개였습니다.

캐시가 언제 깨지는지도 확인했습니다.

여기서 하나 걸리는 게 있습니다. 저는 resources/ 를 .gitignore 에 넣어 두었고 배포 워크플로에도 이 폴더를 캐시하는 단계가 없습니다. 이 상태로 이미지 처리를 쓰면 배포 서버는 매번 처음 빌드하는 셈입니다. 제 컴퓨터에서는 24장에 1초 정도라 문제가 없었지만 이미지가 많은 사이트라면 이 처리 시간이 배포마다 반복됩니다. CI에서 resources/ 를 캐시했을 때 얼마나 줄어드는지는 이번에 재지 못했습니다.

글 폴더(page bundle)에 넣으면 원본도 같이 나갑니다

새 액자들을 내보내는 화랑 직원 뒤로 포장도 안 뜯은 원본 캔버스가 같은 수레에 실려 나가는 장면

assets/ 대신 글 폴더에 이미지를 같이 두는 방법도 있습니다. content/posts/p1/index.md 옆에 00.png 를 두고 본문에 ![번들 이미지](00.png) 로 쓰는 방식이고 이걸 page bundle이라고 부릅니다. 위 훅이 .Page.Resources.Get 을 먼저 찾기 때문에 같은 srcset 이 나왔습니다.

그런데 결과물 폴더를 보니 파일이 하나 더 있었습니다.

public/posts/p1/00_hu_1b9308e35ad72f41.webp
public/posts/p1/00_hu_c5404b2cbd0d4062.webp
public/posts/p1/00_hu_e764dab69b4f76f0.webp
public/posts/p1/00.png                      ← 628,852 B 원본

HTML 어디에도 연결되지 않은 원본 PNG가 같이 배포됩니다. 글 폴더 안의 파일은 기본적으로 전부 내보내기 때문입니다 (publishResources: true 가 기본값, Hugo · Build options). 글의 front matter에 아래를 넣자 원본이 빠지고 WebP 3개만 남았습니다.

build:
  publishResources: false

assets/ 쪽에서는 이 설정이 필요 없었습니다.

다시 한다면 이 순서로 하겠습니다

정리된 공방 벽에 원목 판재 보관함, 재단 지그, 남은 조각 상자가 차례로 걸려 있는 모습
  1. 빌드 출력의 Processed images 가 0인지 먼저 봅니다. 본문 이미지가 있는데 0이면 원본이 그대로 나가고 있는 겁니다
  2. 원본은 static/ 이 아니라 assets/ 에 둡니다. page bundle을 쓰면 publishResources: false 를 같이 넣습니다
  3. render-image 훅에서 Resize "1280x webp q78" 처럼 폭·형식·화질을 한 번에 지정하고 srcset·width·height 를 같이 냅니다
  4. 리소스를 못 찾으면 warnf 로 빌드 로그에 남깁니다. 조용히 원본이 나가는 걸 막는 장치입니다
  5. 화질 값을 바꾼 뒤에는 hugo --gc 로 resources/_gen 의 옛 파일을 정리합니다

용량만 줄이는 게 목적이면 수동 cwebp도 결과 용량이 거의 같았습니다. 제가 훅을 고른 이유는 원본을 한 번 넣어두면 크기 여러 개와 width·height 까지 빌드가 맞춰주기 때문입니다.