Chapter 12

속도도 디자인이다

출근길 지하철에서 서점 사이트를 연다. 주소를 누르고 나서 화면은 한참 하얗다. 겨우 글자가 나오는가 싶더니 사진 자리가 비어 있고, 그 사이 "신청하기"를 누르려는 순간 사진이 도착해 버튼이 아래로 밀려난다. 엉뚱한 곳을 누른다. 이 모든 일은 코드의 문제처럼 보이지만, 시작은 디자인의 결정이었다. 첫 화면에 어떤 크기의 사진을 몇 장 둘지, 글꼴을 몇 벌 쓸지, 사진이 올 자리를 미리 잡아 둘지. 이 장은 속도를 숫자로 재고, 그 숫자를 움직이는 디자인의 손잡이를 찾는다.

페이지의 무게

웹 페이지는 한 장의 그림이 아니라 여러 파일의 묶음이다. 브라우저는 먼저 HTML을 받고, 그 안에 적힌 CSS, 자바스크립트, 글꼴, 사진을 다시 요청한다. 이 파일들을 통신망으로 받은 양을 합한 것을 흔히 페이지 무게(Page weight)라고 부른다. 압축해서 보낸 실제 전송량으로 센다.

내려받는 시간은 두 가지로 정해진다. 하나는 대역폭(Bandwidth), 곧 1초에 흘려보낼 수 있는 양이다. 다른 하나는 왕복 지연(Round-trip time, RTT), 곧 요청이 서버에 갔다가 첫 응답이 돌아오기까지의 시간이다. 처음 서버에 접속할 때는 주소 찾기, 연결, 암호화 약속에 왕복이 여러 번 들고, 파일을 요청할 때마다 다시 한 번씩 든다.

$$t \;\approx\; n \cdot \mathrm{RTT} \;+\; \frac{8\,B}{\text{대역폭}}$$
\(t\): 내려받는 시간(초), \(n\): 기다려야 하는 왕복 횟수, RTT: 왕복 지연(초), \(B\): 전송량(바이트, 8을 곱해 비트로), 대역폭: 초당 비트 수. 이 책의 시뮬레이터는 이 식을 쓰는 교육용 근사다. 실제 통신은 속도가 들쭉날쭉하고, 처음에는 천천히 시작해 점점 빨라지며(TCP 느린 시작), 여러 파일을 동시에 받는다.

식의 두 항은 서로 다른 디자인 결정과 이어진다. 둘째 항은 무게다. 사진 한 장을 4 MB에서 150 KB로 줄이면 느린 통신에서 수십 초가 1초 안쪽이 된다. 첫째 항은 단계다. HTML을 받아야 CSS를 알고, CSS를 받아야 글꼴을 알고, 자바스크립트가 돌아야 사진을 아는 식으로 발견이 늦어질수록 왕복이 쌓인다. 통신이 느린 곳일수록 왕복 지연도 길어서, 무게가 같아도 단계가 많은 페이지가 더 늦다.

아래 막대는 책방 E 새 사이트의 첫 화면을 이루는 파일의 무게다. 처음 값은 지후가 2월에 올린 시험판(가상 자료)이다. 막대의 경계를 끌어 각 갈래의 무게를 바꾸고, 네 가지 통신에서 다 받는 데 걸리는 시간을 본다.

SIMULATOR

첫 화면 무게 예산

막대의 경계를 끈다
첫 화면 무게—
예산 500 KB 대비—
느린 모바일에서 다 받기—
해볼 것: ① "개편 시험판"에서 가장 긴 갈래를 찾는다. 사진 한 장(4 MB)과 통짜 한글 글꼴(1.87 MB)이 무게의 90%를 넘는다. 느린 모바일에서 다 받는 데 30초가 넘는다. ② 사진 갈래의 오른쪽 경계를 왼쪽으로 끌어 150 KB 근처로 줄인다. 다른 갈래를 아무리 다듬어도 이만큼 줄지 않는다. ③ "예산 안"을 누르고 "지하철 터널 근처" 줄을 본다. 500 KB도 여기서는 10초 넘게 걸린다. 무게만이 아니라 순서, 곧 무엇을 먼저 보여 줄지가 중요한 이유다(다음 절). (통신 조건은 교육용 대표값이다. 느린 모바일 1.6 Mbps·왕복 150 ms는 Lighthouse의 모바일 기본 측정 조건과 비슷하게 잡았다. 시간은 왕복 4번에 전송량 ÷ 대역폭을 더한 단순 계산이고, 시험판의 무게는 가상 자료다.)

