Chapter 05

반응형

책방 E의 새 행사 일정 페이지는 휴대폰에서도, 회사 모니터에서도 멀쩡했다. 그런데 지후가 브라우저 창의 오른쪽 끝을 잡고 천천히 줄이자, 폭이 700픽셀을 조금 넘는 곳에서 행사 카드가 화면 끝까지 늘어나 한 줄이 마흔 자를 넘겼고, "신청하기" 버튼은 손바닥보다 길어졌다. 768픽셀이 되는 순간 화면이 갑자기 다른 모습으로 바뀌었다. 세 폭(360·768·1280)으로 그린 시안 어디에도 700픽셀은 없었다. 반응형 디자인은 그 몇 개의 폭이 아니라 그 사이의 모든 폭을 설계하는 일이다.

고정, 유동, 그리고 그 사이

책방 E의 지금 웹사이트는 2019년쯤 만든 고정폭 레이아웃(Fixed-width layout)이다. 페이지 전체의 폭을 960픽셀로 못 박은 디자인이다. 2000년대 후반부터 한동안 웹 디자인의 표준처럼 쓰였다. 당시 흔한 모니터 해상도가 1024×768이었고, 960은 2·3·4·5·6·8·10·12로 나누어떨어져 단을 나누기 좋았다. 화면이 960보다 넓으면 양옆이 비고, 좁으면 넘친다. 종이 포스터처럼 크기를 먼저 정하고 그 안에 그리는 방식이다.

휴대폰이 나오자 고정폭 페이지는 두 가지 방식으로 망가졌다. 2장에서 본 대로, 뷰포트를 알려 주는 메타 태그가 없는 페이지를 휴대폰 브라우저는 폭 980픽셀 안팎의 가상 화면에 그린 뒤 통째로 줄여 보여 준다. 글자가 깨알처럼 작아진다. 메타 태그를 넣으면 이번에는 360픽셀 화면에 960픽셀 페이지가 들어가 가로로 넘친다. 독자는 한 줄을 읽을 때마다 화면을 좌우로 밀어야 한다.

반대쪽 끝은 유동 레이아웃(Fluid layout)이다. 폭을 픽셀이 아니라 화면 폭의 비율(%)이나 fr로 적어, 화면이 넓어지면 함께 넓어지고 좁아지면 함께 좁아진다. 넘치지는 않지만 끝까지 밀면 다른 문제가 생긴다. 좁은 화면에서는 옆 칸까지 같은 비율로 줄어 글 칸이 손가락 폭만 해지고, 넓은 모니터에서는 한 줄이 백 자를 넘는다. 그래서 실무에서는 유동 레이아웃에 최대 폭(max-width)을 함께 둔다. 4장에서 정한 1,200픽셀이 그 값이다. 그보다 넓은 화면에서는 내용이 가운데에 멈추고 양옆 여백만 늘어난다.

360px 768px 1440px 고정 960 유동 유동 + 최대 폭 600px 넘침 192px 넘침 양옆이 빈다 옆 칸이 비좁다 그런대로 글줄이 너무 길다 여전히 비좁다 그런대로 가운데에 멈춘다
그림 1. 세 가지 폭 전략. 같은 페이지를 세 화면 폭(360·768·1440픽셀)에서 본 모습을 10분의 1로 줄여 그렸다. 고정폭(위)은 좁은 화면에서 붉은 점선만큼 넘친다. 유동(가운데)은 넘치지 않지만 좁은 화면에서는 옆 칸까지 함께 쪼그라들고 넓은 화면에서는 글줄이 끝없이 길어진다. 최대 폭을 둔 유동(아래)은 넓은 화면 문제를 푼다. 좁은 화면 문제는 아직 남아 있다.

아래는 책방 E 행사 일정 페이지의 가상 화면이다. 맨 위 눈금자를 끌어 화면 폭을 320에서 1,600픽셀까지 바꾸고, 세 전략을 번갈아 본다. 폭이 넓을 때는 가상 화면을 줄여서 그린다. "viewport 메타 태그 없이"를 켜면 메타 태그가 없던 시절의 휴대폰 화면이 된다.

SIMULATOR

고정폭·유동·최대 폭

좌우로 끌어 화면 폭을 바꾼다
화면 폭—
가로로 넘친 폭—
소개 글 한 줄 글자 수—
본문 17px이 보이는 크기—
해볼 것: ① "고정 960"에서 폭을 360으로 줄인다. 600픽셀이 화면 밖으로 넘치고, 아래에 가로 스크롤 막대가 생긴다. ② "viewport 메타 태그 없이"를 켠다. 넘침은 사라지지만 페이지 전체가 980픽셀 기준으로 줄어 17픽셀 본문이 6픽셀 남짓으로 보인다. 끄고 ③ "유동"으로 바꾼 뒤 폭을 1,600까지 넓힌다. 소개 글이 한 줄 100자를 넘는다. 다시 360으로 줄이면 옆 칸(달력)까지 30%로 줄어 목록이 비좁다. ④ "유동 + 최대 폭"(콘텐츠 1,200px, 글 28em)은 넓은 쪽 문제만 푼다. (가상 화면은 이 책의 단순 모형이다. 한 줄 글자 수는 한 글자를 평균 0.76em으로 친 어림으로, TypeBook 6장의 "28em에 약 37자"와 같은 비율이다.)

세 전략 가운데 어느 것도 360픽셀과 1,600픽셀에서 동시에 좋지는 않았다. 고정폭은 한 폭만 책임지고, 유동은 모든 폭을 같은 모양으로 늘인다. 필요한 것은 폭에 따라 모양 자체를 바꾸는 장치다. 웹 디자이너 이선 마콧(Ethan Marcotte)은 2010년 A List Apart에 실은 글 「Responsive Web Design」에서 유동 그리드, 유연한 이미지, 미디어 쿼리 세 가지를 묶어 반응형 웹 디자인(Responsive web design)이라고 불렀다. 화면 폭에 반응해 배치가 바뀌는 페이지라는 뜻이다. 이 장은 그 세 재료가 지금 어떻게 쓰이는지, 그리고 그 뒤에 더해진 재료(clamp(), 컨테이너 쿼리, srcset)를 차례로 본다.

