반응형 디자인 — Media Query부터 Container Query까지
같은 웹페이지를 모바일에서 열면 왜 레이아웃이 깨질까? 화면 크기가 달라져도 콘텐츠가 자연스럽게 재배치되려면 어떤 원리가 필요할까?
이 질문의 답이 바로 반응형 디자인(Responsive Design) 입니다. 한 번 만든 페이지가 모바일, 태블릿, 데스크톱 어디서든 읽기 좋게 적응하는 것 — CSS에서는 이걸 어떻게 구현하는지 하나씩 정리해 보겠습니다.
1. 반응형 디자인이란
반응형 디자인은 하나의 HTML로 모든 화면 크기에 대응하는 설계 방식 입니다.
Ethan Marcotte가 2010년에 제안한 개념으로, 핵심 원칙은 세 가지입니다.
- 유동적 그리드(Fluid Grid) — 고정 px 대신 비율 기반 레이아웃
- ** 유동적 이미지(Flexible Images)** — 컨테이너에 맞게 크기가 조절되는 이미지
- Media Query — 화면 조건에 따라 CSS를 분기
/* 반응형의 가장 기본 — 이미지가 컨테이너를 넘지 않도록 */
img {
max-width: 100%;
height: auto;
}
적응형 디자인(Adaptive Design)은 특정 해상도별로 별도 레이아웃을 만드는 방식이에요. 반응형은 유동적으로 변하고, 적응형은 딱딱 끊어서 변합니다. 요즘은 거의 반응형이 표준입니다.
2. viewport 메타 태그의 역할
반응형 CSS를 아무리 잘 작성해도, 이 한 줄이 없으면 모바일에서 제대로 동작하지 않습니다.
<meta name="viewport" content="width=device-width, initial-scale=1.0">
모바일 브라우저는 기본적으로 데스크톱 크기(보통 980px)로 페이지를 렌더링한 뒤 축소해서 보여줍니다. 이 메타 태그가 하는 일은:
| 속성 | 의미 |
|---|---|
width=device-width | 뷰포트 너비를 실제 디바이스 너비로 설정 |
initial-scale=1.0 | 초기 확대/축소 비율을 1배로 설정 |
이 태그가 없으면 Media Query의 max-width: 768px이 모바일에서 작동하지 않는 상황이 생깁니다. 브라우저가 자기 뷰포트를 980px로 인식하기 때문이에요.
<!-- ❌ user-scalable=no는 접근성을 해침 — 사용 지양 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<!-- ✅ 확대/축소를 허용하면서 반응형 적용 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
3. Media Query
기본 문법
Media Query는 "이 조건이면 이 스타일을 적용해라" 라는 조건부 CSS입니다.
/* 뷰포트 너비가 768px 이상이면 적용 */
@media (min-width: 768px) {
.container {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
min-width vs max-width
이 둘의 차이가 반응형 전략의 방향을 결정합니다.
/* max-width: "이 너비 이하이면" — 데스크톱 퍼스트 */
@media (max-width: 768px) {
/* 데스크톱 기본 스타일에서 모바일용으로 축소 */
.sidebar { display: none; }
}
/* min-width: "이 너비 이상이면" — 모바일 퍼스트 */
@media (min-width: 768px) {
/* 모바일 기본 스타일에서 데스크톱용으로 확장 */
.sidebar { display: block; }
}
모바일 퍼스트 전략
모바일 퍼스트는 기본 CSS가 모바일용 이고, 화면이 커질수록 스타일을 추가하는 방식입니다.
/* 기본: 모바일 (1열) */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* 태블릿 이상: 2열 */
@media (min-width: 768px) {
.grid {
grid-template-columns: 1fr 1fr;
}
}
/* 데스크톱 이상: 3열 */
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
모바일 퍼스트가 권장되는 이유:
- 모바일 사용자에게 불필요한 CSS를 로드하지 않음 (성능)
- 핵심 콘텐츠부터 설계하게 됨 (콘텐츠 우선)
- Media Query가 점진적으로 추가되어 코드가 깔끔함
다양한 조건 조합
/* 가로 모드 */
@media (orientation: landscape) {
.hero { height: 60vh; }
}
/* 조건 결합: AND */
@media (min-width: 768px) and (max-width: 1023px) {
/* 태블릿 전용 스타일 */
}
/* hover 가능한 디바이스 (마우스가 있는 환경) */
@media (hover: hover) {
.card:hover { transform: translateY(-4px); }
}
/* 다크 모드 감지 */
@media (prefers-color-scheme: dark) {
:root { --bg: #1a1a2e; }
}
/* 모션 줄이기 선호 (접근성) */
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}
hover: hover는 꽤 유용합니다. 모바일에는 hover 상태가 없으니, 터치 디바이스에서 불필요한 hover 효과를 제거할 때 쓸 수 있어요.
4. 반응형 단위
고정 px만 쓰면 반응형이 어렵습니다. 상황에 맞는 단위를 골라 쓰는 게 핵심이에요.
% (퍼센트)
부모 요소를 기준으로 한 상대 크기입니다.
.container {
width: 90%; /* 부모의 90% */
max-width: 1200px; /* 하지만 1200px을 넘지 않음 */
margin: 0 auto; /* 가운데 정렬 */
}
vw / vh / dvh
뷰포트(브라우저 창)를 기준으로 한 단위입니다.
.hero {
/* 뷰포트 너비의 100%, 높이의 100% */
width: 100vw;
height: 100vh;
}
모바일 브라우저에서 100vh를 쓰면 주소창에 가려지는 문제 가 생깁니다. 스크롤하면 주소창이 숨겨지면서 높이가 변하거든요. 이걸 해결하는 게 동적 뷰포트 단위입니다.
.hero {
/* dvh: 동적 뷰포트 높이 — 주소창 상태에 따라 변함 */
height: 100dvh;
/* svh: 주소창이 보일 때의 최소 높이 */
/* lvh: 주소창이 숨겨질 때의 최대 높이 */
}
| 단위 | 설명 | 사용 시점 |
|---|---|---|
vh | 고정 뷰포트 높이 | 데스크톱 전용 |
dvh | 동적 뷰포트 높이 | 모바일 풀스크린 히어로 |
svh | 최소 뷰포트 높이 (주소창 보임) | 콘텐츠가 잘리면 안 되는 경우 |
lvh | 최대 뷰포트 높이 (주소창 숨김) | 최대 공간 활용 시 |
rem / em
/* rem: html의 font-size 기준 (보통 16px) */
/* em: 부모의 font-size 기준 */
html { font-size: 16px; }
.title {
font-size: 2rem; /* 32px — 일관된 기준 */
margin-bottom: 1em; /* 자신의 font-size(32px) 기준 = 32px */
}
rem— 전역 기준이라 예측 가능, 레이아웃·여백에 추천em— 부모에 따라 달라져서, padding이나 margin이 글자 크기에 비례해야 할 때 유용
clamp() — 반응형의 핵심 함수
clamp(최솟값, 선호값, 최댓값)은 세 값 사이에서 자동으로 조절됩니다.
.title {
/* 최소 1.5rem, 뷰포트 기준 4vw, 최대 3rem */
font-size: clamp(1.5rem, 4vw, 3rem);
}
.container {
/* Media Query 없이 반응형 패딩 */
padding: clamp(1rem, 3vw, 3rem);
}
.grid {
/* 반응형 간격 */
gap: clamp(0.5rem, 2vw, 2rem);
}
clamp()를 잘 쓰면 Media Query 없이도 부드러운 반응형을 만들 수 있어요. 브레이크포인트에서 "딱" 변하는 대신, 뷰포트 크기에 따라 자연스럽게 전환됩니다.
5. Container Queries
Media Query의 한계
Media Query는 뷰포트 기준이라, 같은 컴포넌트를 다른 영역에 배치했을 때 문제가 생깁니다.
┌─ 뷰포트 1200px ──────────────────────────┐
│ ┌─ 메인 (800px) ─┐ ┌─ 사이드바 (400px) ─┐│
│ │ 카드 컴포넌트 │ │ 카드 컴포넌트 ││
│ │ → 가로 레이아웃 │ │ → 가로 레이아웃? ││
│ └────────────────┘ └───────────────────┘│
└──────────────────────────────────────────┘
뷰포트가 1200px이면 두 카드 모두 "데스크톱 스타일"이 적용되지만, 사이드바의 카드는 400px 공간에 있으니 세로 레이아웃이 더 적절합니다. Media Query로는 이걸 해결하기 어렵습니다.
@container 기본 사용법
Container Query는 ** 부모 컨테이너의 크기 **를 기준으로 스타일을 전환합니다.
/* 1단계: 컨테이너로 지정 */
.card-wrapper {
container-type: inline-size; /* 너비 기준 쿼리 허용 */
container-name: card; /* 이름 지정 (선택) */
}
/* 2단계: 컨테이너 크기에 따른 스타일 */
.card {
display: grid;
grid-template-columns: 1fr; /* 기본: 세로 레이아웃 */
}
@container card (min-width: 400px) {
.card {
/* 컨테이너가 400px 이상이면 가로 레이아웃 */
grid-template-columns: 200px 1fr;
}
}
container-type 값
.wrapper {
/* inline-size: 인라인 방향(보통 너비)만 쿼리 가능 — 가장 많이 사용 */
container-type: inline-size;
/* size: 너비와 높이 모두 쿼리 가능 */
container-type: size;
/* normal: 기본값, 컨테이너로 사용하지 않음 */
container-type: normal;
}
/* 축약 문법 */
.wrapper {
container: card / inline-size;
/* = container-name: card + container-type: inline-size */
}
실전 예시 — 재사용 가능한 카드 컴포넌트
/* 카드를 감싸는 컨테이너 */
.card-container {
container-type: inline-size;
}
/* 카드 기본 스타일 (좁은 공간) */
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card__image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 8px;
}
.card__title {
font-size: 1rem;
}
/* 컨테이너 400px 이상 — 가로 배치 */
@container (min-width: 400px) {
.card {
flex-direction: row;
align-items: center;
}
.card__image {
width: 150px;
aspect-ratio: 1;
}
}
/* 컨테이너 600px 이상 — 더 넓은 가로 배치 */
@container (min-width: 600px) {
.card__image {
width: 250px;
}
.card__title {
font-size: 1.25rem;
}
}
이렇게 만들면 카드 컴포넌트가 메인 영역에 있든, 사이드바에 있든, 모달 안에 있든 자기 공간에 맞게 알아서 적응 합니다.
Container Query 단위
컨테이너 크기를 기준으로 한 상대 단위도 있습니다.
@container (min-width: 400px) {
.card__title {
/* cqi: 컨테이너 인라인 사이즈의 1% */
font-size: clamp(1rem, 3cqi, 1.5rem);
}
}
| 단위 | 설명 |
|---|---|
cqw | 컨테이너 너비의 1% |
cqh | 컨테이너 높이의 1% |
cqi | 컨테이너 인라인 크기의 1% |
cqb | 컨테이너 블록 크기의 1% |
6. 반응형 이미지
이미지는 반응형에서 가장 성능에 영향을 주는 요소입니다. 모바일에서 4K 이미지를 로드하면 데이터와 시간 낭비예요.
srcset — 해상도별 이미지 후보
<!-- w 서술자: 이미지의 실제 너비를 브라우저에 알려줌 -->
<img
src="photo-800.jpg"
srcset="
photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w
"
sizes="(max-width: 600px) 100vw, 50vw"
alt="풍경 사진"
>
동작 원리:
sizes로 이미지가 차지할 레이아웃 너비를 브라우저에 알림- 브라우저가 뷰포트 크기와 디바이스 픽셀 비율(DPR)을 고려해서 최적 이미지 선택
- 2배 레티나(DPR=2) 디바이스에서 뷰포트 400px → 실제 필요한 건 800w 이미지
<!-- x 서술자: 디바이스 픽셀 비율 기준 -->
<img
src="logo.png"
srcset="
logo.png 1x,
logo@2x.png 2x,
logo@3x.png 3x
"
alt="로고"
>
picture 태그 — 아트 디렉션
srcset은 같은 이미지의 크기만 다르지만, <picture>는 완전히 다른 이미지 를 보여줄 수 있습니다.
<picture>
<!-- 모바일: 세로로 잘린 클로즈업 -->
<source
media="(max-width: 767px)"
srcset="hero-mobile.webp"
type="image/webp"
>
<!-- 데스크톱: 넓은 파노라마 -->
<source
media="(min-width: 768px)"
srcset="hero-desktop.webp"
type="image/webp"
>
<!-- 폴백 -->
<img src="hero-desktop.jpg" alt="히어로 이미지">
</picture>
source는 위에서부터 조건을 확인, 맞는 첫 번째 것을 사용type으로 WebP/AVIF 같은 최신 포맷 지원 여부도 분기 가능img는 반드시 마지막에 폴백으로 포함해야 함
7. 브레이크포인트 설계 전략
대표적인 브레이크포인트
특정 디바이스 크기에 맞추기보다는, 콘텐츠가 깨지는 지점을 기준으로 잡는 게 좋습니다. 그래도 참고할 수 있는 일반적인 기준은:
/* 모바일 퍼스트 기준 */
/* 기본: 0 ~ 639px (모바일) */
@media (min-width: 640px) { /* sm: 태블릿 세로 */ }
@media (min-width: 768px) { /* md: 태블릿 가로 */ }
@media (min-width: 1024px) { /* lg: 소형 데스크톱 */ }
@media (min-width: 1280px) { /* xl: 데스크톱 */ }
@media (min-width: 1536px) { /* 2xl: 대형 모니터 */ }
CSS 변수로 관리
브레이크포인트를 직접 사용하기보다, 전체적인 접근 전략을 세우는 게 중요합니다.
:root {
/* 레이아웃 관련 변수 */
--content-max-width: 1200px;
--content-padding: clamp(1rem, 5vw, 3rem);
}
.layout {
max-width: var(--content-max-width);
padding-inline: var(--content-padding);
margin-inline: auto;
}
디바이스가 아니라 콘텐츠 기준으로
/* ❌ 디바이스 기준 — "아이폰은 375px이니까..." */
@media (max-width: 375px) { ... }
/* ✅ 콘텐츠 기준 — "이 카드가 2열이면 읽기 좋은 최소 너비가 600px" */
@media (min-width: 600px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
정리
| 기술 | 기준 | 사용 시점 |
|---|---|---|
| Media Query | 뷰포트 크기 | 전체 페이지 레이아웃 전환 |
| Container Query | 컨테이너 크기 | 재사용 컴포넌트의 자체 적응 |
| clamp() | 뷰포트 비례 | 글자·여백의 부드러운 크기 조절 |
| srcset/picture | 디바이스 조건 | 이미지 최적화 |
| dvh/svh | 동적 뷰포트 | 모바일 풀스크린 레이아웃 |
공부하다 보니, 반응형은 단순히 "모바일에서도 보이게 하는 것"이 아니라 다양한 맥락에서 콘텐츠가 최적으로 소비될 수 있도록 설계하는 것 이더라고요. Media Query로 전체 레이아웃을 잡고, Container Query로 컴포넌트를 자립적으로 만들고, clamp()로 미세한 크기 조절을 자동화하는 — 이 세 가지 조합이 모던 반응형의 핵심이라고 생각합니다.