이렇게 페이지가 넘지 말아야 할 무게나 시간을 미리 정해 두는 것을 성능 예산(Performance budget)이라고 한다. 예산은 디자인 회의의 언어가 된다. "첫 화면에 사진을 한 장 더 넣고 싶다"는 말은 "사진 갈래의 150 KB를 둘로 나누거나, 다른 갈래에서 빼 오자"는 말이 된다. 자동 재생 영상, 사진이 넘어가는 큰 슬라이더, 장식용 글꼴 한 벌은 모두 예산표 위에서 다른 무엇과 맞바꾸는 물건이다.

세 가지 지표: LCP·INP·CLS

"빠르다"는 느낌은 숫자 하나로 잡히지 않는다. 첫 글자가 빨리 떠도 주인공 사진이 늦게 뜨면 느리고, 다 떴어도 누를 때마다 반응이 굼뜨면 느리고, 빨리 떴어도 화면이 들썩이면 불안하다. 구글이 제안하고 web.dev에서 설명하는 Core Web Vitals(핵심 웹 지표)는 이 경험을 세 지표로 나눈다.

좋음 개선 필요 나쁨 방문의 75번째 백분위가 "좋음" 칸에 들어야 한다
그림 1. Core Web Vitals의 기준값. web.dev의 설명(Web Vitals, 2024년 INP 반영)을 옮겼다. LCP 2.5초·INP 200밀리초·CLS 0.1 이하가 "좋음", LCP 4초·INP 500밀리초·CLS 0.25를 넘으면 "나쁨"이다. 판정은 실제 방문의 75번째 백분위로 한다. 휴대폰과 데스크톱은 따로 본다. 막대의 길이는 이 그림을 위해 고른 눈금이다.

"75번째 백분위"는 방문 100번을 빠른 순서로 줄 세웠을 때 75번째 방문의 값이다. 방문의 4분의 3이 이 값보다 빠르거나 같다. 평균이 아니라 이 값을 쓰는 것은, 느린 기기나 나쁜 통신으로 들어온 사람들까지 꽤 많이 품으라는 뜻이다. 회사 노트북과 와이파이로 잰 한 번의 숫자는 이 값보다 대개 훨씬 좋다.

실험실 숫자와 현장 숫자 개발자 도구의 Lighthouse처럼 정해진 조건에서 한 번 재는 값을 실험실 자료(Lab data), 실제 방문자의 브라우저가 보고한 값을 모은 것을 현장 자료(Field data)라고 한다. Core Web Vitals의 판정은 현장 자료로 한다. 실험실 자료는 원인을 찾고 고친 효과를 비교하는 데 좋지만, 그 숫자가 곧 손님의 경험은 아니다. 특히 INP는 사람이 실제로 눌러야 생기므로 실험실에서는 그대로 잴 수 없다. 이 장의 시뮬레이터도 단순한 실험실 모형이다.

사진: 가장 무거운 짐

대부분의 페이지에서 가장 무거운 것은 사진이고, 첫 화면의 큰 사진은 대개 LCP 요소다. 사진의 무게는 ImageBook 10장에서 쟀다. 같은 사진도 화소 수와 형식, 압축 품질에 따라 수십 배가 달라진다. 웹 화면에 필요한 화소 수는 그 사진이 화면에 그려지는 CSS 폭 × 기기 픽셀 비율이면 충분하다(2장). 360px 폭 휴대폰에서 화면 가득 보이는 사진은 기기 픽셀 비율 2에서 가로 720화소, 3에서 1,080화소면 된다. 4,000화소 원본을 그대로 올리면 쓰지도 않을 화소를 보내는 셈이다.

첫 화면 사진(같은 장면)대략의 무게느린 모바일에서 전송만근거
휴대폰 카메라 원본 JPEG(가로 4,000화소)4,000 KB약 20초가상 자료
가로 1,600화소 JPEG 품질 85365 KB약 1.8초ImageBook 10장의 측정
가로 1,600화소 WebP 품질 80220 KB약 1.1초ImageBook 10장의 측정
가로 800화소 WebP 품질 8075 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)가 쓰는 구분이다.