확대를 막지 않는다 휴대폰에서 레이아웃이 흔들리는 것을 막으려고 viewport 메타 태그에 user-scalable=no나 maximum-scale=1을 넣는 일이 있다. 손가락 두 개로 화면을 키우는 확대를 막는 설정이다. 글자를 키워야 읽을 수 있는 사람에게 이 설정은 페이지를 닫으라는 말과 같다. 웹 콘텐츠 접근성 지침 WCAG 2.2의 성공 기준 1.4.4(글자 크기 조정)는 글자를 200%까지 키워도 내용과 기능을 잃지 않을 것을 요구한다. 메타 태그는 width=device-width, initial-scale=1까지만 쓴다.

중단점은 콘텐츠가 정한다

폭에 따라 모양을 바꾸는 지점을 중단점(Breakpoint)이라고 한다. 중단점은 CSS의 미디어 쿼리(Media query)로 적는다. "화면 폭이 얼마 이상이면 이 규칙을 쓴다"는 조건문이다.

/* 기본: 행사 카드를 한 줄에 하나씩 쌓는다 */
.events { display: grid; gap: 1rem; }

/* 화면 폭이 35em 이상이면 날짜·글·버튼을 한 행으로 */
@media (min-width: 35em) {
  .event { grid-template-columns: 8em 1fr 9.5em; }
}

중단점을 어디에 둘까. 오랫동안 쓰인 답은 기기 이름이었다. 아이폰의 320, 아이패드 세로의 768, 아이패드 가로의 1024. 지금도 많은 템플릿과 디자인 도구가 "모바일·태블릿·데스크톱" 세 칸에 이 숫자를 넣어 둔다. 문제는 2장에서 본 대로 실제 화면 폭이 그 몇 개의 숫자에 모여 있지 않다는 것이다. 휴대폰만 해도 폭이 제각각이고, 태블릿을 가로로 돌리고 노트북 창을 반으로 나누고 글자 크기를 키우면 그 사이의 모든 폭이 나온다. 기기 이름의 중단점은 그 기기들의 폭에서만 맞고, 사이에서는 운에 맡긴다.

그래서 지금 널리 쓰는 요령은 순서를 뒤집는 것이다. 가장 좁은 화면에서 시작해 창을 천천히 넓혀 가다가, 콘텐츠가 어색해지는 곳에 중단점을 둔다. 기준은 기기가 아니라 내용이다. 글줄이 너무 길어지는가, 글 칸이 너무 좁아져 제목이 세 줄로 꺾이는가, 버튼이 쓸데없이 길어지는가. 그런 곳이 콘텐츠가 "모양을 바꿔 달라"고 말하는 폭이다.

아래 시뮬레이터에서 행사 일정 페이지는 세 가지 모양을 가진다. 좁을 때는 카드를 한 줄에 하나씩 쌓고(1단), 넓어지면 날짜·글·버튼을 한 행에 놓고(행), 더 넓어지면 옆에 이달의 달력 칸을 붙인다(행 + 옆 칸). 눈금자 아래의 띠는 320~1,600픽셀의 모든 폭에서 그 폭의 모양이 깨지는지(붉은색) 괜찮은지(초록색)를 미리 계산해 칠한 것이다. "직접 끌기"를 고르면 눈금자의 삼각형 중단점 두 개를 끌어 옮길 수 있다.

SIMULATOR

깨지는 곳을 찾아 중단점 두기

화면을 끌어 폭을, 삼각형을 끌어 중단점을 옮긴다
지금 폭 · 모양—
이 폭의 판정—
깨지는 폭(320~1,600 중)—
중단점—
해볼 것: ① "기기 이름"에서 띠를 본다. 580~768 사이와 910~1,024 사이가 붉다. 화면을 700으로 끌면 카드의 설명이 한 줄 40자를 넘는다. 1단을 너무 오래 끌고 간 것이다. ② "콘텐츠"로 바꾼다. 띠가 모두 초록이 된다. 35em(560px)은 "행이 처음 가능해지는 폭"과 "1단이 깨지기 시작하는 폭" 사이에 있다. ③ "글자 크기 150%"를 켠다. 글자와 카드 안쪽 간격이 1.5배가 되자 기기 이름 중단점에서는 768 바로 위와 1,024 위쪽에 새 붉은 구간이 생긴다. em으로 적은 콘텐츠 중단점은 840·1,344픽셀로 함께 밀려 여전히 초록이다. ④ "직접 끌기"로 두 삼각형을 옮겨 띠를 모두 초록으로 만들어 본다. 들어갈 수 있는 구간이 생각보다 좁다. (판정 기준: 설명 글 한 줄 40자 초과, 또는 행 모양에서 글 칸이 12자 미만이면 깨짐. 한 글자를 0.76em, 날짜 칸 8em·버튼 칸 9.5em, 카드 안쪽 여백과 칸 사이 1rem, 목록과 옆 칸 사이 24px, 화면 가장자리 16px, 옆 칸은 280px과 콘텐츠 폭의 30% 중 큰 쪽, 최대 폭 1,200px로 놓은 가상 모형이다. 직접 끈 중단점은 px로 적은 셈이라 글자 크기를 따라 움직이지 않는다.)

기기 이름의 중단점은 틀린 숫자라서가 아니라 질문이 틀려서 깨진다. "어떤 기기가 있나"를 물으면 768이 나오지만, "이 카드는 몇 픽셀부터 한 행에 들어가나"를 물으면 이 모형에서는 560 안팎이 나온다. 그리고 그 답은 카드에 무엇이 들어 있는지에 달려 있다. 날짜 칸을 넓히거나 버튼 문구를 바꾸면 중단점도 움직인다. 중단점은 레이아웃의 설정값이 아니라 콘텐츠의 성질이다.

