1-5. 반응형 디자인
한 줄 요약: 연속적인 뷰포트라는 웹 고유의 제약을 이해하고, 모바일 퍼스트 미디어 쿼리·유동 레이아웃·반응형 이미지로 하나의 마크업이 모든 화면에 대응하게 만들 수 있다.
학습 목표
- viewport 메타 태그가 왜 필요한지 모바일 브라우저의 렌더링 방식으로 설명할 수 있다.
- 콘텐츠 기준의 브레이크포인트를 설계하고 모바일 퍼스트로 미디어 쿼리를 작성할 수 있다.
clamp()와 유연한 레이아웃으로 미디어 쿼리 자체를 줄이는 방식을 판단할 수 있다.srcset/sizes/<picture>의 역할 분담을 이해하고 상황에 맞게 선택할 수 있다.
배경: 왜 이것이 존재하는가
모바일 앱 개발자는 대응할 해상도의 목록을 안다. iOS라면 기기 종류가 유한하고, Android도 밀도 버킷으로 정리된다. 웹에는 그 목록이 없다. 320px 폰부터 5120px 울트라와이드까지, 창 크기를 실시간으로 드래그하는 사용자까지 — 뷰포트는 이산적이지 않고 연속적이다. "지원 해상도 목록"이라는 개념 자체가 성립하지 않는다.
2007년 아이폰이 등장했을 때 웹은 이 문제에 답이 없었다. 당시 사이트들은 데스크톱(~980px) 전제로 만들어져 있었고, 애플의 선택은 "모바일 브라우저가 980px짜리 가상 뷰포트에 페이지를 렌더링한 뒤 화면에 맞게 축소해 보여주기"였다. 깨지진 않지만 글자가 좁쌀만 한, 그 시절의 모바일 웹이다. 한동안 업계는 m.example.com 같은 모바일 전용 사이트를 별도로 운영했다 — 마크업 두 벌, 기능 불일치, URL 분열이라는 비용과 함께.
2010년 "반응형 웹 디자인(responsive web design)"이라는 접근이 이를 정리했다. 하나의 HTML이 유동 그리드, 유연한 이미지, 미디어 쿼리로 모든 뷰포트에 적응한다는 원칙이다. 이 문서는 그 세 기둥의 현대판을 다룬다. 전제 지식은 1-4의 Flexbox/Grid다 — 반응형의 절반은 미디어 쿼리가 아니라 애초에 유연한 레이아웃이 해결하기 때문이다.
핵심 개념
viewport 메타 태그: 가상 뷰포트 끄기
위 역사 때문에, 모바일 브라우저는 지금도 기본적으로 가상 뷰포트(~980px)로 렌더링한다. 반응형 CSS를 아무리 잘 짜도 이 스위치를 끄지 않으면 무의미하다.
<meta name="viewport" content="width=device-width, initial-scale=1">width=device-width는 "가상 뷰포트 말고 기기의 실제 논리 폭으로 렌더링하라", initial-scale=1은 "축소하지 말라"는 뜻이다. 이때의 폭은 물리 픽셀이 아니라 CSS 픽셀이다. 아이폰의 물리 해상도가 1170px여도 CSS 뷰포트는 390px이고, 그 비율(3배)이 DPR(device pixel ratio)이다. DPR은 뒤의 반응형 이미지에서 다시 등장한다.
접근성 주의 하나: maximum-scale=1이나 user-scalable=no로 사용자 확대(핀치 줌)를 막는 설정은 저시력 사용자를 차단하는 안티패턴이며, 최신 브라우저는 이를 무시하기도 한다.
미디어 쿼리와 모바일 퍼스트
미디어 쿼리는 뷰포트 조건에 따라 규칙 블록을 켜고 끄는 조건문이다.
/* 기본(조건 없음): 모바일 레이아웃 */
.products { display: grid; grid-template-columns: 1fr; gap: 16px; }
/* 뷰포트가 640px 이상이 되면 2열 */
@media (min-width: 640px) {
.products { grid-template-columns: repeat(2, 1fr); }
}
/* 1024px 이상이면 사이드바 등장 */
@media (min-width: 1024px) {
.page { display: grid; grid-template-columns: 240px 1fr; }
.products { grid-template-columns: repeat(3, 1fr); }
}모바일 퍼스트 — 기본 스타일을 최소 화면 기준으로 쓰고 min-width로 넓은 화면을 추가해 가는 방식 — 가 표준 관행이 된 데는 구체적 이유가 있다:
- 좁은 화면 레이아웃은 대개 "한 열로 쌓기"여서 CSS가 단순하다. 단순한 쪽을 기본값으로, 복잡한 쪽을 예외로 두는 것이 오버라이드 양을 줄인다.
- 데스크톱 퍼스트(
max-width로 깎아 내려가기)는 복잡한 레이아웃을 먼저 선언하고 좁은 화면에서 해제하는 방식이라, 취소용 선언(float: none,width: auto류)이 쌓인다. - 성능·우선순위 관점에서도 제약이 큰 환경(모바일)을 기준선으로 삼는 사고방식이 콘텐츠 우선순위 결정을 강제한다.
브레이크포인트는 기기가 아니라 콘텐츠에서 나온다. "아이폰이 390px이니까 390px"이 아니라, 창을 좁혀 가며 레이아웃이 실제로 어색해지는 지점에 끊는 것이다. 기기 목록은 매년 바뀌지만 "이 카드 그리드는 640px 아래에서 한 열이 낫다"는 판단은 콘텐츠가 유지되는 한 유효하다. 관행적으로 640/768/1024/1280px 근처의 값이 많이 쓰이지만 이는 출발점일 뿐이다.
뷰포트 폭 외의 유용한 쿼리도 있다. (prefers-color-scheme: dark)(다크 모드), (prefers-reduced-motion: reduce)(동작 최소화 — 1-6의 애니메이션과 연결), (hover: hover)(hover 가능한 포인팅 장치인가 — 터치 기기에서 hover 의존 UI를 걸러낼 때) 등 사용자 환경·선호를 조건으로 쓸 수 있다.
미디어 쿼리를 줄이는 유동 기법
미디어 쿼리는 "계단"이다 — 브레이크포인트에서 값이 점프한다. 현대 CSS는 값 자체를 뷰포트에 연속적으로 연동시켜 계단을 없애는 도구를 제공한다.
h1 {
/* 최소 1.5rem, 최대 3rem, 그 사이에서는 뷰포트 폭에 비례해 흐른다 */
font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
}
.container {
/* 미디어 쿼리 없는 반응형 컨테이너: 넓으면 1100px, 좁으면 폭의 92% */
width: min(1100px, 92%);
margin-inline: auto;
}여기에 1-4에서 본 repeat(auto-fill, minmax(250px, 1fr)) 그리드까지 합치면, 상당수 레이아웃이 미디어 쿼리 없이 반응형이 된다. 실무 감각은 이렇다: 유동 기법으로 기본을 해결하고, 구조 자체가 바뀌는 지점(사이드바 등장/소멸, 내비게이션 접힘)에만 미디어 쿼리를 쓴다.
컨테이너 쿼리: 컴포넌트 단위 반응형
미디어 쿼리의 구조적 한계는 기준이 항상 뷰포트라는 점이다. 같은 카드 컴포넌트가 넓은 본문에선 가로형, 좁은 사이드바에선 세로형이어야 할 때, 뷰포트 폭은 아무것도 말해 주지 않는다 — 중요한 것은 부모가 내준 폭이다. 컴포넌트 기반 개발(React 등)이 표준이 되면서 이 불일치가 커졌고, 그 답이 컨테이너 쿼리(@container)다.
.sidebar, .main-content {
container-type: inline-size; /* 이 요소를 크기 측정 기준으로 선언 */
}
.card { display: grid; gap: 12px; }
@container (min-width: 400px) {
/* 뷰포트가 아니라 "내가 놓인 컨테이너"가 400px 이상일 때 */
.card { grid-template-columns: 120px 1fr; }
}카드는 이제 자기가 어디에 놓이든 스스로 적응한다. "페이지가 반응하는" 설계에서 "컴포넌트가 반응하는" 설계로의 전환이며, 미디어 쿼리를 대체한다기보다 역할을 나눈다 — 페이지 구조는 미디어 쿼리, 컴포넌트 내부는 컨테이너 쿼리. 컨테이너 쿼리는 모든 주요 브라우저에서 지원되며 Baseline Widely available(2026년 기준) 상태다.
반응형 이미지: 다운로드 크기의 문제
레이아웃이 유연해도 이미지가 한 벌이면, 320px 화면의 3G 사용자가 2000px짜리 원본을 받는다. 반응형 이미지는 레이아웃 문제가 아니라 전송 최적화 문제이고, 도구가 두 계열이다.
같은 이미지의 해상도 선택 — srcset + sizes:
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="(min-width: 1024px) 50vw, 100vw"
alt="여름 시즌 프로모션 배너">동작 모델: srcset은 후보와 각각의 실제 폭(w)을, sizes는 "이 이미지가 레이아웃에서 차지할 폭"을 브라우저에 알려준다. 브라우저는 그 폭 × DPR을 계산해 가장 경제적인 후보를 스스로 고른다. 선택권이 개발자가 아니라 브라우저에 있다는 점이 핵심이다 — 브라우저만이 현재 DPR, 네트워크 상태를 알기 때문이다. sizes가 필요한 이유도 여기 있다: 후보 선택은 CSS 레이아웃 계산 전에 일어나므로, 이미지가 차지할 폭을 힌트로 미리 줘야 한다.
다른 이미지로 교체(아트 디렉션) 또는 포맷 분기 — <picture>:
<picture>
<!-- 좁은 화면: 인물 위주로 크롭한 세로 컷 -->
<source media="(max-width: 640px)" srcset="hero-portrait.jpg">
<source type="image/avif" srcset="hero.avif"> <!-- 지원 브라우저엔 AVIF -->
<img src="hero.jpg" alt="여름 시즌 프로모션 배너">
</picture>판단 기준: 같은 그림의 크기만 다르면 srcset(브라우저가 선택), 그림 자체가 달라져야 하면 <picture>(개발자가 지정). 포맷·최적화 전략의 심화(AVIF/WebP, 지연 로딩, CDN 리사이징)는 Phase 7-3 성능 문서에서 다룬다.
이미지 요소에는 width/height 속성(또는 CSS aspect-ratio)을 함께 명시해 로딩 전에 자리를 확보한다 — 로딩 중 레이아웃이 밀리는 현상(CLS)의 예방이며, 역시 Phase 7-3의 주제와 연결된다.
실무 관점
- 테스트는 브레이크포인트가 아니라 연속 구간으로. DevTools 기기 툴바에서 특정 기기 프리셋만 확인하는 습관은 위험하다. 창을 연속으로 드래그하면서 모든 구간에서 깨지는 지점을 찾는다. 잊기 쉬운 구간: 641~1023px의 태블릿·세로 모드, 320px(최소 보장선), 그리고 브라우저 폰트 크기를 125%로 키운 상태.
100vh함정. 모바일 브라우저의 주소창이 나타났다 사라지며 뷰포트 높이가 변하는데,100vh는 주소창이 숨은 상태 기준이라 하단이 잘리는 사고가 잦았다. 현대 단위100dvh(dynamic viewport height)가 이 문제의 해답이며 Baseline이다.- hover 의존 UI 점검. 터치 기기에는 hover가 없다. 데스크톱에서 hover로만 드러나는 메뉴·버튼은 터치 사용자에게 접근 불가가 된다.
(hover: hover)쿼리로 분기하거나, hover는 보조 신호로만 쓴다. - 반응형 ≠ 축소. 화면이 좁아질 때 모든 요소를 그대로 우겨넣는 것이 아니라 우선순위를 재배치하는 것이다 — 무엇을 접고(내비게이션), 무엇을 뒤로 미루고(사이드바), 무엇을 유지할지(핵심 콘텐츠)의 정보 설계 판단이 먼저고 CSS는 그 표현이다.
- 이미지 파이프라인은 빌드/서버의 일.
srcset에 넣을 여러 크기의 이미지를 손으로 만들지는 않는다. 실무에서는 빌드 도구나 이미지 CDN이 생성한다. 지금 단계에서는 마크업 계층의 원리를 정확히 아는 것이 목표다.
더 깊이
컨테이너 쿼리는 왜 20년 늦게 왔는가. "부모 크기에 반응하는 컴포넌트"는 반응형 초기부터의 요구였지만, 순진하게 구현하면 순환 참조가 생긴다: 컨테이너 폭은 (정규 흐름에서) 콘텐츠가 결정할 수 있는데, 그 콘텐츠의 스타일이 다시 컨테이너 폭에 의존하면 "자식이 커진다 → 부모가 커진다 → 쿼리 조건이 바뀐다 → 자식이 작아진다 → …"의 무한 진동이 가능하다. container-type: inline-size가 격리(containment)를 강제하는 이유가 이것이다 — 이 선언은 "내 인라인 크기는 내 콘텐츠와 무관하게 결정된다"는 계약이어서, 순환의 한쪽 고리를 끊어야만 쿼리가 성립한다. 컨테이너로 선언한 요소가 콘텐츠 기반 크기(auto sizing)를 잃는 것은 버그가 아니라 이 계약의 대가다.
뷰포트 단위는 이제 가족이다. 모바일 주소창 문제의 정확한 해부: 주소창이 보일 때의 뷰포트가 small(svh), 숨었을 때가 large(lvh), 현재 상태를 실시간 추적하는 것이 dynamic(dvh)이다. 100dvh가 만능은 아니다 — 스크롤 중 주소창이 접히며 값이 연속적으로 변하므로, dvh에 묶인 레이아웃은 스크롤 내내 리사이즈된다(비용과 출렁임). 화면을 꽉 채우는 첫 화면(hero)처럼 "지금 보이는 만큼"이 중요하면 dvh, 스크롤 중 안정성이 중요하면 svh(최솟값 보장)가 안전한 기본값이다.
srcset 선택은 비결정적이도록 설계됐다. 스펙은 후보 중 무엇을 고를지 브라우저 재량에 맡긴다. 실제로 브라우저는 "계산상 필요한 크기보다 큰 이미지가 이미 캐시에 있으면 그것을 재사용"하고, 데이터 절약 모드에서는 더 작은 후보를 고를 수 있다. 따라서 srcset은 "이 조건엔 이 파일"이라는 지시가 아니라 후보 집합의 공표로 이해해야 한다. 특정 이미지를 강제해야 하는 요구(아트 디렉션)에 <picture>의 media가 별도로 존재하는 이유이기도 하다 — 그쪽은 재량 없이 결정적으로 매칭된다.
정리
- 모바일 브라우저는 기본적으로 ~980px 가상 뷰포트로 렌더링하며, viewport 메타 태그가 이를 끄는 스위치다. 뷰포트 폭은 물리 픽셀이 아닌 CSS 픽셀이다.
- 미디어 쿼리는 모바일 퍼스트(
min-width누적)로 작성하고, 브레이크포인트는 기기 목록이 아니라 콘텐츠가 어색해지는 지점에서 얻는다. clamp(),min(),auto-fill그리드 같은 유동 기법으로 기본을 해결하고, 구조가 바뀌는 지점에만 미디어 쿼리를 쓴다. 컴포넌트 내부의 반응형은 컨테이너 쿼리의 영역이다.- 반응형 이미지는 전송 최적화 문제다. 크기 선택은
srcset+sizes로 브라우저에 맡기고, 그림 자체가 바뀌면<picture>로 지정한다. 100dvh,(hover: hover), 핀치 줌 허용 — 모바일 실기기의 현실을 반영하는 디테일을 챙긴다.
확인 문제
1. 반응형 CSS를 완벽히 작성했는데 실제 폰에서 보니 페이지 전체가 축소된 데스크톱 레이아웃으로 보인다. 미디어 쿼리 (min-width: 640px)도 폰에서 적용되어 버렸다. 원인은?
정답과 해설
viewport 메타 태그가 없다. 브라우저가 ~980px 가상 뷰포트로 렌더링한 뒤 화면에 맞게 축소하므로, CSS 입장에서 뷰포트 폭은 980px이고 min-width: 640px 쿼리가 참이 된다. <meta name="viewport" content="width=device-width, initial-scale=1">을 추가하면 실제 기기 폭(CSS 픽셀) 기준으로 렌더링되어 모바일 레이아웃이 적용된다.
2. 카드 컴포넌트가 본문(넓음)과 사이드바(좁음) 양쪽에 쓰인다. 뷰포트 1200px에서 사이드바 속 카드가 가로형으로 펼쳐져 깨진다. 미디어 쿼리로 이 문제를 풀 수 없는 이유와 올바른 도구는?
정답과 해설
미디어 쿼리의 조건은 뷰포트 폭인데, 이 문제의 변수는 뷰포트가 아니라 카드가 놓인 부모의 폭이다. 뷰포트 1200px라는 같은 조건에서 본문 카드는 가로형이 맞고 사이드바 카드는 세로형이어야 하므로, 뷰포트 기준 분기로는 두 상태를 동시에 만족시킬 수 없다. 부모에 container-type: inline-size를 선언하고 @container (min-width: …)로 카드 레이아웃을 분기하는 컨테이너 쿼리가 올바른 도구다.
3. 다음 마크업에서 DPR 2, 뷰포트 500px인 폰은 어느 이미지를 받을 가능성이 높은가? 추론 과정을 설명하라.
<img srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="100vw" src="photo-800.jpg" alt="...">정답과 해설
sizes="100vw"이므로 이미지의 레이아웃 폭은 500 CSS px. 필요한 실제 픽셀은 500 × DPR 2 = 1000px. 후보 중 1000px 이상을 처음 만족하는 것은 1600w이므로 photo-1600.jpg를 받을 가능성이 높다(800w는 1000px에 못 미쳐 흐려진다). 정확한 선택은 브라우저 재량(네트워크 상태 등 반영 가능)이지만 계산 모델은 "레이아웃 폭 × DPR ≥ 후보 폭"이다.
참고 자료
- MDN — 반응형 디자인 — 반응형의 세 기둥을 하나의 흐름으로 정리한 입문 겸 레퍼런스.
- MDN — Viewport 메타 태그 — viewport 설정 값들의 정확한 의미와 접근성 주의사항.
- MDN — 반응형 이미지 — srcset/sizes/picture의 동작 모델과 선택 기준.
- MDN — CSS 컨테이너 쿼리 — container-type, 컨테이너 단위(cqw 등)까지 포함한 정리.
- web.dev — Learn Responsive Design — Google의 반응형 디자인 코스. 이론과 실측 도구를 함께 다룬다.