① 첫 바이트TTFB ② 로드 지연발견·요청까지 ③ 로드 시간사진 파일을 받는 시간 ④ 렌더 지연그려질 때까지 페이지 열기 시작 LCP 서버·통신 lazy, 늦은 발견 사진의 무게·우선순위 CSS·글꼴 대기 디자인이 직접 움직이는 것은 ②와 ③이다: 무엇을, 얼마나 무겁게, 언제 부를지.
그림 2. LCP의 네 구간. ① 서버가 HTML의 첫 바이트를 보내기까지, ② 그 뒤 브라우저가 LCP 사진을 발견해 요청을 시작하기까지, ③ 사진 파일을 다 받기까지, ④ 받은 뒤 화면에 실제로 그려지기까지. 구간의 너비는 예시다.

아래는 책방 E 첫 화면을 여는 과정의 폭포 그림(Waterfall chart)이다. 줄마다 파일 하나이고, 가는 선은 응답을 기다리는 시간, 굵은 막대는 받는 시간이다. 아래 필름 띠는 그 시각의 화면이다. 사진의 크기, 불러오는 방식, 통신을 바꿔 본다.

SIMULATOR

첫 화면 사진과 LCP

LCP(첫 화면 사진)—
첫 글자(FCP)—
사진 요청 시작—
해볼 것: ① "원본 4 MB", 느린 모바일. 글자는 1초 남짓에 뜨지만 사진은 20초를 넘긴다. 필름 띠의 대부분이 빈 사진 자리다. ② "800 WebP"로 바꾼다. 같은 장면, 같은 통신에서 LCP가 2.5초 안쪽으로 들어온다. ③ "lazy(실수)"를 누르고 "사진 요청 시작" 칸을 본다. 요청이 CSS를 받고 레이아웃을 마친 뒤로 밀린다(② 구간). 느린 모바일에서는 그동안에도 통신이 다른 파일로 꽉 차 있어 이 모형의 손해가 작게 나오지만, LTE로 바꾸면 늦게 시작한 만큼 LCP가 그대로 늦어진다. 실제 브라우저에서는 레이아웃이 스크립트에 막히면 더 늦어진다. "fetchpriority=high"는 다른 파일과 대역폭을 나눌 때 사진에 더 많은 몫을 주어, 느린 모바일에서 LCP를 0.7초쯤 당긴다. ④ "터널 근처"로 바꾼다. 가벼운 사진도 왕복 지연이 쌓여 늦다. (교육용 모형: HTML 15 KB, CSS 25 KB, 자바스크립트 50 KB(defer), 웹폰트 조각 160 KB. 첫 접속에 왕복 3번, 파일마다 왕복 1번. 동시에 받는 파일은 대역폭을 우선순위 비율로 나눈다(CSS 4, 글꼴 3, 스크립트 2, 사진 1, high는 4). lazy 사진은 CSS를 받고 0.25초(레이아웃과 위치 판단) 뒤에 요청하며, 글꼴은 CSS를 받은 뒤 발견하고, 글자는 대체 글꼴로 먼저 그린다(swap). 그리는 데 50 ms, 4 MB 사진은 풀어 그리는 데 150 ms를 더 잡았다.)

모형이 보여 주는 순서대로 고치면 된다. 먼저 무게(③): 화면에 필요한 화소만, 가벼운 형식으로. 다음은 발견(②): 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장의 시뮬레이터에서 볼 수 있다. 여기서는 서브셋의 무게가 글자 수에 따라 어떻게 변하는지, 그리고 그것이 첫 화면 예산에서 무엇을 뜻하는지 본다.

SIMULATOR

글자 수와 웹폰트의 무게

이 페이지 글자만 서브셋—
통짜 파일과 비교—
글꼴 예산 200 KB—
해볼 것: ① 처음 값은 서로 다른 한글 약 150자, 첫 화면 정도의 글이다. 페이지 글자만 남긴 서브셋은 통짜 파일의 수십 분의 1이다. ② 글자 수를 끝까지(11,172자) 올린다. 서브셋의 이점이 사라진다. 글이 길고 자주 바뀌는 페이지에 조각 나누기를 쓰는 이유다. ③ 2,350자(옛 완성형 규격 KS X 1001의 한글 수) 근처에 둔다. 흔히 쓰는 "2,350자 서브셋"도 두 벌이면 글꼴 예산을 넘는다. (교육용 근사: 서브셋 무게 = 3 KB + 글자 수 × 0.105 KB. 한 자당 0.105 KB는 TypeBook 11장이 잰 Noto Sans KR 한 굵기 1,868 KB를 담긴 글자 17,648자로 나눈 평균이고, 3 KB는 글자와 상관없이 드는 몫으로 잡았다. 통짜 파일은 그 측정값 그대로다. 굵은 굵기의 무게도 같다고 보았다. 시간은 왕복 1번에 전송량 ÷ 대역폭을 더했다.)