중단점을 em으로 적은 까닭도 여기서 나온다. 미디어 쿼리 안의 em은 부모 요소가 아니라 브라우저의 기본 글자 크기(대개 16px)를 기준으로 한다(Media Queries Level 4). 사용자가 기본 글자 크기를 키우면 35em은 560픽셀에서 840픽셀로 따라 커진다. 글자가 커지면 같은 카드가 한 행에 들어가는 데 더 넓은 폭이 필요하니, 중단점이 함께 움직이는 쪽이 맞다. 픽셀로 적은 768은 그대로 남아, 글자를 키운 사람에게만 깨진 화면을 보여 준다. 다만 휴대폰 운영체제의 글자 크기 설정이 웹 페이지에 어떻게 전해지는지는 운영체제와 브라우저마다 다르므로 실제 기기에서 따로 확인한다.

중단점 이름은 기기가 아니라 순서로 디자인 시스템에서 중단점에 mobile, tablet, desktop 같은 이름을 붙이면 머지않아 "태블릿인데 왜 모바일 레이아웃이냐"는 질문을 받는다. 폴더형 휴대폰을 펼치면 태블릿만 해지고, 노트북 창을 반으로 나누면 태블릿보다 좁아진다. 이름은 bp-1, bp-2나 narrow, wide처럼 순서와 성질만 말하게 짓고, 문서에는 "그 폭에서 무엇이 바뀌는지(카드가 한 행이 된다)"를 적는다. 그래야 콘텐츠가 바뀌어 중단점을 옮길 때 이름이 거짓말이 되지 않는다.

모바일 우선: 작은 화면부터 쌓기

미디어 쿼리는 두 방향으로 쓸 수 있다. 넓은 화면의 레이아웃을 기본으로 두고 max-width로 좁은 화면에서 하나씩 덮어쓰는 방식과, 좁은 화면의 레이아웃을 기본으로 두고 min-width로 넓은 화면에서 하나씩 더하는 방식이다. 뒤의 방식을 모바일 우선(Mobile first)이라고 부른다. 디자이너 루크 로블르스키(Luke Wroblewski)가 2011년 책 『Mobile First』에서 이름 붙여 널리 알렸다. 그가 강조한 것은 CSS의 순서보다 생각의 순서였다. 작은 화면에서 먼저 설계하면 무엇이 정말 필요한지부터 정하게 된다는 것이다.

모바일 우선(min-width): 더해 간다 기본: 1단 쌓기 min-width: 35em → 행 min-width: 56em → 옆 칸 넓은 화면 우선(max-width): 지워 간다 기본: 행 + 옆 칸 max-width: 55.99em → 옆 칸 지우기 max-width: 34.99em → 행 지우기 320 560 896 1280 1600 화면 폭(px, 기본 글자 크기 16px일 때)
그림 2. 같은 세 모양을 두 방향으로 적기. 위는 모바일 우선이다. 기본 규칙이 모든 폭에 깔리고, min-width 규칙이 넓은 쪽에 한 겹씩 더해진다. 아래는 넓은 화면을 기본으로 두고 max-width로 좁은 쪽을 덮어쓰는 방식이다. 결과는 같아도, 아래 방식에서는 휴대폰이 넓은 화면의 규칙을 다 읽은 뒤 그것을 하나씩 지워야 한다.

모바일 우선이 CSS에서 유리한 까닭은 단순하다. 1단 쌓기는 블록 요소의 기본 동작이라 거의 아무것도 적지 않아도 된다. 넓은 화면의 복잡한 규칙(단 나누기, 옆 칸, 행 배치)은 그것이 필요한 폭에서만 더한다. 반대로 넓은 화면을 기본으로 두면 좁은 화면에서 그 규칙을 하나하나 되돌려야 하고, 되돌리는 것을 잊은 규칙이 휴대폰에 남는다. 그리고 지금 대부분의 방문은 휴대폰에서 온다. 2장에서 본 책방 E의 방문 기록(가상)도 그랬다. 가장 많은 손님이 보는 화면을 "덮어쓴 결과"가 아니라 "기본"으로 두는 편이 안전하다.

모바일 우선은 "휴대폰 화면만 먼저 예쁘게 그린다"는 뜻이 아니다. 시안 순서로는 오히려 내용의 순위표부터다. CompositionBook 12장에서 서윤이 웹 첫 화면을 짤 때 그랬듯, 무엇이 먼저 읽혀야 하는지를 정한 순서가 1단에서는 그대로 위에서 아래로의 순서가 된다. 넓은 화면은 그 순서를 옆으로 펼치는 일이다. 순서가 정해져 있으면 넓은 화면에서 옆 칸으로 빠지는 것(이달의 달력)과 본문에 남는 것(행사 목록)도 쉽게 갈린다.

종이 띠로 중단점 찾기 A4 종이를 세로로 길게 잘라 폭 6 cm 띠를 만든다. 띠 위에 행사 카드 하나(날짜, 제목, 두 줄 설명, "신청하기" 버튼)를 연필로 그린다. 같은 카드를 폭 10 cm, 14 cm, 20 cm 띠에도 그리되, 설명 글은 띠 폭을 다 채워 쓴다. 그다음 각 띠를 보며 "이 폭에서는 날짜를 옆으로 빼는 편이 낫다", "이 폭에서는 줄이 너무 길다"를 적는다. 모양을 바꾸고 싶어진 첫 폭이 그 카드의 중단점이다. 자로 재서 cm를 적고, 글자 크기(손글씨 한 글자의 폭)로 나누어 "몇 글자 폭"인지도 적어 둔다. 그 숫자가 em으로 적은 중단점이다. 마지막으로 자기 브라우저 창의 오른쪽 끝을 잡고 천천히 줄이며, 즐겨 찾는 웹사이트 하나가 어디에서 모양을 바꾸는지 찾아본다. 그 폭이 기기 이름의 숫자(768, 1024)와 같은지도 본다.

clamp(): 계단 대신 비탈

중단점은 모양을 바꾸는 장치다. 그런데 모든 것이 모양을 바꿔야 하는 것은 아니다. 제목 크기, 구역 사이의 간격처럼 "조금 더 크게, 조금 더 넓게"만 바뀌면 되는 값도 있다. 이런 값을 중단점마다 하나씩 정하면 계단이 된다. 559픽셀에서 30픽셀이던 제목이 560픽셀에서 갑자기 35픽셀로 뛴다. 창을 끄는 사람만 보는 어색함이지만, 계단이 많을수록 사이의 폭에서 비례가 어긋나는 곳도 많아진다.

