같은 웹페이지를 모바일에서 열면 왜 레이아웃이 깨질까? 화면 크기가 달라져도 콘텐츠가 자연스럽게 재배치되려면 어떤 원리가 필요할까?

이 질문의 답이 바로 반응형 디자인(Responsive Design) 입니다. 한 번 만든 페이지가 모바일, 태블릿, 데스크톱 어디서든 읽기 좋게 적응하는 것 — CSS에서는 이걸 어떻게 구현하는지 하나씩 정리해 보겠습니다.


1. 반응형 디자인이란

반응형 디자인은 하나의 HTML로 모든 화면 크기에 대응하는 설계 방식 입니다.

Ethan Marcotte가 2010년에 제안한 개념으로, 핵심 원칙은 세 가지입니다.

  • 유동적 그리드(Fluid Grid) — 고정 px 대신 비율 기반 레이아웃
  • ** 유동적 이미지(Flexible Images)** — 컨테이너에 맞게 크기가 조절되는 이미지
  • Media Query — 화면 조건에 따라 CSS를 분기
CSS
/* 반응형의 가장 기본 — 이미지가 컨테이너를 넘지 않도록 */
img {
  max-width: 100%;
  height: auto;
}

적응형 디자인(Adaptive Design)은 특정 해상도별로 별도 레이아웃을 만드는 방식이에요. 반응형은 유동적으로 변하고, 적응형은 딱딱 끊어서 변합니다. 요즘은 거의 반응형이 표준입니다.


2. viewport 메타 태그의 역할

반응형 CSS를 아무리 잘 작성해도, 이 한 줄이 없으면 모바일에서 제대로 동작하지 않습니다.

HTML
<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로 인식하기 때문이에요.

HTML
<!-- ❌ 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입니다.

CSS
/* 뷰포트 너비가 768px 이상이면 적용 */
@media (min-width: 768px) {
  .container {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

min-width vs max-width

이 둘의 차이가 반응형 전략의 방향을 결정합니다.

CSS
/* max-width: "이 너비 이하이면" — 데스크톱 퍼스트 */
@media (max-width: 768px) {
  /* 데스크톱 기본 스타일에서 모바일용으로 축소 */
  .sidebar { display: none; }
}

/* min-width: "이 너비 이상이면" — 모바일 퍼스트 */
@media (min-width: 768px) {
  /* 모바일 기본 스타일에서 데스크톱용으로 확장 */
  .sidebar { display: block; }
}

모바일 퍼스트 전략

모바일 퍼스트는 기본 CSS가 모바일용 이고, 화면이 커질수록 스타일을 추가하는 방식입니다.

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가 점진적으로 추가되어 코드가 깔끔함

다양한 조건 조합

CSS
/* 가로 모드 */
@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만 쓰면 반응형이 어렵습니다. 상황에 맞는 단위를 골라 쓰는 게 핵심이에요.

% (퍼센트)

부모 요소를 기준으로 한 상대 크기입니다.

CSS
.container {
  width: 90%;        /* 부모의 90% */
  max-width: 1200px; /* 하지만 1200px을 넘지 않음 */
  margin: 0 auto;    /* 가운데 정렬 */
}

vw / vh / dvh

뷰포트(브라우저 창)를 기준으로 한 단위입니다.

CSS
.hero {
  /* 뷰포트 너비의 100%, 높이의 100% */
  width: 100vw;
  height: 100vh;
}

모바일 브라우저에서 100vh를 쓰면 주소창에 가려지는 문제 가 생깁니다. 스크롤하면 주소창이 숨겨지면서 높이가 변하거든요. 이걸 해결하는 게 동적 뷰포트 단위입니다.

CSS
.hero {
  /* dvh: 동적 뷰포트 높이 — 주소창 상태에 따라 변함 */
  height: 100dvh;

  /* svh: 주소창이 보일 때의 최소 높이 */
  /* lvh: 주소창이 숨겨질 때의 최대 높이 */
}
단위설명사용 시점
vh고정 뷰포트 높이데스크톱 전용
dvh동적 뷰포트 높이모바일 풀스크린 히어로
svh최소 뷰포트 높이 (주소창 보임)콘텐츠가 잘리면 안 되는 경우
lvh최대 뷰포트 높이 (주소창 숨김)최대 공간 활용 시

rem / em

CSS
/* 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(최솟값, 선호값, 최댓값)은 세 값 사이에서 자동으로 조절됩니다.

CSS
.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는 뷰포트 기준이라, 같은 컴포넌트를 다른 영역에 배치했을 때 문제가 생깁니다.

PLAINTEXT
┌─ 뷰포트 1200px ──────────────────────────┐
│ ┌─ 메인 (800px) ─┐ ┌─ 사이드바 (400px) ─┐│
│ │  카드 컴포넌트   │ │  카드 컴포넌트      ││
│ │  → 가로 레이아웃 │ │  → 가로 레이아웃?   ││
│ └────────────────┘ └───────────────────┘│
└──────────────────────────────────────────┘

뷰포트가 1200px이면 두 카드 모두 "데스크톱 스타일"이 적용되지만, 사이드바의 카드는 400px 공간에 있으니 세로 레이아웃이 더 적절합니다. Media Query로는 이걸 해결하기 어렵습니다.

@container 기본 사용법

Container Query는 ** 부모 컨테이너의 크기 **를 기준으로 스타일을 전환합니다.

CSS
/* 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 값

CSS
.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 */
}

실전 예시 — 재사용 가능한 카드 컴포넌트

CSS
/* 카드를 감싸는 컨테이너 */
.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 단위

컨테이너 크기를 기준으로 한 상대 단위도 있습니다.

CSS
@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 — 해상도별 이미지 후보

HTML
<!-- 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="풍경 사진"
>

동작 원리:

  1. sizes로 이미지가 차지할 레이아웃 너비를 브라우저에 알림
  2. 브라우저가 뷰포트 크기와 디바이스 픽셀 비율(DPR)을 고려해서 최적 이미지 선택
  3. 2배 레티나(DPR=2) 디바이스에서 뷰포트 400px → 실제 필요한 건 800w 이미지
HTML
<!-- x 서술자: 디바이스 픽셀 비율 기준 -->
<img
  src="logo.png"
  srcset="
    logo.png    1x,
    logo@2x.png 2x,
    logo@3x.png 3x
  "
  alt="로고"
>

picture 태그 — 아트 디렉션

srcset은 같은 이미지의 크기만 다르지만, <picture>완전히 다른 이미지 를 보여줄 수 있습니다.

HTML
<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. 브레이크포인트 설계 전략

대표적인 브레이크포인트

특정 디바이스 크기에 맞추기보다는, 콘텐츠가 깨지는 지점을 기준으로 잡는 게 좋습니다. 그래도 참고할 수 있는 일반적인 기준은:

CSS
/* 모바일 퍼스트 기준 */
/* 기본: 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 변수로 관리

브레이크포인트를 직접 사용하기보다, 전체적인 접근 전략을 세우는 게 중요합니다.

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;
}

디바이스가 아니라 콘텐츠 기준으로

CSS
/* ❌ 디바이스 기준 — "아이폰은 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()로 미세한 크기 조절을 자동화하는 — 이 세 가지 조합이 모던 반응형의 핵심이라고 생각합니다.

댓글 로딩 중...