서브셋은 가장 가볍지만 함정이 있다. 나중에 글을 고치다 서브셋에 없는 글자가 들어가면 그 글자만 다른 글꼴로 튀어나온다. 그래서 내용이 바뀌는 본문은 조각 나누기로, 글자가 고정된 로고와 장식 제목은 서브셋이나 SVG 그림으로 나누는 것이 흔한 분업이다. 기기에 이미 있는 시스템 글꼴(System font)만 쓰면 글꼴 전송량은 0이다. 대신 기기마다 다른 글꼴로 보인다는 것을 받아들인다. 무엇을 고르든, 굵기 한 벌이 곧 무게 한 벌이라는 것을 디자인 단계에서 기억한다. 시안에 굵기를 다섯 단계 쓰면 그만큼 받는다.

밀리는 화면: 레이아웃 이동

읽던 글줄이 갑자기 아래로 내려가고, 누르려던 버튼 자리에 다른 것이 들어오는 일. 늦게 도착한 무언가가 자리를 차지하며 이미 그려진 요소를 밀어낼 때 생긴다. 흔한 원인은 세 가지다. 크기를 모르는 사진(width·height가 없는 img), 늦게 도착해 글줄의 길이를 바꾸는 웹폰트, 나중에 끼워 넣는 띠 배너와 광고.

CLS는 이런 이동마다 점수를 매겨 더한다. 한 번의 이동 점수는 두 비율의 곱이다.

$$\text{이동 점수} \;=\; \text{영향 비율} \times \text{거리 비율}$$
영향 비율(impact fraction): 움직인 요소들이 이동 전과 이동 후에 차지한 자리를 합친 넓이가 화면(뷰포트) 넓이에서 차지하는 비율. 거리 비율(distance fraction): 움직인 요소 가운데 가장 멀리 움직인 거리를 화면의 긴 변(가로와 세로 중 큰 쪽)으로 나눈 값. web.dev의 CLS 설명(Cumulative Layout Shift)을 따랐다.

예를 들어 화면 높이의 가운데 절반(위에서 4분의 1부터 4분의 3까지)을 차지하던 글 덩어리가 화면 높이의 4분의 1만큼 내려가면, 이동 전후의 자리를 합친 넓이는 화면의 4분의 3이고(0.75), 거리 비율은 0.25이니 점수는 약 0.19다. 이동이 1초 안쪽 간격으로 이어지면 한 묶음(세션 창, 최대 5초)으로 더하고, CLS는 페이지가 열려 있는 동안 가장 큰 묶음의 합이다. 사용자가 누른 뒤 0.5초 안의 이동은 "예상한 이동"이라 세지 않는다. 메뉴를 열었더니 아래가 밀린 것은 사용자가 원한 일이기 때문이다.

SIMULATOR

늦게 온 사진과 글꼴이 밀어내는 화면

이동 점수—
CLS(지금까지)—
판정—
해볼 것: ① 시각을 0.5초에서 1초로 민다. 0.8초에 웹폰트가 도착해 본문이 여섯 줄에서 다섯 줄로 줄고, 그 아래 버튼과 카드가 한 줄만큼 올라간다. 빨간 점선이 직전 자리다. ② 1.5초로 민다. 1.4초에 사진이 도착해 그 아래 모든 것이 202px 밀려난다. 점수가 한 번에 0.25 가까이 오른다. ③ "사진 자리 미리 잡기"를 켠다. 처음부터 회색 자리가 있어 사진이 와도 아무것도 움직이지 않는다. ④ "대체 글꼴 크기 맞추기"까지 켜면 CLS가 0이 된다. (가상 화면: 뷰포트 360×640 CSS px, 요소의 폭은 328px로 같다고 보았다. 두 이동은 0.6초 간격이라 한 세션 창에 묶여 더해진다.)

사진의 이동은 앞 절의 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초 걸리더라도 "다음 화면"은 바로 그려진다. 그러려면 개발에서 무거운 일을 잘게 나누고, 화면을 먼저 바꾼 뒤 나머지를 하게 짜야 한다.