CSS의 clamp(최솟값, 선호값, 최댓값)은 이 계단을 비탈로 바꾼다. 선호값을 화면 폭에 비례하는 식으로 적고, 그 값이 최솟값과 최댓값 사이를 벗어나지 않게 묶는다. 좁은 화면 폭 \(W_1\)에서 \(s_1\), 넓은 화면 폭 \(W_2\)에서 \(s_2\)가 되게 하려면 두 점을 잇는 직선을 쓴다.

$$s(W) = \mathrm{clamp}\!\left(s_1,\; s_1 + (s_2 - s_1)\,\frac{W - W_1}{W_2 - W_1},\; s_2\right)$$
\(W\): 화면(뷰포트) 폭(CSS 픽셀). 가운데 식을 "고정 부분 + 기울기 × 화면 폭"으로 풀면 기울기는 \(100\,(s_2 - s_1)/(W_2 - W_1)\) vw, 고정 부분은 \(s_1 - W_1 (s_2 - s_1)/(W_2 - W_1)\)이다. 고정 부분과 양 끝은 rem으로 적는다(1rem = 사용자의 기본 글자 크기, 대개 16px).

TypeBook 11장에서 서윤은 이 식으로 본문 크기(17 → 18픽셀)를 이었다. 본문은 폭이 1픽셀밖에 변하지 않는 얌전한 경우다. 제목과 간격은 폭이 훨씬 크다. 아래 그래프의 두 손잡이를 끌어 \((W_1, s_1)\)과 \((W_2, s_2)\)를 바꾸면 그 식이 CSS로 적힌다. 그래프 위에서 마우스를 움직이면(휴대폰에서는 누르면) 그 폭에서의 크기로 "행사 일정"을 그려 보인다.

SIMULATOR

clamp()로 잇는 제목 크기와 간격

두 손잡이를 끈다
CSS—
폭 360 · 768 · 1,280에서—
최댓값 ÷ 최솟값—
해볼 것: ① 처음 상태는 360픽셀에서 30, 1,280픽셀에서 40픽셀인 페이지 제목이다. 회색 계단(중단점 35em·56em에서만 바뀌는 값)과 비교하면, 계단은 560과 896에서 뛰고 그 사이에서는 평평하다. ② "구역 사이 간격"으로 바꾼다. 간격은 32 → 64픽셀로 두 배가 된다. 휴대폰에서 64픽셀 간격은 한 화면의 6분의 1을 비우는 셈이라, 간격도 화면에 비례해 줄어드는 편이 낫다. ③ "기본 글자 크기 24px"를 켠다. 식 전체가 1.5배로 올라간다. 오른쪽 손잡이를 끌어 최댓값을 최솟값의 2.5배 넘게 키우면 읽음값이 붉어진다. (그래프의 크기는 확대 전 CSS 픽셀. 2.5배 기준은 TypeBook 11장에서 소개한 확대 접근성 계산을 따른다.)

제목 크기의 비탈에는 지켜야 할 선이 있다. 선호값을 vw만으로 적으면(font-size: 4vw) 브라우저 확대가 듣지 않는다. 확대하면 CSS 픽셀로 잰 화면 폭이 줄어 vw 값도 함께 줄기 때문이다. 고정 부분을 rem으로 섞고, 최댓값을 최솟값의 2.5배 안쪽으로 두면 200% 확대에서도 글자가 충분히 커진다(TypeBook 11장). 간격은 글자보다 너그럽다. 다만 간격이 화면 폭만 따라 커지고 글자 크기를 따르지 않으면, 글자를 키운 사람의 화면에서는 글자에 비해 간격이 좁아진다. 간격의 비탈도 rem을 섞어 적는다.

vw는 무엇의 1%인가 1vw는 뷰포트 폭의 1%다. 휴대폰 브라우저에서는 주소창이 나타났다 사라지면 뷰포트의 높이가 바뀌므로, 높이 단위 vh는 스크롤할 때 값이 흔들린다. 그래서 CSS에는 주소창이 가장 클 때 기준인 svh, 가장 작을 때 기준인 lvh, 지금 기준인 dvh가 더해졌다(CSS Values and Units Level 4). 폭 단위 vw도 데스크톱에서는 세로 스크롤 막대의 폭까지 포함해 재므로, 100vw로 폭을 정한 요소는 스크롤 막대만큼 넘쳐 가로 스크롤을 만든다. 화면 폭에 맞추려면 100%를, 글자와 간격의 비탈에는 vw를 쓰는 식으로 나누어 쓴다.

글줄 길이를 지키는 상한

유동 레이아웃이 넓은 화면에서 가장 먼저 망가뜨리는 것은 글줄 길이다. TypeBook 6장에서 본 것처럼 한 줄이 너무 길면 눈이 다음 줄의 시작으로 돌아가다 길을 잃는다. 웹 콘텐츠 접근성 지침의 성공 기준 1.4.8(시각적 표현, AAA 등급)은 글 덩어리의 폭을 한 줄 80자, 한중일 문자는 40자 이하로 둘 수 있게 하라고 적는다. 서윤은 지난여름 서점 소식 글의 최대 글줄 폭을 28em, 한 줄 37자 안팎으로 정했다.

이 상한을 무엇으로 적느냐가 반응형에서 중요하다. 픽셀로 max-width: 504px라고 적으면 기본 글자 크기에서는 28em과 같다. 그런데 글자를 키운 사람에게는 504픽셀이 그대로라 한 줄 글자 수가 줄어든다. 글자가 두 배가 되면 한 줄이 열여덟 자로 꺾인다. max-width: 28em으로 적으면 글자와 함께 폭도 커져 한 줄 글자 수가 그대로 남는다. 물론 화면 폭이 그보다 좁으면 화면 폭이 먼저 상한이 된다.

SIMULATOR

글줄 길이 상한: px로, em으로

