글실험 기록

썸네일 200장을 파일 4개로: 요청 수를 줄인 이미지 묶음 로딩

UX/UI 소개 페이지는 열 때마다 작은 JPG 200장을 하나씩 요청했습니다. 이미지를 50장씩 묶은 파일 4개로 바꿔, 이 사이트로 가는 요청을 200개 넘게 줄인 방법입니다.

UX/UI 요소 썸네일이 벽처럼 늘어선 소개 페이지

UX/UI Prompt 소개 페이지에는 갤러리 요소의 썸네일이 벽처럼 늘어서 있습니다. 썸네일은 요소마다 한 장씩, 언어마다 200장입니다. 한 장은 평균 4KB 남짓한 작은 JPG입니다.

10월 15일에 사이트 사용량을 살펴보다가 이 페이지가 눈에 들어왔습니다. 페이지를 한 번 열고 끝까지 스크롤하면 요청이 210~235개 나갔고, 그중 200개가 썸네일 JPG였습니다. 파일 크기를 다 합쳐도 832KB로 크지 않았지만, 요청 수가 문제였습니다.

요청 수가 왜 문제였나

HTTP/2에서는 연결 하나로 여러 요청을 동시에 보낼 수 있어서, 작은 파일 수백 개도 예전만큼 느리지는 않습니다. 그래도 요청마다 머리 정보가 오가고, 브라우저와 서버가 각각 처리할 일이 생깁니다.

더 직접적인 이유는 호스팅이었습니다. 이 사이트는 Vercel에서 운영하는데, 사용량 화면에서 CDN 요청 수가 생각보다 빠르게 늘고 있었습니다. 방문자 한 명이 이 페이지를 한 번 보면 요청 200개가 추가되는 구조였습니다. 같은 이미지를 더 적은 요청으로 보내면 방문자도, 운영 비용도 가벼워집니다.

방법: 50장씩 한 파일로 묶는다

빌드할 때 썸네일 JPG를 50장씩 이어 붙여 바이너리 파일 하나로 만듭니다. 언어마다 4개, 한국어와 영어를 합쳐 8개입니다.

  • 파일 이름에 내용 해시를 넣습니다(thumbs-ko-0.27e198e74206.bin). 내용이 바뀌면 이름도 바뀌므로, 브라우저와 CDN이 1년 동안 그대로 캐시해도 됩니다.
  • 각 썸네일이 파일 안 어디서 시작해서 몇 바이트인지를 목록으로 만들어 페이지 데이터에 함께 넣습니다.
  • 첫 번째 묶음에는 처음 화면에 보이는 썸네일을 먼저 넣습니다. 첫 화면을 그리는 데 필요한 이미지가 가장 먼저 도착하게 하려는 것입니다.

브라우저에서는 묶음 파일을 한 번 받은 뒤, 목록을 보고 잘라서 이미지로 씁니다.

묶음을 받아 썸네일로 나누기 (요약)
async function loadPack(pack) {
  const buf = await (await fetch(pack.url)).arrayBuffer();
  for (const [id, offset, length] of pack.items) {
    const blob = new Blob([buf.slice(offset, offset + length)], { type: 'image/jpeg' });
    urls.set(id, URL.createObjectURL(blob));   // <img src>에 그대로 넣는다
  }
}

URL.createObjectURL로 만든 blob: 주소는 브라우저 메모리 안의 데이터를 가리킵니다. 그래서 이미지 200장을 그려도 네트워크 요청은 묶음 파일 4개뿐입니다.

실패에 대비한 장치

묶음 하나가 받아지지 않으면 그 안의 50장이 한꺼번에 빠집니다. 그래서 개별 JPG 파일도 지우지 않고 남겨 두었습니다.

  • 묶음을 받는 중이면 그 썸네일은 잠시 기다립니다.
  • 묶음 받기에 실패하면 그 묶음은 실패로 표시하고, 해당 썸네일은 원래의 개별 파일 주소로 불러옵니다.
  • 개별 파일도 실패하면 한 번 더 시도합니다.

평소에는 쓰이지 않지만, 네트워크가 불안정할 때 화면 일부가 비는 일을 막습니다.

결과

오늘(10월 24일) 운영 중인 페이지를 열고 끝까지 스크롤하며 다시 쟀습니다.

항목바꾸기 전지금
이 사이트(lucianlabs.dev)로 가는 요청210~235개13개
썸네일 JPG 개별 요청200개0개
썸네일 묶음 파일 요청없음4개 (187~227KB)
화면에 그려진 썸네일200장246장, 모두 묶음에서

언어를 영어로 바꾸면 영어 썸네일 묶음 4개와 미리보기 이미지 1개, 모두 5개 요청이 추가됩니다. 썸네일마다 글자가 들어 있어 언어별로 따로 만들었기 때문입니다.

대신 포기한 것

공짜는 아닙니다.

  • 필요 없는 이미지까지 받는다. 첫 화면만 보고 나가는 방문자도 첫 묶음 50장을 다 받습니다. 첫 묶음에 처음 보이는 썸네일을 몰아 둔 이유가 이것입니다.
  • 첫 이미지가 조금 늦을 수 있다. 작은 파일 하나보다 200KB짜리 묶음이 도착하는 데 더 걸립니다. 이 페이지에서는 썸네일이 화면에 들어오기 전에 미리 불러오기 때문에 차이가 거의 보이지 않았습니다.
  • 빌드 과정이 하나 늘었다. 썸네일을 바꾸면 묶음도 다시 만들어야 합니다. 빌드 스크립트가 자동으로 하므로 손이 더 가지는 않습니다.

이미지 여러 장을 한 장으로 합치는 CSS 스프라이트도 같은 목적의 오래된 방법입니다. 다만 합친 그림을 다시 압축하면 화질이 한 번 더 손상되고, 썸네일마다 위치를 계산해 잘라 보여줘야 합니다. JPG 파일을 그대로 이어 붙이는 쪽은 원본을 건드리지 않고, 화면에서는 평범한 <img>로 쓸 수 있어서 이 페이지에 더 맞았습니다.

이런 경우에 쓸 만하다

  • 작은 이미지가 수십~수백 장이고, 대부분 한 번에 보이는 페이지
  • 이미지가 자주 바뀌지 않아 오래 캐시할 수 있는 경우
  • 호스팅 요금이나 한도에 요청 수가 들어가는 경우

반대로 사용자가 스크롤해야 조금씩 보는 긴 목록이라면, 보이는 이미지만 그때그때 받는 지연 로딩이 더 낫습니다. 이 소개 페이지는 썸네일 벽이 한 화면을 가득 채우는 구조라 묶음 쪽이 맞았습니다.

이 글에서 다룬 도구

Lucian UX/UI Prompt

보고, 눌러 보고, 프롬프트로 가져가세요

자세히 보기

루샨 · lucianLabs 운영자

AI 에이전트와 일하며 필요한 도구를 직접 만들고, 그 과정에서 얻은 숫자와 시행착오를 기록합니다. 글에 틀린 내용이 있으면 알려주세요.