SIMULATOR

탭에서 다음 화면까지

입력 지연—
다음 화면까지—
판정(INP 기준)—
해볼 것: ① 처음 설정에서 탭은 다른 스크립트의 긴 작업 한가운데 떨어진다. 그 작업이 끝날 때까지 150 ms를 기다리고, 처리 320 ms를 마친 뒤에야 화면이 바뀐다. ② "다른 스크립트"를 끈다. 입력 지연은 사라지지만 여전히 200 ms를 넘는다. ③ "먼저 눌린 모양을 그리고"를 켠다. 처리 코드의 첫 30 ms에 버튼 모양만 바꾸고 일꾼을 잠시 놓아주어, 다음 화면이 그 직후에 그려진다. 나머지 일은 그 뒤에 한다. (교육용 모형: 표시 지연은 24 ms로 고정했고, 다른 스크립트의 긴 작업은 탭 100 ms 전에 시작한다고 두었다. 실제 INP는 한 방문의 여러 상호작용 가운데 가장 느린 축의 값이다.)

체감 속도: 스켈레톤과 스피너

숫자로 잰 시간과 사람이 느끼는 기다림은 같지 않다. 오래 쓰여 온 기준으로 Jakob Nielsen이 『Usability Engineering』(1993)에서 정리한 세 경계가 있다. 0.1초 안의 반응은 "즉각"으로 느끼고, 1초 안이면 생각의 흐름이 끊기지 않으며, 10초를 넘으면 주의가 다른 데로 떠난다. 더 앞선 연구들을 바탕으로 한 경험칙이지만, 지금도 기다림을 설계하는 출발점으로 쓰인다.

기다림이 1초를 넘을 수 있는 곳에는 무언가를 보여 준다. 흔한 방법은 둘이다. 빙글 도는 스피너(Spinner)는 "일하는 중"이라는 것만 알린다. 스켈레톤(Skeleton screen)은 콘텐츠가 들어갈 자리를 회색 덩어리로 먼저 그려 "무엇이 어디에 올지"를 보여 준다. 스켈레톤이 더 빠르게 느껴진다는 주장이 널리 퍼져 있지만, 몇몇 실험의 결과는 엇갈리고 기다림의 길이와 디자인에 따라 달라진다. 근거가 더 분명한 것은 두 가지다. 받는 대로 차례로 보여 주면 사람은 그 사이에 읽기 시작할 수 있고, 스켈레톤이 실제 배치와 같은 크기면 콘텐츠가 와도 화면이 밀리지 않는다.

SIMULATOR

같은 기다림, 다른 화면

기다림—
Nielsen의 경계—
첫 글이 보인 때(차례로)—
해볼 것: ① 3초로 두고 "다시 재생"을 누른다. 세 화면은 정확히 같은 때 내용을 받는다. 빈 화면은 고장 난 것처럼 보이고, 스피너는 일하는 중이라는 것만, 스켈레톤은 무엇이 어디에 올지를 알린다. ② "받는 대로 차례로 보여 주기"를 켠다. 제목과 날짜가 먼저 오고 사진이 나중에 온다. 스켈레톤 화면에서는 먼저 온 것부터 제자리에 채워지고, 아무것도 밀리지 않는다. ③ 0.5초로 줄인다. 이 정도 기다림에서는 스피너나 스켈레톤이 오히려 깜박임처럼 보인다. 아주 짧은 기다림에는 아무것도 보이지 않는 편이 낫다는 실무 의견도 있다. (가상 화면이다. 세 화면 가운데 어느 쪽이 더 빠르게 "느껴지는지"는 이 모형이 재지 않는다. 차례로 보여 줄 때 제목은 기다림의 30%, 목록 글은 60%, 사진은 100% 시점에 온다고 두었다.)

체감 속도를 다루는 디자인 요령을 모으면 이렇다. 누르면 0.1초 안에 무언가 바뀌게 한다(눌린 모양, "신청 중"). 1초를 넘을 수 있는 곳에는 진행을 보여 주고, 10초를 넘을 수 있는 곳에는 얼마나 남았는지(퍼센트, 단계)와 그만두는 길을 보여 준다. 받는 대로 차례로 보여 주고, 먼저 올 것을 디자인으로 정한다. 그리고 무엇보다, 실제로 빠르게 만든다. 스켈레톤은 느린 페이지의 화장이 아니라 빠른 페이지의 마지막 손질이다.