좌우로 끌어 화면 폭을 바꾼다
본문 크기 · 글줄 폭—
한 줄 글자 수(어림)—
판정—
해볼 것: ① "상한 없음"에서 폭을 1,600까지 넓힌다. 한 줄이 80자를 넘는다. ② "28em"으로 바꾼다. 한 줄 30자대 후반에 멈추고 남는 폭은 오른쪽 여백이 된다. ③ 기본 글자 크기를 32px(두 배)로 올린다. "28em"은 글줄 폭이 두 배로 커져 한 줄 글자 수가 거의 그대로다. "504px"로 바꾸면 같은 상황에서 한 줄이 열여덟 자 남짓으로 줄어든다. 넓은 화면에서 글을 키운 독자에게 짧은 줄은 잦은 줄바꿈이 된다. (본문은 기본 글자 크기의 1.125배, 곧 기본 설정에서 넓은 화면의 18px이고 줄 높이는 1.7이다. 한 줄 글자 수는 한 글자를 0.76em으로 친 어림으로 TypeBook 6장의 "28em ≈ 37자"와 같은 비율이다. 그림의 줄나눔은 화면 글꼴로 실제로 나눈 것이라 한두 자 다를 수 있다. 화면 가장자리 여백 16px.)

상한은 글 덩어리에만 둔다. 페이지 전체를 28em으로 묶으면 넓은 화면에서 사진과 표, 달력까지 좁은 기둥에 갇힌다. 반응형 페이지에는 보통 폭의 층이 셋 있다. 화면 폭, 콘텐츠의 최대 폭(책방 E에서는 1,200픽셀), 그리고 글 덩어리의 최대 글줄 폭(28em). 사진과 그리드는 둘째 층까지, 글은 셋째 층까지 늘어난다. 이 셋을 구분해 두면 "넓은 화면을 어떻게 채울까"라는 질문이 "무엇을 넓게, 무엇을 좁게 둘까"로 바뀐다.

컨테이너 쿼리: 놓인 자리를 보고 바뀐다

미디어 쿼리는 화면 전체의 폭만 본다. 그런데 웹 페이지의 부품은 화면 폭이 아니라 자기가 놓인 칸의 폭 안에서 산다. 같은 "신간 카드"가 넓은 본문 칸에도, 좁은 옆 칸에도 들어간다. 넓은 화면에서 카드를 가로형(표지 왼쪽, 글 오른쪽)으로 바꾸는 규칙을 미디어 쿼리로 적으면, 넓은 화면의 좁은 옆 칸에 들어간 카드도 가로형이 되어 글 칸이 손가락 하나만 해진다.

컨테이너 쿼리(Container query)는 질문의 대상을 화면에서 부품을 담은 칸으로 바꾼다. 칸에 container-type: inline-size를 적어 두면, 그 안의 부품은 @container (min-width: 25em)처럼 "나를 담은 칸이 25em 이상이면"이라는 조건을 쓸 수 있다. CSS Containment Module Level 3에 정의되어 있고, 2023년 초부터 주요 브라우저가 모두 지원한다.

.book-slot { container-type: inline-size; }

/* 기본은 세로형: 표지 위, 글 아래 */
.book-card { display: grid; gap: 0.75rem; }

/* 담은 칸이 25em 이상이면 가로형 */
@container (min-width: 25em) {
  .book-card { grid-template-columns: 9em 1fr; }
}

아래 시뮬레이터에는 신간 카드가 두 자리에 놓여 있다. 본문 칸의 "이달의 신간"과, 넓은 화면에서는 오른쪽 옆 칸에 붙고 좁은 화면에서는 본문 아래로 내려오는 "주인의 추천"이다. 미디어 쿼리 판은 "화면 폭 56em 이상이면 가로형", 컨테이너 쿼리 판은 "담은 칸이 25em 이상이면 가로형"이다.

SIMULATOR

같은 카드, 다른 자리

좌우로 끌어 화면 폭을 바꾼다
본문 칸의 카드—
추천 칸의 카드—
판정—
해볼 것: ① "미디어 쿼리"에서 폭을 1,200으로 둔다. 옆 칸의 추천 카드도 가로형이 되어 글 칸이 열 자 남짓으로 쪼그라든다. ② 폭을 850으로 줄인다. 이번에는 추천 카드가 본문 아래 넓은 자리로 내려왔는데 화면 폭이 56em보다 좁으니 세로형이다. 표지 띠가 카드 폭만큼 커져 높이가 400픽셀을 넘는다. ③ "컨테이너 쿼리"로 바꾸고 폭을 끝에서 끝까지 끈다. 두 카드가 각자 자기 칸의 폭을 보고 모양을 고른다. 같은 폭에서 두 카드의 모양이 다를 수 있다. (카드 기준: 가로형은 글 칸이 12자 이상일 때, 세로형은 카드 폭이 34em(578px) 이하라 표지 띠(카드 폭의 절반 높이)가 290픽셀을 넘지 않을 때 괜찮다. 가로형의 글 칸은 28em에서 멈춘다. 한 글자 0.76em, 본문 17px, 표지 폭 9em으로 놓은 가상 모형이다. 25em은 본문 크기 기준 425px다.)

컨테이너 쿼리가 생기면서 반응형의 일이 둘로 나뉘었다. 페이지 전체의 뼈대(몇 단인가, 옆 칸이 있는가)는 여전히 미디어 쿼리가 화면 폭을 보고 정한다. 부품의 모양(카드가 가로형인가 세로형인가)은 부품이 스스로 자기 칸을 보고 정한다. 부품을 만드는 사람은 "이 카드가 어느 페이지 어느 칸에 들어갈지" 몰라도 되고, 페이지를 짜는 사람은 "이 칸에 들어갈 카드가 어떤 규칙을 가졌는지" 몰라도 된다. 7장에서 다룰 컴포넌트가 이 성질 덕분에 어디에나 옮겨 놓을 수 있게 된다.

반응형 이미지: srcset과 sizes

