속도도 디자인이다
출근길 지하철에서 서점 사이트를 연다. 주소를 누르고 나서 화면은 한참 하얗다. 겨우 글자가 나오는가 싶더니 사진 자리가 비어 있고, 그 사이 "신청하기"를 누르려는 순간 사진이 도착해 버튼이 아래로 밀려난다. 엉뚱한 곳을 누른다. 이 모든 일은 코드의 문제처럼 보이지만, 시작은 디자인의 결정이었다. 첫 화면에 어떤 크기의 사진을 몇 장 둘지, 글꼴을 몇 벌 쓸지, 사진이 올 자리를 미리 잡아 둘지. 이 장은 속도를 숫자로 재고, 그 숫자를 움직이는 디자인의 손잡이를 찾는다.
- 페이지 무게와 통신 속도, 왕복 지연으로 내려받는 시간을 어림하고, 첫 화면 무게 예산을 세운다.
- Core Web Vitals의 세 지표 LCP·INP·CLS가 각각 무엇을 재는지, "좋음" 기준값(2.5초, 200밀리초, 0.1)과 75번째 백분위의 뜻을 안다.
- 가장 큰 콘텐츠(대개 첫 화면 사진)가 늦게 그려지는 원인을 폭포 그림으로 읽고, 사진 크기·형식·불러오기 우선순위로 고친다.
- 한글 웹폰트의 용량을 서브셋과 조각 나누기로 줄이는 효과를 잰다.
- 레이아웃 이동 점수(영향 비율 × 거리 비율)를 계산하고, 자리 잡기와 대체 글꼴 크기 맞추기로 줄인다.
- 스켈레톤과 스피너, 즉각적인 눌림 반응이 체감 속도에 하는 일과 그 근거의 수준을 안다.
페이지의 무게
웹 페이지는 한 장의 그림이 아니라 여러 파일의 묶음이다. 브라우저는 먼저 HTML을 받고, 그 안에 적힌 CSS, 자바스크립트, 글꼴, 사진을 다시 요청한다. 이 파일들을 통신망으로 받은 양을 합한 것을 흔히 페이지 무게(Page weight)라고 부른다. 압축해서 보낸 실제 전송량으로 센다.
내려받는 시간은 두 가지로 정해진다. 하나는 대역폭(Bandwidth), 곧 1초에 흘려보낼 수 있는 양이다. 다른 하나는 왕복 지연(Round-trip time, RTT), 곧 요청이 서버에 갔다가 첫 응답이 돌아오기까지의 시간이다. 처음 서버에 접속할 때는 주소 찾기, 연결, 암호화 약속에 왕복이 여러 번 들고, 파일을 요청할 때마다 다시 한 번씩 든다.
식의 두 항은 서로 다른 디자인 결정과 이어진다. 둘째 항은 무게다. 사진 한 장을 4 MB에서 150 KB로 줄이면 느린 통신에서 수십 초가 1초 안쪽이 된다. 첫째 항은 단계다. HTML을 받아야 CSS를 알고, CSS를 받아야 글꼴을 알고, 자바스크립트가 돌아야 사진을 아는 식으로 발견이 늦어질수록 왕복이 쌓인다. 통신이 느린 곳일수록 왕복 지연도 길어서, 무게가 같아도 단계가 많은 페이지가 더 늦다.
아래 막대는 책방 E 새 사이트의 첫 화면을 이루는 파일의 무게다. 처음 값은 지후가 2월에 올린 시험판(가상 자료)이다. 막대의 경계를 끌어 각 갈래의 무게를 바꾸고, 네 가지 통신에서 다 받는 데 걸리는 시간을 본다.
첫 화면 무게 예산
이렇게 페이지가 넘지 말아야 할 무게나 시간을 미리 정해 두는 것을 성능 예산(Performance budget)이라고 한다. 예산은 디자인 회의의 언어가 된다. "첫 화면에 사진을 한 장 더 넣고 싶다"는 말은 "사진 갈래의 150 KB를 둘로 나누거나, 다른 갈래에서 빼 오자"는 말이 된다. 자동 재생 영상, 사진이 넘어가는 큰 슬라이더, 장식용 글꼴 한 벌은 모두 예산표 위에서 다른 무엇과 맞바꾸는 물건이다.
세 가지 지표: LCP·INP·CLS
"빠르다"는 느낌은 숫자 하나로 잡히지 않는다. 첫 글자가 빨리 떠도 주인공 사진이 늦게 뜨면 느리고, 다 떴어도 누를 때마다 반응이 굼뜨면 느리고, 빨리 떴어도 화면이 들썩이면 불안하다. 구글이 제안하고 web.dev에서 설명하는 Core Web Vitals(핵심 웹 지표)는 이 경험을 세 지표로 나눈다.
- LCP(Largest Contentful Paint): 페이지를 열기 시작한 때부터 첫 화면에서 가장 큰 그림이나 글 덩어리가 그려질 때까지의 시간. "주인공이 언제 나타났나"를 잰다.
- INP(Interaction to Next Paint): 누르거나 탭하거나 키를 쳤을 때부터 그 결과가 다음 화면에 그려질 때까지의 시간. 한 번 방문 동안의 상호작용 가운데 가장 느린 축(상호작용이 아주 많으면 극단값 몇 개는 뺀다)을 대표값으로 쓴다. 2024년 3월부터 이전 지표 FID를 대신한다.
- CLS(Cumulative Layout Shift): 사용자가 건드리지 않았는데 화면의 요소가 밀려 움직인 정도를 점수로 모은 것. 단위가 없다.
"75번째 백분위"는 방문 100번을 빠른 순서로 줄 세웠을 때 75번째 방문의 값이다. 방문의 4분의 3이 이 값보다 빠르거나 같다. 평균이 아니라 이 값을 쓰는 것은, 느린 기기나 나쁜 통신으로 들어온 사람들까지 꽤 많이 품으라는 뜻이다. 회사 노트북과 와이파이로 잰 한 번의 숫자는 이 값보다 대개 훨씬 좋다.
사진: 가장 무거운 짐
대부분의 페이지에서 가장 무거운 것은 사진이고, 첫 화면의 큰 사진은 대개 LCP 요소다. 사진의 무게는 ImageBook 10장에서 쟀다. 같은 사진도 화소 수와 형식, 압축 품질에 따라 수십 배가 달라진다. 웹 화면에 필요한 화소 수는 그 사진이 화면에 그려지는 CSS 폭 × 기기 픽셀 비율이면 충분하다(2장). 360px 폭 휴대폰에서 화면 가득 보이는 사진은 기기 픽셀 비율 2에서 가로 720화소, 3에서 1,080화소면 된다. 4,000화소 원본을 그대로 올리면 쓰지도 않을 화소를 보내는 셈이다.
| 첫 화면 사진(같은 장면) | 대략의 무게 | 느린 모바일에서 전송만 | 근거 |
|---|---|---|---|
| 휴대폰 카메라 원본 JPEG(가로 4,000화소) | 4,000 KB | 약 20초 | 가상 자료 |
| 가로 1,600화소 JPEG 품질 85 | 365 KB | 약 1.8초 | ImageBook 10장의 측정 |
| 가로 1,600화소 WebP 품질 80 | 220 KB | 약 1.1초 | ImageBook 10장의 측정 |
| 가로 800화소 WebP 품질 80 | 75 KB | 약 0.4초 | 가상 어림(면적이 4분의 1) |
ImageBook 10장의 숫자는 수채화 한 점을 크롬 인코더로 저장해 잰 것이고, 사진마다 다르다. 가로 800화소의 값은 화소 수가 4분의 1이 될 때 무게가 대략 3분의 1쯤 된다고 본 이 책의 어림이다. 전송 시간은 1.6 Mbps에서 전송량만 나눈 것이라 왕복 지연은 빠져 있다. AVIF 같은 더 새로운 형식은 같은 품질에서 더 가벼운 경우가 많지만, 인코딩이 느리고 지원 범위를 확인해야 한다.
한 장의 사진을 여러 폭으로 만들어 두고 브라우저가 고르게 하는 방법은 5장의 srcset과 sizes다. 여기에 속도를 위해 세 가지를 더 적는다.
<!-- 첫 화면의 주인공: 비율(width·height)로 자리를 잡고, 먼저 받는다 -->
<img src="reading-800.webp"
srcset="reading-480.webp 480w, reading-800.webp 800w, reading-1600.webp 1600w"
sizes="(min-width: 960px) 600px, 100vw"
width="1600" height="900"
fetchpriority="high"
alt="서가 앞에 둥글게 앉은 손님들이 낭독을 듣고 있다">
<!-- 첫 화면 밖의 사진은 보일 때가 가까워지면 받는다 -->
<img src="shelf-800.webp" loading="lazy" width="800" height="600" alt="…">
width와 height는 사진을 그 크기로 늘리라는 말이 아니다. CSS로 폭을 100%로 두어도 브라우저는 두 숫자의 비율로 사진이 오기 전에 높이를 계산해 자리를 비워 둔다(아래 "밀리는 화면" 절). fetchpriority="high"는 이 사진을 다른 사진보다 먼저 받으라는 힌트이고, loading="lazy"는 화면에 보일 때가 가까워질 때까지 받지 말라는 지시다. 그래서 lazy는 첫 화면 밖의 사진에만 쓴다. 첫 화면 사진에 lazy를 붙이면 브라우저가 레이아웃을 마친 뒤에야 요청을 시작해 LCP가 늦어진다.
가장 큰 콘텐츠가 그려질 때
LCP 시간은 네 구간의 합으로 나누어 볼 수 있다. web.dev의 LCP 최적화 안내(Optimize Largest Contentful Paint)가 쓰는 구분이다.
아래는 책방 E 첫 화면을 여는 과정의 폭포 그림(Waterfall chart)이다. 줄마다 파일 하나이고, 가는 선은 응답을 기다리는 시간, 굵은 막대는 받는 시간이다. 아래 필름 띠는 그 시각의 화면이다. 사진의 크기, 불러오는 방식, 통신을 바꿔 본다.
첫 화면 사진과 LCP
모형이 보여 주는 순서대로 고치면 된다. 먼저 무게(③): 화면에 필요한 화소만, 가벼운 형식으로. 다음은 발견(②): LCP 사진은 HTML 안에 img로 적어 브라우저가 HTML을 읽는 즉시 찾게 하고, lazy를 붙이지 않는다. 자바스크립트가 나중에 끼워 넣는 사진이나 CSS 배경 사진은 발견이 늦다. 마지막이 우선순위: fetchpriority="high"나 <link rel="preload">로 주인공을 앞줄에 세운다. 첫 화면에 큰 사진 대신 글자 제목을 주인공으로 두는 것도 진지한 선택지다. 글자는 HTML과 함께 오므로 대개 가장 빨리 그려진다.
한글 웹폰트의 무게
TypeBook 11장에서 한글 웹폰트가 왜 무거운지 쟀다. 현대 한글 음절만 11,172자이고, 한자와 기호까지 담은 글꼴은 1만 7천 자가 넘는다. 그 책이 2026년 10월에 잰 Noto Sans KR 보통 굵기 하나는 조각을 모두 합해 약 1.87 MB였다. 라틴 글꼴 한 굵기(약 24 KB)의 80배쯤이다.
무게를 줄이는 길은 두 가지다. 서브셋(Subset)은 쓰지 않을 글자를 글꼴 파일에서 지우는 것이고, 조각 나누기(unicode-range)는 글꼴을 여러 파일로 나눠 페이지에 나온 글자가 든 조각만 받게 하는 것이다. 조각 나누기의 실제 모습은 TypeBook 11장의 시뮬레이터에서 볼 수 있다. 여기서는 서브셋의 무게가 글자 수에 따라 어떻게 변하는지, 그리고 그것이 첫 화면 예산에서 무엇을 뜻하는지 본다.
글자 수와 웹폰트의 무게
서브셋은 가장 가볍지만 함정이 있다. 나중에 글을 고치다 서브셋에 없는 글자가 들어가면 그 글자만 다른 글꼴로 튀어나온다. 그래서 내용이 바뀌는 본문은 조각 나누기로, 글자가 고정된 로고와 장식 제목은 서브셋이나 SVG 그림으로 나누는 것이 흔한 분업이다. 기기에 이미 있는 시스템 글꼴(System font)만 쓰면 글꼴 전송량은 0이다. 대신 기기마다 다른 글꼴로 보인다는 것을 받아들인다. 무엇을 고르든, 굵기 한 벌이 곧 무게 한 벌이라는 것을 디자인 단계에서 기억한다. 시안에 굵기를 다섯 단계 쓰면 그만큼 받는다.
밀리는 화면: 레이아웃 이동
읽던 글줄이 갑자기 아래로 내려가고, 누르려던 버튼 자리에 다른 것이 들어오는 일. 늦게 도착한 무언가가 자리를 차지하며 이미 그려진 요소를 밀어낼 때 생긴다. 흔한 원인은 세 가지다. 크기를 모르는 사진(width·height가 없는 img), 늦게 도착해 글줄의 길이를 바꾸는 웹폰트, 나중에 끼워 넣는 띠 배너와 광고.
CLS는 이런 이동마다 점수를 매겨 더한다. 한 번의 이동 점수는 두 비율의 곱이다.
예를 들어 화면 높이의 가운데 절반(위에서 4분의 1부터 4분의 3까지)을 차지하던 글 덩어리가 화면 높이의 4분의 1만큼 내려가면, 이동 전후의 자리를 합친 넓이는 화면의 4분의 3이고(0.75), 거리 비율은 0.25이니 점수는 약 0.19다. 이동이 1초 안쪽 간격으로 이어지면 한 묶음(세션 창, 최대 5초)으로 더하고, CLS는 페이지가 열려 있는 동안 가장 큰 묶음의 합이다. 사용자가 누른 뒤 0.5초 안의 이동은 "예상한 이동"이라 세지 않는다. 메뉴를 열었더니 아래가 밀린 것은 사용자가 원한 일이기 때문이다.
늦게 온 사진과 글꼴이 밀어내는 화면
사진의 이동은 앞 절의 width·height, 또는 CSS의 aspect-ratio로 막는다. 웹폰트의 이동은 두 가지로 줄인다. 하나는 웹폰트를 빨리 받는 것(가볍게, 일찍)이고, 다른 하나는 웹폰트가 오기 전에 쓰는 대체 글꼴의 크기와 줄 높이를 웹폰트에 맞추는 것이다. CSS의 @font-face에는 이를 위한 서술자가 있다.
/* 기기의 기본 고딕을 웹폰트와 같은 크기로 보이게 맞춘 대체 글꼴 */
@font-face {
font-family: "본문 대체";
src: local("Apple SD Gothic Neo"), local("Malgun Gothic"), local("Noto Sans KR");
size-adjust: 97%; /* 글자 너비를 웹폰트에 맞춘다(값은 직접 재서 정한다) */
ascent-override: 92%; /* 줄 높이 안에서 글자가 앉는 자리 */
descent-override: 24%;
}
body { font-family: "책방 본문", "본문 대체", sans-serif; }
위의 퍼센트 값은 예시다. 실제 값은 두 글꼴로 같은 글을 조판해 겹쳐 보며 맞춘다. 늦게 끼워 넣는 공지 띠나 광고는 처음부터 그 높이의 자리를 비워 두거나, 화면 위가 아니라 아래에 겹쳐 띄운다. 스켈레톤(마지막 절)도 실제 콘텐츠와 같은 크기여야 이동을 만들지 않는다.
누르면 바로: INP
페이지가 다 떴어도 "신청하기"를 눌렀는데 반 초 동안 아무 반응이 없으면 손님은 한 번 더 누른다. 그러면 신청이 두 번 들어가기도 한다. INP는 이 반응의 굼뜸을 잰다. 한 번의 상호작용 시간은 세 부분의 합이다. 브라우저의 일꾼(메인 스레드)이 다른 일을 하느라 입력을 받지 못한 입력 지연, 누름을 처리하는 코드가 도는 처리 시간, 처리가 끝난 뒤 다음 화면을 그려 내보내기까지의 표시 지연이다.
디자인이 할 수 있는 일은 생각보다 크다. 누른 순간 버튼이 눌린 모양으로 바뀌고 "신청 중…"이라고 말해 주면, 서버의 응답이 1초 걸리더라도 "다음 화면"은 바로 그려진다. 그러려면 개발에서 무거운 일을 잘게 나누고, 화면을 먼저 바꾼 뒤 나머지를 하게 짜야 한다.
탭에서 다음 화면까지
체감 속도: 스켈레톤과 스피너
숫자로 잰 시간과 사람이 느끼는 기다림은 같지 않다. 오래 쓰여 온 기준으로 Jakob Nielsen이 『Usability Engineering』(1993)에서 정리한 세 경계가 있다. 0.1초 안의 반응은 "즉각"으로 느끼고, 1초 안이면 생각의 흐름이 끊기지 않으며, 10초를 넘으면 주의가 다른 데로 떠난다. 더 앞선 연구들을 바탕으로 한 경험칙이지만, 지금도 기다림을 설계하는 출발점으로 쓰인다.
기다림이 1초를 넘을 수 있는 곳에는 무언가를 보여 준다. 흔한 방법은 둘이다. 빙글 도는 스피너(Spinner)는 "일하는 중"이라는 것만 알린다. 스켈레톤(Skeleton screen)은 콘텐츠가 들어갈 자리를 회색 덩어리로 먼저 그려 "무엇이 어디에 올지"를 보여 준다. 스켈레톤이 더 빠르게 느껴진다는 주장이 널리 퍼져 있지만, 몇몇 실험의 결과는 엇갈리고 기다림의 길이와 디자인에 따라 달라진다. 근거가 더 분명한 것은 두 가지다. 받는 대로 차례로 보여 주면 사람은 그 사이에 읽기 시작할 수 있고, 스켈레톤이 실제 배치와 같은 크기면 콘텐츠가 와도 화면이 밀리지 않는다.
같은 기다림, 다른 화면
체감 속도를 다루는 디자인 요령을 모으면 이렇다. 누르면 0.1초 안에 무언가 바뀌게 한다(눌린 모양, "신청 중"). 1초를 넘을 수 있는 곳에는 진행을 보여 주고, 10초를 넘을 수 있는 곳에는 얼마나 남았는지(퍼센트, 단계)와 그만두는 길을 보여 준다. 받는 대로 차례로 보여 주고, 먼저 올 것을 디자인으로 정한다. 그리고 무엇보다, 실제로 빠르게 만든다. 스켈레톤은 느린 페이지의 화장이 아니라 빠른 페이지의 마지막 손질이다.
서윤의 웹 개편 노트
지하철에서 20초, 그리고 밀려난 버튼
2월 마지막 주 월요일, 책방 E 주인이 출근길 지하철에서 보낸 메시지가 왔다. "사이트를 열었는데 한참 하얗다가 글자만 나오고, 사진은 역 두 개를 지나서야 나왔어요. 신청하기를 누르려는데 사진이 들어오면서 버튼이 내려가서 다른 데를 눌렀고요."
지후가 개발자 도구로 시험판의 첫 화면을 다시 쟀다(가상 자료). 첫 화면 사진은 서윤이 휴대폰 카메라로 찍은 낭독회 원본 JPEG를 그대로 올린 것이라 4.0 MB였다. 글꼴은 TypeBook 11장에서 정한 조각 방식 대신, 지후가 시험 삼아 내려받은 한글 글꼴 통짜 파일 한 굵기(1.87 MB)가 그대로 들어가 있었다. 둘이 첫 화면 6.4 MB의 90%를 넘었다. 이 장의 모형으로 느린 모바일(1.6 Mbps·왕복 150 ms)에서 사진이 그려지기까지 20초가 넘었다. 사진에는 width·height가 없었고, 웹폰트는 대체 글꼴과 글자 너비가 달라 본문이 한 줄 줄어들었다. 모형으로 센 레이아웃 이동은 두 번, 합 0.25로 "나쁨" 경계를 넘었다(0.253).
서윤은 처음에 사진을 첫 화면에서 빼자고 했다. 한도경이 반대했다. "낭독회는 분위기를 보고 오는 거야. 사진은 남기고 무게를 덜어." 서윤은 ImageBook 10장의 표를 다시 꺼냈다. 웹용 사진은 가로 1,600화소 WebP 품질 80, 장당 400 KB 이하. 여기에 휴대폰용으로 480·800화소 두 폭을 더 뽑아 srcset으로 고르게 하면, 기기 픽셀 비율 2인 360px 휴대폰은 800화소(약 75 KB)를 받는다. 그리고 회의 탁자에 막대 하나를 그렸다. 첫 화면에 들어갈 모든 것을 500 KB 안에 나눠 담는 예산표였다.
| 첫 화면(휴대폰) 갈래 | 개편 시험판(가상) | 예산 | 방법 |
|---|---|---|---|
| HTML·CSS | 90 KB | 50 KB | 쓰지 않는 CSS 덜기 |
| 자바스크립트 | 380 KB | 50 KB | 사진 슬라이더 빼기, defer |
| 웹폰트 | 1,868 KB | 200 KB | TypeBook 11장의 조각 방식, 두 굵기, swap |
| 첫 화면 사진 | 4,000 KB | 150 KB | 480·800·1600 WebP 80, srcset |
| 기타(로고 SVG, 아이콘) | 60 KB | 50 KB | "책방 E" 로고는 SVG |
| 합 | 6,398 KB | 500 KB |
예산대로 고친 판을 같은 모형으로 다시 재니, 느린 모바일에서 첫 화면 사진(800화소, fetchpriority high)의 LCP는 약 1.7초, 레이아웃 이동은 사진 자리를 잡고 대체 글꼴 크기를 맞춰 0이 되었다. 지후는 "신청하기"를 누르면 바로 눌린 모양과 "신청 중…"이 보이게 바꾸었다. 행사 목록처럼 서버에서 받아 오는 데 1초가 넘을 수 있는 곳에만, 실제 카드와 같은 크기의 스켈레톤을 두었다.
width·height 필수, 첫 사진은 fetchpriority="high"이고 lazy 금지, 첫 화면 밖 사진은 loading="lazy". 웹폰트: 조각(unicode-range) 두 굵기, font-display: swap, 크기를 맞춘 대체 글꼴. 누르면 0.1초 안에 눌린 모양, 1초 넘는 곳에만 같은 크기의 스켈레톤.핵심 정리
- 내려받는 시간 ≈ 왕복 횟수 × 왕복 지연 + 전송량 ÷ 대역폭. 디자인은 무게(사진, 글꼴, 스크립트)와 단계(무엇을 언제 발견하는가)를 함께 정한다.
- 성능 예산은 페이지가 넘지 말아야 할 무게나 시간이다. 첫 화면에 무엇을 더할지는 예산표 위에서 다른 무엇과 맞바꾸는 결정이 된다.
- Core Web Vitals(web.dev): LCP는 가장 큰 콘텐츠가 그려질 때(≤ 2.5초 좋음, > 4초 나쁨), INP는 상호작용에서 다음 화면까지(≤ 200 ms, > 500 ms), CLS는 예상하지 못한 이동의 점수(≤ 0.1, > 0.25). 판정은 실제 방문의 75번째 백분위로 한다.
- 첫 화면 사진은 화면에 필요한 화소만, 가벼운 형식으로 만들고
srcset으로 고르게 한다. HTML에img로 적고 lazy를 붙이지 않으며fetchpriority="high"로 앞줄에 세운다. 첫 화면 밖 사진에만loading="lazy". - 한글 웹폰트 한 굵기 통짜 파일은 MB 단위다. 고정된 글자는 서브셋·SVG로, 바뀌는 본문은 조각 나누기로, 굵기는 꼭 필요한 만큼만.
- 이동 점수 = 영향 비율 × 거리 비율. 사진에
width·height(또는aspect-ratio), 대체 글꼴 크기 맞추기, 늦게 오는 띠의 자리 잡기로 막는다. - 체감 속도: 0.1초 안에 반응, 1초 넘으면 진행 표시, 받는 대로 차례로 보여 주기. 스켈레톤의 체감 효과는 연구 결과가 엇갈리며, 실제 배치와 같은 크기일 때 이동을 만들지 않는다.
확인 퀴즈
1. web.dev가 설명하는 Core Web Vitals의 "좋음" 기준을 바르게 짝지은 것은?
2. 늦게 도착한 사진이 글 덩어리를 밀어냈다. 움직인 요소들이 이동 전후에 차지한 자리를 합친 넓이가 화면의 50%이고, 가장 멀리 움직인 거리가 화면 긴 변의 25%다. 이 이동의 점수는?
3. 첫 화면 사진의 LCP가 늦다. 폭포 그림을 보니 사진 요청이 CSS를 다 받은 뒤에야 시작된다. 사진 무게는 이미 80 KB로 가볍다. 먼저 의심할 것은?
width·height를 지우면 레이아웃 이동만 늘어난다.4. 스켈레톤 화면에 대한 설명으로 가장 알맞은 것은?