개발자 도구로 느린 통신을 흉내 낸다 크롬, 엣지, 파이어폭스의 개발자 도구에는 네트워크 패널이 있고, 통신 속도를 느린 모바일 수준으로 늦추는 설정(스로틀링)이 있다. 캐시 사용을 끄고 페이지를 새로 고치면 파일마다 무게와 받는 순서가 폭포 그림으로 나온다. 성능(Performance) 패널은 시간에 따른 화면 띠와 레이아웃 이동을 보여 준다. 디자이너가 직접 해 볼 만한 것은 둘이다. 시안의 첫 화면 사진이 실제로 몇 KB로 나가는지, 그리고 느린 통신에서 첫 화면이 어떤 순서로 채워지는지. 이것만 보아도 "사진을 한 장 더"라는 말을 다시 생각하게 된다. 실제 방문자의 현장 자료는 PageSpeed Insights 같은 도구에서 Chrome 사용자 경험 보고서(CrUX)를 바탕으로 볼 수 있다(방문이 충분히 많은 사이트만).
내 페이지의 무게를 재고 예산표를 그린다 자주 가는 가게나 단체의 웹사이트 첫 화면 하나를 고른다. 노트북 브라우저의 개발자 도구에서 네트워크 패널을 열고, 캐시 사용 끄기를 켠 뒤 새로 고친다. 패널 아래쪽에 나오는 전송량 합계를 적고, 종류별(문서, CSS, JS, 글꼴, 이미지)로 걸러 각각의 합을 적는다. 종이에 가로 막대 하나를 그려 다섯 갈래로 나누어 칠하고, 옆에 500 KB 예산 막대를 그린다. 가장 긴 갈래가 무엇인지, 첫 화면에 보이지도 않는 것이 섞여 있는지 표시한다. 마지막으로 통신을 느린 모바일로 늦춰 다시 열고, 첫 화면이 다 채워질 때까지 몇 초인지 휴대폰 초시계로 잰다.

서윤의 웹 개편 노트

CASE 서윤웹 개편 노트 · 12장

지하철에서 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·CSS90 KB50 KB쓰지 않는 CSS 덜기
자바스크립트380 KB50 KB사진 슬라이더 빼기, defer
웹폰트1,868 KB200 KBTypeBook 11장의 조각 방식, 두 굵기, swap
첫 화면 사진4,000 KB150 KB480·800·1600 WebP 80, srcset
기타(로고 SVG, 아이콘)60 KB50 KB"책방 E" 로고는 SVG
합6,398 KB500 KB

예산대로 고친 판을 같은 모형으로 다시 재니, 느린 모바일에서 첫 화면 사진(800화소, fetchpriority high)의 LCP는 약 1.7초, 레이아웃 이동은 사진 자리를 잡고 대체 글꼴 크기를 맞춰 0이 되었다. 지후는 "신청하기"를 누르면 바로 눌린 모양과 "신청 중…"이 보이게 바꾸었다. 행사 목록처럼 서버에서 받아 오는 데 1초가 넘을 수 있는 곳에만, 실제 카드와 같은 크기의 스켈레톤을 두었다.

이 장에서 정한 것첫 화면(휴대폰) 무게 예산 500 KB: HTML·CSS 50, JS 50, 웹폰트 200(TypeBook 11장의 글꼴 예산 그대로), 첫 사진 150, 기타 50. 목표는 Core Web Vitals "좋음"(LCP ≤ 2.5초, INP ≤ 200 ms, CLS ≤ 0.1, 75번째 백분위), 공개 전 점검은 느린 모바일 모형(1.6 Mbps·150 ms)에서 LCP 2.5초 안. 사진: 원본에서 480·800·1600 세 폭 WebP 품질 80(대체 JPEG 85), 장당 400 KB 이하, width·height 필수, 첫 사진은 fetchpriority="high"이고 lazy 금지, 첫 화면 밖 사진은 loading="lazy". 웹폰트: 조각(unicode-range) 두 굵기, font-display: swap, 크기를 맞춘 대체 글꼴. 누르면 0.1초 안에 눌린 모양, 1초 넘는 곳에만 같은 크기의 스켈레톤.
아직 남은 문제11장의 접근성 점검표와 이 장의 예산표, 그리고 앞 장들에서 정한 중단점·토큰·터치 타깃이 한 페이지에서 서로 부딪치지 않는지는 아직 한꺼번에 본 적이 없다. 공개는 3월 첫 주다.
다음 단계13장: 첫 화면, 행사 일정, 신청 폼을 실험실에서 창 폭·대비·터치 타깃·용량으로 한꺼번에 점검한다.