이미지에도 폭의 문제가 있다. 서윤이 ImageBook 10장에서 정한 웹용 사진은 가로 1,600픽셀이다. 넓은 화면의 큰 사진 자리에는 맞지만, 휴대폰의 작은 썸네일 칸에 1,600픽셀 파일을 보내면 받은 픽셀 대부분을 버린다. 반대로 작은 파일 하나만 보내면 큰 화면과 고해상도 화면에서 흐릿하다. 마콧이 말한 "유연한 이미지"의 첫 단계는 max-width: 100%로 이미지를 칸보다 커지지 않게 하는 것이었다. 지금은 여기에 더해 칸에 맞는 크기의 파일을 고르게 한다.

<img src="reading-1600.webp"
     srcset="reading-480.webp 480w, reading-800.webp 800w,
             reading-1200.webp 1200w, reading-1600.webp 1600w"
     sizes="(min-width: 56em) 384px,
            (min-width: 35em) calc(50vw - 28px),
            calc(100vw - 32px)"
     width="1600" height="1067" alt="…">

srcset은 같은 사진을 크기별로 내보낸 파일 목록이고, 뒤의 480w는 그 파일의 가로 픽셀 수다. sizes는 "이 사진이 화면에서 차지할 폭"을 미디어 조건별로 알려 준다. 위의 예는 넓은 화면에서 사진이 세 칸 그리드의 한 칸(384픽셀), 중간 폭에서 두 칸 중 하나, 좁은 화면에서 화면 폭에서 양옆 여백을 뺀 폭이라는 뜻이다. 브라우저는 페이지의 CSS를 다 읽기 전에 이미지를 받기 시작하므로, 사진이 얼마나 크게 놓일지를 이렇게 미리 귀띔해 주어야 한다.

① sizes 읽기 화면 360px → 100vw − 32px = 칸 328px ② DPR 곱하기 칸 328px × DPR 3 = 984픽셀 필요 ③ 후보 고르기 srcset에서 984 이상인 가장 작은 것 ④ 내려받기 1200w 약 124 KB (가상 용량) srcset 후보 480w 800w 1200w 1600w 480w·800w는 984보다 작아 흐리고, 1600w는 필요보다 2.6배 많은 픽셀이다.
그림 3. 브라우저가 파일을 고르는 길. sizes에서 지금 화면 폭에 맞는 조건을 찾아 사진이 놓일 폭(CSS 픽셀)을 얻고, 거기에 기기 픽셀 비율(DPR)을 곱해 필요한 실제 픽셀 수를 낸 뒤, 목록에서 그 이상인 파일 하나를 고른다. 마지막 단계의 정확한 방식은 명세가 브라우저에 맡겨 두어서, 이미 받아 둔 큰 파일이 있거나 통신이 느리면 다른 파일을 고를 수도 있다.

아래 시뮬레이터는 행사 사진 아홉 장이 들어가는 사진첩 페이지다. 화면 폭과 기기 픽셀 비율을 바꾸며 세 가지 적기 방식을 비교한다. sizes를 빠뜨리면 브라우저는 사진이 화면 폭 전체(100vw)를 차지한다고 가정한다.

SIMULATOR

srcset·sizes: 어느 파일이 내려오나

좌우로 끌어 화면 폭을 바꾼다
사진 한 장의 칸 폭—
필요한 픽셀 → 고른 파일—
사진 9장 합계—
받은 픽셀 중 버린 비율—
해볼 것: ① "src 하나"에 DPR 3, 폭 360으로 둔다. 칸은 328픽셀, 필요한 픽셀은 984인데 1,600픽셀 파일이 내려와 받은 픽셀의 62%를 버린다(가로세로 모두 줄이므로 넓이로 센다). ② "srcset + sizes"로 바꾼다. 1,200w 파일이 골라져 버리는 비율이 33%로 준다. DPR 1로 바꾸면 480w로 내려간다. ③ DPR 2로 바꾸고 폭을 1,280으로 넓힌 뒤 "srcset만"을 고른다. 사진은 세 칸 그리드의 한 칸(384픽셀)인데 브라우저는 화면 폭 전체를 가정해 가장 큰 파일을 고른다. "srcset + sizes"의 800w와 비교하면 아홉 장에서 1 MB 넘게 차이 난다. (용량은 ImageBook 10장에서 잰 "가로 1,600 WebP 품질 80 ≈ 220 KB"를 픽셀 수에 비례해 줄인 가상 값이다. 파일 고르기는 "필요한 픽셀 이상인 가장 작은 파일" 규칙으로 계산했다. 실제 브라우저는 캐시와 통신 상태에 따라 달리 고를 수 있다.)

사진의 내용을 화면에 따라 바꾸고 싶을 때도 있다. 넓은 화면에서는 서점 전경을, 휴대폰에서는 같은 사진의 가운데 낭독자만 잘라 보이고 싶다면 <picture> 요소에 <source media="…">를 여러 개 두어 화면 조건마다 다른 사진을 지정한다. 이것을 아트 디렉션(Art direction)이라고 부른다. 같은 사진의 크기만 고르는 srcset과 달리, 어떻게 자를지를 디자이너가 정하는 일이다. 어느 경우든 width와 height 속성으로 사진의 비율을 알려 두어야 사진이 도착하기 전에 자리가 잡혀 글이 밀리지 않는다. 이 이야기는 12장에서 이어진다.

행사 일정 페이지 반응형 작업대

이 장의 재료를 한 페이지에 모은다. 행사 일정 페이지의 "첫 시안"은 기기 이름 중단점(768·1024px), 픽셀로 박은 40픽셀 제목, 상한 없는 소개 글, 언제나 펼친 메뉴를 썼다. "서윤의 선택"은 콘텐츠 중단점(35em·56em), clamp()로 잇는 제목, 28em 소개 글, 좁은 화면에서 접히는 메뉴를 쓴다. 눈금자를 끌어 폭을 바꾸고, 도경처럼 글자 크기를 키워 본다.

SIMULATOR

행사 일정 페이지: 첫 시안과 서윤의 선택

좌우로 끌어 화면 폭을 바꾼다
모양 · 적용된 중단점—
제목 ÷ 본문—
소개 글 한 줄—
깨진 곳—
해볼 것: ① "첫 시안"에서 폭을 700에 둔다. 1단 카드의 설명이 40자를 넘고, 소개 글도 길다. 지후가 창을 줄이다 본 화면이다. ② 폭을 360으로 줄이고 "글자 200%"를 고른다. 펼친 메뉴가 두 줄로 꺾이고 화면 밖으로 밀린다. 픽셀로 박은 40픽셀 제목은 34픽셀이 된 본문과 거의 같아 위계가 사라진다(1.2배 미만이면 표시). ③ "서윤의 선택"으로 바꾸고 같은 조건을 본다. 메뉴는 "메뉴" 단추로 접히고, 제목은 rem을 따라 커지고, 중단점은 글자 크기만큼 밀린다. 폭과 글자 크기를 바꿔 가며 깨진 곳이 남는 조합을 찾아본다. (앞 시뮬레이터들의 모형을 합친 것이다. 첫 시안의 제목은 40px 고정, 서윤의 선택은 clamp(1.875rem, 1.630rem + 1.087vw, 2.5rem). 메뉴는 메뉴 항목 너비의 합이 머리 띠에 들어가지 않으면 깨짐으로 친다.)

작업대에서 서윤의 선택도 모든 조합에서 완벽하지는 않다. 글자를 200%로 키우고 폭을 아주 좁히면 행사 제목 한 줄에 몇 글자 들어가지 않는다. 반응형 디자인의 목표는 깨짐이 없는 상태가 아니라, 깨질 때 덜 아프게 깨지는 상태다. 가로로 넘치는 대신 아래로 길어지고, 겹치는 대신 줄을 바꾸고, 사라지는 대신 접힌다. WCAG 2.2의 성공 기준 1.4.10(리플로)이 요구하는 것도 같은 방향이다. 폭 320 CSS 픽셀(데스크톱에서 1,280픽셀 화면을 400%로 확대한 것과 같다)에서 세로 스크롤만으로 내용을 다 읽을 수 있어야 한다.

시안에 "사이"를 적어 넘긴다 세 폭(360·768·1280)으로 그린 시안만 넘기면 개발자는 그 사이를 짐작으로 채운다. 지후가 서윤에게 부탁한 것은 그림을 더 그리는 것이 아니라 메모를 다는 것이었다. 각 요소 옆에 "이 칸은 늘어난다(1fr)", "이 글은 28em에서 멈춘다", "이 제목은 30에서 40픽셀로 비탈", "이 카드는 칸이 25em을 넘으면 가로형"처럼 늘어나는 규칙을 적는다. 그리고 시안 파일 안에서 프레임 폭을 직접 끌어 보며(대부분의 디자인 도구가 자동 레이아웃으로 지원한다) 사이 폭에서 깨지는 곳을 먼저 찾는다. 고정 그림 세 장보다 규칙 다섯 줄이 화면에 더 가깝다.

서윤의 웹 개편 노트

CASE 서윤웹 개편 노트 · 5장

700픽셀에서 늘어난 카드

1월 말, 지후가 행사 일정 페이지의 첫 코드를 올렸다. 서윤의 세 폭 시안(360·768·1280)을 그대로 옮기고, 중단점은 디자인 도구의 기본 프리셋대로 768과 1024픽셀에 두었다. 세 폭에서는 시안과 똑같았다. 회의실에서 지후가 노트북의 브라우저 창 오른쪽 끝을 잡고 천천히 줄였다. 1,024 아래로 내려오자 행 모양 카드의 설명이 다시 한 줄 40자를 넘기더니, 768 바로 위에서는 1단으로 쌓인 카드가 화면 끝까지 늘어나 설명 한 줄이 마흔 자를 훌쩍 넘었다. 버튼은 손바닥보다 길었다. "시안에는 700이 없잖아요. 그런데 접는 휴대폰을 펼치면 딱 이쯤이에요."

서윤은 이 장의 방식대로 가장 좁은 폭에서 출발해 창을 넓혀 가며 콘텐츠가 어색해지는 곳을 적었다. 이 장의 모형으로 계산하면, 1단 카드는 화면 폭 580픽셀을 넘으면 설명이 40자를 넘고, 행 모양은 549픽셀부터 글 칸이 12자를 넘어 들어간다. 첫 중단점은 그 사이, 35em(560픽셀)으로 정했다. 행 모양은 910픽셀을 넘으면 다시 설명이 길어지고, 옆에 달력 칸을 붙인 모양은 853픽셀부터 가능하다. 둘째 중단점은 56em(896픽셀)이다. 둘 다 em으로 적었다. 한도경이 글자 크기를 가장 크게 해 둔 휴대폰과, 글자 크기를 150%로 맞춘 서윤의 노트북 브라우저로 확인해 보니 기기 이름 중단점에서는 768 바로 위와 1,024 위쪽에 새로 깨지는 구간이 생겼고, em 중단점은 840·1,344픽셀로 함께 밀려 깨지지 않았다. 도경은 "글자를 키우면 화면이 아예 다른 모양이 되는 게 오히려 편하네. 옛날 사이트는 그냥 다 겹쳤거든" 했다.

제목 크기는 중단점마다 정하던 30·35·40픽셀의 계단을 버리고, 화면 폭 360~1,280픽셀 사이를 잇는 비탈로 바꿨다. 기울기는 10px ÷ 920px = 1.087vw, 고정 부분은 30 − 360 × 10/920 ≈ 26.09px = 1.630rem이다. 최댓값이 최솟값의 1.33배라 확대에도 안전하다. 행사 설명과 서점 소개 같은 글 덩어리는 TypeBook 여름 노트의 28em 상한을 그대로 가져왔다. 상한을 504px이 아니라 28em으로 적은 까닭을, 서윤은 도경의 휴대폰 화면을 캡처해 노트에 붙였다. 신간 카드는 본문과 옆 칸 두 곳에 들어가므로 화면 폭이 아니라 담은 칸의 폭(25em)으로 가로형과 세로형을 가르기로 했다. 지후는 "이제 시안에 숫자 대신 규칙이 적혀 있어서 좋네요" 했다.