핵심 정리

  1. 내려받는 시간 ≈ 왕복 횟수 × 왕복 지연 + 전송량 ÷ 대역폭. 디자인은 무게(사진, 글꼴, 스크립트)와 단계(무엇을 언제 발견하는가)를 함께 정한다.
  2. 성능 예산은 페이지가 넘지 말아야 할 무게나 시간이다. 첫 화면에 무엇을 더할지는 예산표 위에서 다른 무엇과 맞바꾸는 결정이 된다.
  3. Core Web Vitals(web.dev): LCP는 가장 큰 콘텐츠가 그려질 때(≤ 2.5초 좋음, > 4초 나쁨), INP는 상호작용에서 다음 화면까지(≤ 200 ms, > 500 ms), CLS는 예상하지 못한 이동의 점수(≤ 0.1, > 0.25). 판정은 실제 방문의 75번째 백분위로 한다.
  4. 첫 화면 사진은 화면에 필요한 화소만, 가벼운 형식으로 만들고 srcset으로 고르게 한다. HTML에 img로 적고 lazy를 붙이지 않으며 fetchpriority="high"로 앞줄에 세운다. 첫 화면 밖 사진에만 loading="lazy".
  5. 한글 웹폰트 한 굵기 통짜 파일은 MB 단위다. 고정된 글자는 서브셋·SVG로, 바뀌는 본문은 조각 나누기로, 굵기는 꼭 필요한 만큼만.
  6. 이동 점수 = 영향 비율 × 거리 비율. 사진에 width·height(또는 aspect-ratio), 대체 글꼴 크기 맞추기, 늦게 오는 띠의 자리 잡기로 막는다.
  7. 체감 속도: 0.1초 안에 반응, 1초 넘으면 진행 표시, 받는 대로 차례로 보여 주기. 스켈레톤의 체감 효과는 연구 결과가 엇갈리며, 실제 배치와 같은 크기일 때 이동을 만들지 않는다.

확인 퀴즈

1. web.dev가 설명하는 Core Web Vitals의 "좋음" 기준을 바르게 짝지은 것은?

"좋음"은 LCP 2.5초·INP 200 ms·CLS 0.1 이하이고, 4초·500 ms·0.25를 넘으면 "나쁨"이다. 판정은 실제 방문자에게서 모은 현장 자료의 75번째 백분위로 한다. 실험실에서 한 번 잰 값은 원인을 찾는 데 쓰지만 판정 기준은 아니다.

2. 늦게 도착한 사진이 글 덩어리를 밀어냈다. 움직인 요소들이 이동 전후에 차지한 자리를 합친 넓이가 화면의 50%이고, 가장 멀리 움직인 거리가 화면 긴 변의 25%다. 이 이동의 점수는?

이동 점수 = 영향 비율 × 거리 비율 = 0.5 × 0.25 = 0.125. 이것 하나로 이미 "좋음" 기준 0.1을 넘는다. 두 비율을 더하는 것(0.75)이 아니라 곱한다.

3. 첫 화면 사진의 LCP가 늦다. 폭포 그림을 보니 사진 요청이 CSS를 다 받은 뒤에야 시작된다. 사진 무게는 이미 80 KB로 가볍다. 먼저 의심할 것은?

무게가 가벼운데도 요청 시작이 늦다면 문제는 LCP의 ② 구간, 곧 발견과 요청 지연이다. lazy는 레이아웃이 끝나 보일 위치를 알 때까지 요청을 미루고, 스크립트나 CSS 배경의 사진은 HTML을 읽을 때 발견되지 않는다. width·height를 지우면 레이아웃 이동만 늘어난다.

4. 스켈레톤 화면에 대한 설명으로 가장 알맞은 것은?

스켈레톤은 받는 시간을 줄이지 않는다. 체감 효과에 대한 실험 결과는 조건에 따라 엇갈린다. 분명한 이점은 무엇이 어디에 올지 미리 보여 주고, 같은 크기라면 레이아웃 이동을 만들지 않는다는 것이다. 크기가 다르면 스켈레톤 자체가 이동을 만든다. 아주 짧은 기다림에는 오히려 깜박임이 된다.