중단점 135em (560px) 중단점 256em (896px) 페이지 제목30 → 40px 글줄 상한28em
이 장에서 정한 것중단점 둘: 35em(기본 글자 크기에서 560px, 1단 → 행)과 56em(896px, 행 → 행 + 옆 칸). 모바일 우선(min-width)으로 적고 이름은 기기가 아니라 순서로 짓는다. 페이지 제목: 화면 폭 360~1,280px에서 30 → 40px, clamp(1.875rem, 1.630rem + 1.087vw, 2.5rem). 본문 글 덩어리의 최대 글줄 폭 28em(약 37자, TypeBook 6장 값). 콘텐츠 최대 폭은 4장의 1,200px, 화면 가장자리 여백은 3장의 16px. 신간 카드는 컨테이너 쿼리로, 담은 칸이 25em 이상이면 가로형.
아직 남은 문제시안 파일과 코드에 같은 초록이 몇 가지 다른 hex로 적혀 있고, 글자 크기도 px와 rem이 섞여 있다. 지후가 다크 모드를 넣어 보자고 했는데, 색을 어디서부터 바꿔야 할지 모르겠다.
다음 단계6장: 색과 글자 크기를 이름 붙은 토큰으로 옮겨 한 곳에서 관리한다.

핵심 정리

  1. 고정폭 레이아웃은 한 폭만 책임지고, 유동 레이아웃은 모든 폭을 같은 모양으로 늘인다. 유동에 최대 폭을 더하면 넓은 화면 문제가, 중단점을 더하면 좁은 화면 문제가 풀린다.
  2. 중단점은 기기 이름(768, 1024)이 아니라 콘텐츠가 어색해지는 곳에 둔다. 가장 좁은 폭에서 시작해 창을 넓히며 글줄이 길어지거나 칸이 좁아지는 곳을 찾는다.
  3. 미디어 쿼리의 em은 브라우저 기본 글자 크기를 기준으로 하므로, em으로 적은 중단점은 사용자가 글자를 키우면 함께 밀린다. px 중단점은 그대로 남아 글자를 키운 사람에게 깨진 화면을 보인다.
  4. 모바일 우선은 1단을 기본으로 두고 min-width로 넓은 화면의 규칙을 더하는 방식이다. 내용의 순위표가 1단의 순서가 된다.
  5. 모양은 그대로 두고 크기만 바뀌어야 하는 값(제목, 간격)은 clamp(최솟값, rem + vw, 최댓값)로 계단 대신 비탈을 만든다. vw만 쓰면 확대가 듣지 않는다.
  6. 글 덩어리에는 em으로 적은 최대 글줄 폭(예: 28em)을 둔다. 화면 폭, 콘텐츠 최대 폭, 글줄 폭의 세 층을 나눈다(WCAG 1.4.8: 한중일 문자 40자).
  7. 컨테이너 쿼리는 부품이 놓인 칸의 폭을 보고 모양을 바꾼다. srcset·sizes는 사진이 놓일 폭과 DPR에 맞는 파일을 고르게 한다. sizes를 빠뜨리면 브라우저는 화면 폭 전체를 가정한다.

확인 퀴즈

1. 1단으로 쌓은 행사 카드는 화면 폭 610픽셀부터 설명이 한 줄 40자를 넘고, 날짜를 옆으로 뺀 행 모양은 540픽셀부터 들어간다. 중단점을 두기에 가장 알맞은 곳은?

중단점은 "이 폭 아래에서는 앞 모양, 위에서는 뒤 모양"을 뜻하므로, 두 모양이 모두 괜찮은 구간(540~610) 안에 두어야 한다. 768에 두면 610~768 사이에서 1단 카드의 줄이 너무 길어진다. 320에서 행 모양을 쓰면 글 칸이 너무 좁아 제목이 여러 줄로 꺾인다. 기기 이름은 콘텐츠가 깨지는 곳과 상관이 없다.

2. 페이지 제목을 clamp()로 화면 폭 360픽셀에서 30픽셀, 1,280픽셀에서 40픽셀이 되게 이었다. 화면 폭 820픽셀에서 제목 크기는?

clamp의 선호값은 두 점 (360, 30)과 (1,280, 40)을 잇는 직선이다. 820 − 360 = 460은 920의 절반이므로 크기도 30 + 10 × 0.5 = 35픽셀이다. 최솟값과 최댓값은 360보다 좁거나 1,280보다 넓을 때만 쓰인다. 직선은 원점을 지나지 않으므로 화면 폭에 단순 비례하지 않는다.

3. 넓은 화면에서만 가로형이 되도록 신간 카드를 @media (min-width: 56em)으로 바꿨더니, 넓은 화면의 좁은 옆 칸에 들어간 같은 카드의 글 칸이 쪼그라들었다. 가장 알맞은 처방은?

미디어 쿼리는 화면 전체의 폭만 알아서, 같은 폭의 화면 안에서 넓은 칸과 좁은 칸을 구별하지 못한다. 컨테이너 쿼리를 쓰면 카드가 놓인 칸의 폭으로 판단하므로 같은 카드가 본문 칸에서는 가로형, 옆 칸에서는 세로형이 된다. 카드를 자리마다 복제하면 부품이 갈라지고, vw 글자는 확대가 듣지 않는다.

4. 사진의 칸 폭이 400 CSS 픽셀이고 기기 픽셀 비율이 2인 휴대폰에서, srcset="a.webp 480w, b.webp 800w, c.webp 1200w"와 알맞은 sizes를 적었다. 브라우저가 고를 가능성이 가장 큰 파일은?

브라우저는 sizes로 칸 폭(400 CSS 픽셀)을 알고, 여기에 DPR 2를 곱해 실제 픽셀 800이 필요하다고 계산한다. 그 이상인 파일 중 가장 작은 800w가 맞다. 480w는 흐리게 보이고, 1,200w는 필요보다 2.25배 많은 픽셀을 받는다. 명세는 마지막 고르기를 브라우저에 맡기므로 캐시나 통신 상태에 따라 달라질 수는 있지만, 여러 파일을 다 받지는 않는다.

출처