테마 전환

Tailwind 반응형 레이아웃 실전: 컨테이너 쿼리와 브레이크포인트 전략

Easton editorial illustration: guided setup bench

들어가며

모니터에 보이는 그 카드 컴포넌트를 한번 바라보세요.

홈에서는 완벽합니다. 이미지는 왼쪽, 텍스트는 오른쪽에 있고 간격도 여유롭습니다. 하지만 사이드바로 복사하는 순간, 레이아웃 전체가 구겨진 종이처럼 망가집니다. 이미지는 가느다랗게 눌리고 텍스트는 엉망으로 줄바꿈됩니다.

대체 이 녀석은 자신이 어떤 모습이어야 하는지 어떻게 알 수 있을까요?

문제는 여기에 있습니다. 기존 반응형 디자인은 오직 ‘뷰포트’, 즉 브라우저 창의 크기만 인식해 컴포넌트의 형태를 바꿉니다. 하지만 컴포넌트는 자신이 얼마나 넓은 컨테이너 안에 들어갔는지 전혀 모릅니다. 넓은 단독주택에 살던 사람이 갑자기 30제곱미터짜리 원룸에 던져져 가구를 어떻게 배치해야 할지 모르는 것과 같습니다.

Tailwind의 컨테이너 쿼리는 바로 이 문제를 해결합니다. 컴포넌트가 브라우저 창만 멍하니 바라보는 대신 자신이 속한 컨테이너의 크기를 ‘인식’할 수 있게 해 줍니다.

이 글에서는 Tailwind 반응형 레이아웃에서 가장 실수하기 쉬운 두 부분, 즉 브레이크포인트 전략을 어떻게 선택하고 컨테이너 쿼리를 어떻게 사용하는지 살펴보겠습니다. 가능한 한 실제 코드를 많이 담았습니다. 문서를 백 번 읽는 것보다 직접 한 번 실행해 보는 편이 훨씬 확실하기 때문입니다.

반응형 디자인의 진화: 뷰포트에서 컨테이너로

미디어 쿼리: 오래된 방식의 오래된 문제

솔직히 말하면 저는 미디어 쿼리를 몇 년 동안 쓰면서 늘 꽤 괜찮은 도구라고 생각했습니다. 그러다 같은 카드 컴포넌트를 서로 완전히 다른 세 곳, 즉 홈의 카드 목록과 사이드바 추천 영역, 모달 안의 목록에 넣어야 했던 적이 있습니다.

그때 미디어 쿼리의 문제가 드러났습니다.

미디어 쿼리는 뷰포트 너비, 즉 브라우저 창의 크기를 기준으로 판단합니다. 하지만 컴포넌트가 정말 신경 써야 하는 것은 ‘내 부모, 아니 내 부모 컨테이너의 너비가 얼마인가?‘입니다.

예를 들어 다음과 같은 스타일을 작성했다고 가정해 보겠습니다.

/* 기존 미디어 쿼리 */
@media (min-width: 768px) {
  .card {
    flex-direction: row;
  }
}

이 코드는 브라우저 창이 768px보다 넓을 때 카드를 가로 레이아웃으로 바꾼다는 뜻입니다.

하지만 뷰포트는 1200px인데 사이드바 너비가 280px에 불과하다면 어떻게 될까요? 카드는 여전히 가로 레이아웃으로 바뀌고, 그 비좁은 사이드바 안에서 한 덩어리로 눌려 버립니다.

당시 화면은 마치 280mm 발을 240mm 신발에 억지로 밀어 넣은 것처럼 보였습니다.

컨테이너 쿼리: 관점 바꾸기

컨테이너 쿼리는 관점을 바꿉니다. ‘브라우저 창이 얼마나 큰가?‘가 아니라 ‘내 컨테이너가 얼마나 큰가?‘를 묻습니다.

/* 컨테이너 쿼리 */
@container (min-width: 400px) {
  .card {
    flex-direction: row;
  }
}

이 코드는 완전히 다른 의미를 갖습니다. 컨테이너 너비가 400px을 넘을 때 카드를 가로 레이아웃으로 바꿉니다.

‘그게 그렇게 대단한 차이인가?‘라는 생각이 들 수도 있습니다.

차이는 큽니다.

같은 카드 컴포넌트를 너비 600px의 콘텐츠 영역에 놓으면 자연스럽게 가로로 배치됩니다. 너비 280px의 사이드바에 놓으면 자동으로 세로로 쌓입니다. 두 가지 컴포넌트를 따로 작성하거나 props를 넘기고 여러 조건문을 만들 필요가 없습니다.

드디어 컴포넌트가 진정으로 ‘재사용 가능’해진 것입니다.

브라우저 지원 현황

컨테이너 쿼리는 2023년 무렵부터 주요 브라우저에서 폭넓게 지원되기 시작했습니다. 2024년 기준 Container Size Queries의 브라우저 지원율은 이미 90%를 넘었고 Chrome, Firefox, Safari, Edge가 모두 지원합니다.

프로젝트에서 오래된 브라우저까지 지원해야 한다면 조금 더 기다려야 할 수도 있습니다. 하지만 솔직히 지금은 대부분의 프로젝트에서 사용해도 괜찮습니다.

Tailwind CSS 브레이크포인트 시스템 자세히 알아보기

컨테이너 쿼리를 이야기하기 전에 먼저 Tailwind의 브레이크포인트 시스템을 확실히 알아야 합니다. 가장 기본이 되는 부분이기 때문입니다.

기본 브레이크포인트는 어떻게 구성될까

Tailwind는 기본적으로 다섯 가지 브레이크포인트를 제공합니다.

브레이크포인트 접두사최소 너비일반적인 상황
sm:640px큰 스마트폰 가로 화면
md:768px태블릿 세로 화면
lg:1024px태블릿 가로 화면/소형 노트북
xl:1280px데스크톱 모니터
2xl:1536px대형 모니터

이 숫자를 외우지 못해도 괜찮습니다. 자주 사용하다 보면 자연스럽게 익숙해집니다. 중요한 것은 Tailwind의 브레이크포인트가 ‘최소 너비’라는 점입니다. 즉 md:flex-row는 ‘뷰포트 너비가 768px 이상일 때 flex-row를 적용한다’는 뜻입니다.

모바일 우선: 작은 화면 스타일부터 작성하기

Tailwind의 브레이크포인트 시스템은 모바일 우선(mobile-first)입니다. 이것은 무슨 뜻일까요?

스타일을 작성할 때 기본값을 모바일 스타일로 두고, 브레이크포인트를 사용해 단계적으로 확장한다는 의미입니다.

<!-- 모바일 우선 방식 -->
<div class="flex flex-col md:flex-row lg:gap-8">
  <!-- 모바일: 세로로 쌓기 -->
  <!-- 태블릿(md): 가로 배치로 변경 -->
  <!-- 데스크톱(lg): 간격 확대 -->
</div>

이 방식의 장점은 스타일이 단순한 형태에서 복잡한 형태로 한 단계씩 쌓인다는 것입니다. 사용자가 오래된 스마트폰으로 접속한다면 가장 기본적인 스타일만 로드되므로 성능도 더 좋습니다.

반대로 ‘데스크톱 우선’으로 작성하려면 역방향 브레이크포인트를 잔뜩 써야 하므로 꽤 번거롭습니다. 그래서 특별한 요구 사항이 없다면 저는 곧바로 모바일 우선 방식을 사용합니다.

사용자 정의 브레이크포인트가 필요한 때

솔직히 대부분의 프로젝트는 기본 브레이크포인트만으로 충분합니다. 하지만 예외도 있습니다.

예전에 제가 작업했던 관리자 시스템에서는 UI 디자이너가 정한 브레이크포인트가 Tailwind 기본값과 전혀 맞지 않았습니다. 디자인에서 지정한 값은 480px, 720px, 960px, 1200px이었습니다.

이때는 tailwind.config.js를 수정해야 합니다.

// tailwind.config.js
module.exports = {
  theme: {
    screens: {
      'xs': '480px',   // 더 작은 브레이크포인트 추가
      'sm': '640px',
      'md': '720px',   // 기본값 덮어쓰기
      'lg': '960px',
      'xl': '1200px',
    }
  }
}

흔히 필요한 또 다른 사례는 sm보다 작은 브레이크포인트, 예를 들어 아주 작은 스마트폰 화면을 위한 기준입니다. 이때는 xs를 추가하면 됩니다.

screens: {
  'xs': '475px',   // 새로 추가
  'sm': '640px',
  // ... 나머지는 기본값 유지
}

의미 중심 브레이크포인트 이름에 관한 조언

저도 이 부분에서 실수한 적이 있습니다.

한번은 브레이크포인트 이름을 mobile, tablet, desktop으로 정했습니다. 직관적으로 들리지 않나요? 하지만 나중에 프로젝트에 폴더블 화면과 차량용 화면이 추가되자 이 이름들이 애매해졌습니다.

그 뒤로는 sm, md, lg 같은 크기 기호를 사용합니다. 특정 기기를 뜻하지 않고 오직 크기 단계만 나타내는 이름입니다. 이렇게 하면 앞으로 새로운 기기가 등장해도 브레이크포인트 이름이 낡지 않습니다.

컨테이너 쿼리 실전: 컴포넌트가 공간을 ‘인식’하게 만들기

이제 핵심으로 들어가겠습니다. 이 부분에서는 실제 코드로 컨테이너 쿼리 사용법을 보여 드리겠습니다.

1단계: 컨테이너 정의하기

컨테이너 쿼리를 사용하려면 먼저 브라우저에 해당 요소가 ‘컨테이너’라고 알려 줘야 합니다.

Tailwind에서는 @container 클래스 하나만 추가하면 됩니다.

<!-- 부모 컨테이너 -->
<div class="@container">
  <!-- 자식 요소는 컨테이너 크기에 따라 스타일을 조정할 수 있습니다 -->
  <div class="flex flex-col @sm:flex-row">
    ...
  </div>
</div>

@container를 추가하면 이 div가 쿼리 컨테이너가 됩니다. 자식 요소는 @sm, @md 같은 컨테이너 브레이크포인트로 스타일을 작성할 수 있습니다.

컨테이너 브레이크포인트 종류

Tailwind가 제공하는 컨테이너 브레이크포인트는 뷰포트 브레이크포인트와 조금 다릅니다.

컨테이너 브레이크포인트최소 너비
@xs320px
@sm384px
@md448px
@lg512px
@xl576px
@2xl672px
@3xl768px
@4xl896px
@5xl1024px

컨테이너 브레이크포인트 값이 뷰포트 브레이크포인트보다 훨씬 작다는 점을 눈치챘나요? 이것은 자연스러운 설계입니다. 컨테이너 자체가 페이지 안에 중첩되어 있으므로 너비가 뷰포트보다 클 수 없기 때문입니다.

실전 사례 1: 적응형 카드 컴포넌트

실제로 사용할 수 있는 카드 컴포넌트를 만들어 보겠습니다. 요구 사항은 다음과 같습니다.

  • 컨테이너 너비 < 384px: 세로로 쌓고 이미지는 전체 너비로 표시
  • 컨테이너 너비 >= 384px: 가로로 배치하고 이미지는 고정 너비로 표시
  • 컨테이너 너비 >= 512px: 이미지를 더 크게 표시하고 텍스트 줄 수 확대
<!-- 부모 컨테이너: 브라우저에 쿼리 컨테이너임을 알림 -->
<div class="@container p-4">
  <!-- 카드 컴포넌트 -->
  <article class="flex flex-col @sm:flex-row @lg:gap-6 bg-white rounded-lg shadow">
    <!-- 이미지: 컨테이너에 따라 너비 조정 -->
    <img
      src="https://example.com/image.jpg"
      alt="게시글 이미지"
      class="w-full @sm:w-32 @lg:w-48 h-48 @sm:h-32 @lg:h-36 object-cover rounded-t-lg @sm:rounded-l-lg @sm:rounded-tr-none"
    />

    <!-- 콘텐츠 영역 -->
    <div class="p-4 @sm:py-2 @lg:py-4 flex-1">
      <h3 class="text-base @lg:text-lg font-semibold mb-2">
        Tailwind 컨테이너 쿼리 입문 가이드
      </h3>
      <p class="text-sm text-gray-600 line-clamp-2 @lg:line-clamp-3">
        이 글에서는 Tailwind CSS의 컨테이너 쿼리 기능을 사용해 컴포넌트에 진정한 반응형 레이아웃을 구현하는 방법을 소개합니다...
      </p>
      <div class="mt-3 flex items-center text-xs text-gray-400">
        <span>2026-03-27</span>
        <span class="mx-2">·</span>
        <span>읽는 시간 5분</span>
      </div>
    </div>
  </article>
</div>

이 코드를 너비 280px의 사이드바에 넣으면 카드가 자동으로 간결한 세로 레이아웃으로 바뀝니다. 너비 600px의 콘텐츠 영역에 넣으면 여유로운 가로 레이아웃으로 바뀝니다.

코드는 전혀 수정할 필요가 없습니다.

실전 사례 2: 재사용 가능한 내비게이션 바

내비게이션 바는 컨테이너 쿼리의 가치를 가장 잘 보여 주는 사례 중 하나입니다.

동일한 내비게이션 컴포넌트가 다음 위치에 나타날 수 있습니다.

  • 상단 내비게이션 바(일반적으로 넓음)
  • 사이드바(좁을 수 있음)
  • 모바일 드로어 메뉴
<!-- 내비게이션 컴포넌트 -->
<nav class="@container">
  <ul class="flex flex-col @lg:flex-row @lg:items-center gap-2 @lg:gap-6">
    <li>
      <a href="/" class="block py-2 px-3 rounded hover:bg-gray-100">

      </a>
    </li>
    <li>
      <a href="/posts" class="block py-2 px-3 rounded hover:bg-gray-100">
        게시글
      </a>
    </li>
    <li>
      <a href="/about" class="block py-2 px-3 rounded hover:bg-gray-100">
        소개
      </a>
    </li>
  </ul>
</nav>

컨테이너 너비가 512px(@lg) 이상이면 내비게이션 항목이 자동으로 가로로 배치됩니다.

이렇게 하면 위치마다 별도의 스타일을 작성하지 않고도 동일한 내비게이션 컴포넌트를 어디에서나 재사용할 수 있습니다.

컨테이너 쿼리의 제약

컨테이너 쿼리가 만능은 아닙니다. 몇 가지 주의할 점이 있습니다.

1. 높이를 쿼리할 수 없음

현재 컨테이너 쿼리는 너비만 지원합니다. 높이 쿼리는 아직 명세 논의 단계입니다.

/* 이렇게 작성해도 동작하지 않습니다 */
@container (min-height: 400px) {
  /* 아쉽지만 현재는 지원되지 않습니다 */
}

2. 컨테이너가 크기 컨테이너여야 함

@container가 추가된 요소는 ‘크기 컨테이너’가 되며, 그 자식 요소가 해당 컨테이너를 쿼리할 수 있습니다. 자식의 자식 요소에 컨테이너 쿼리를 작성하면 직접적인 부모가 아니라 가장 가까운 컨테이너 조상을 쿼리할 수도 있습니다.

3. 너무 깊게 중첩하지 않기

컨테이너 쿼리의 중첩 단계가 많아지면 성능에 영향을 줄 수 있습니다. 저는 최대 2~3단계까지만 중첩하고, 그보다 깊어지면 리팩터링을 고려하는 편을 권합니다.

브레이크포인트 전략 선택: 미디어 쿼리 vs 컨테이너 쿼리

여기까지 읽었다면 ‘그래서 언제 미디어 쿼리를 쓰고 언제 컨테이너 쿼리를 써야 할까?‘라는 의문이 들 수 있습니다.

아주 좋은 질문입니다.

제가 정리한 간단한 판단 기준은 스타일이 무엇에 의존하는지 확인하는 것입니다.

결정 흐름

질문: 이 스타일은 무엇에 의존하는가?

├─ 뷰포트 크기에 의존 → 미디어 쿼리(md:, lg: 등) 사용
│   ├─ 페이지 전체 레이아웃
│   ├─ 전역 내비게이션, Header, Footer
│   ├─ Hero 영역, 전체 화면 광고
│   └─ 뷰포트의 특정 위치에 고정된 요소

└─ 컨테이너 크기에 의존 → 컨테이너 쿼리(@sm:, @lg: 등) 사용
    ├─ 재사용 컴포넌트(카드, 목록 항목)
    ├─ 사이드바 위젯
    ├─ 모달 내부 콘텐츠
    └─ 중첩 컴포넌트

미디어 쿼리를 사용하는 상황

페이지 단위 레이아웃: 페이지 전체의 그리드 구조에는 미디어 쿼리가 적합합니다.

<!-- 페이지 레이아웃: 뷰포트에 따라 열 수 조정 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <!-- 콘텐츠 카드 -->
</div>

이 레이아웃이 고려하는 것은 ‘브라우저 창이 얼마나 큰가?‘이며 컨테이너와는 관계가 없습니다.

전역 내비게이션: 상단 내비게이션 바의 펼치기와 접기는 뷰포트 너비에 따라 결정됩니다.

<!-- 모바일: 햄버거 메뉴, 데스크톱: 가로 내비게이션 -->
<header class="flex items-center justify-between px-4 py-3">
  <div class="logo">Logo</div>

  <!-- 모바일에서 숨기고 데스크톱에서 표시 -->
  <nav class="hidden md:flex gap-6">
    <a href="/">홈</a>
    <a href="/posts">게시글</a>
    <a href="/about">소개</a>
  </nav>

  <!-- 모바일에서 표시하고 데스크톱에서 숨김 -->
  <button class="md:hidden">
    <span class="sr-only">메뉴 열기</span>
    <!-- 햄버거 아이콘 -->
  </button>
</header>

컨테이너 쿼리를 사용하는 상황

재사용 컴포넌트: 서로 다른 너비의 컨테이너에서 사용하는 컴포넌트입니다.

예를 들어 게시글 카드는 다음 위치에 나타날 수 있습니다.

  • 홈 카드 목록(너비 약 300~400px)
  • 게시글 상세 페이지의 관련 추천(너비 약 250px)
  • 사이드바의 최신 게시글(너비 약 280px)

이런 상황에는 컨테이너 쿼리가 가장 적합합니다.

중첩 컴포넌트: 컴포넌트 안에 다른 컴포넌트가 들어가는 경우입니다.

<!-- 바깥 컨테이너 -->
<div class="@container w-full md:w-80">
  <!-- 안쪽 컨테이너 -->
  <div class="@container">
    <!-- 가장 안쪽 요소는 두 단계의 컨테이너에 따라 스타일을 조정할 수 있습니다 -->
    <div class="@sm:flex-row @lg:gap-4">
      ...
    </div>
  </div>
</div>

함께 사용하기

실제 프로젝트에서는 미디어 쿼리와 컨테이너 쿼리를 함께 사용하는 경우가 많습니다.

페이지 단위에는 미디어 쿼리를, 컴포넌트 단위에는 컨테이너 쿼리를 적용합니다.

<!-- 페이지 단위 레이아웃: 미디어 쿼리 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">

  <!-- 메인 콘텐츠 영역 -->
  <main class="md:col-span-2">
    <!-- 카드 컴포넌트: 컨테이너 쿼리 -->
    <div class="@container">
      <article class="flex flex-col @sm:flex-row">
        <!-- 카드 콘텐츠 -->
      </article>
    </div>
  </main>

  <!-- 사이드바 -->
  <aside class="@container">
    <!-- 사이드바 컴포넌트: 컨테이너 쿼리 -->
    <div class="@sm:grid-cols-2">
      <!-- 컴포넌트 콘텐츠 -->
    </div>
  </aside>

</div>

이렇게 작성하면 페이지의 전체 구조는 기기에 따라 조정되고, 컴포넌트 내부는 실제 사용 가능한 공간에 따라 조정됩니다. 두 단계의 반응형 설계가 각자의 역할을 담당합니다.

성능 최적화와 모범 사례

마지막으로 실제 작업에서 주의해야 할 점을 살펴보겠습니다.

컨테이너 쿼리의 성능 비용

솔직히 말해 컨테이너 쿼리에는 성능 비용이 있습니다. 브라우저가 컨테이너 크기를 추가로 계산하고 크기 변화도 감지해야 합니다.

하지만 이 비용은 실제로 아주 작습니다. 지나치게 중첩하지만 않는다면 말입니다.

저도 예전에 실수한 적이 있습니다. 목록의 모든 항목에 @container를 추가하고, 각 항목 내부에도 여러 단계의 컨테이너를 중첩했습니다. 그 결과 페이지를 스크롤할 때 약간의 끊김이 느껴졌습니다.

해결 방법은 간단합니다. 불필요한 컨테이너 단계를 줄이는 것입니다.

<!-- 권장하지 않음: 모든 단계에 @container 추가 -->
<div class="@container">
  <div class="@container">
    <div class="@container">
      <div class="@sm:flex-row">
        <!-- 너무 깊은 중첩 -->
      </div>
    </div>
  </div>
</div>

<!-- 권장: 필요한 곳에만 @container 추가 -->
<div class="@container">
  <div>
    <div>
      <div class="@sm:flex-row">
        <!-- 가장 바깥쪽만 컨테이너 -->
      </div>
    </div>
  </div>
</div>

컨테이너 쿼리를 지나치게 사용하지 않기

컨테이너 쿼리가 필요하지 않은 상황도 있습니다.

예를 들어 홈에서만 표시되는 Hero 영역은 너비가 거의 고정되어 있으므로 미디어 쿼리만으로 충분합니다. 여기에 굳이 @container를 적용하면 오히려 불필요한 작업이 됩니다.

제 원칙은 컴포넌트를 서로 다른 너비의 컨테이너에서 재사용해야 할 때만 컨테이너 쿼리를 사용하는 것입니다. 컴포넌트가 고정된 너비의 위치에만 나타난다면 미디어 쿼리로 충분합니다.

디버깅 팁

Chrome DevTools는 컨테이너 쿼리 디버깅을 지원하지만 진입 경로가 조금 깊숙이 숨어 있습니다.

DevTools를 열고 Elements 패널에서 @container가 있는 요소를 선택한 뒤 Styles 패널에서 @container 규칙을 찾습니다. 브레이크포인트 위에 마우스를 올리면 Chrome이 해당 컨테이너를 강조 표시합니다.

또 다른 방법은 Computed 패널에서 container-type을 검색하는 것입니다. 현재 요소가 쿼리 컨테이너인지 확인할 수 있습니다.

모범 사례 체크리스트

지금까지 시행착오를 겪으며 얻은 경험을 정리하면 다음과 같습니다.

  1. 컨테이너 단계 제한: @container는 최대 2~3단계까지만 중첩하고, 더 깊어지면 리팩터링을 고려합니다
  2. 필요할 때만 사용: 컴포넌트를 재사용해야 할 때만 컨테이너 쿼리를 사용하고, 고정된 상황에는 미디어 쿼리를 사용합니다
  3. 의미에 맞는 이름 사용: 브레이크포인트 이름은 크기 기호(sm/md/lg)를 사용하고 기기 이름(mobile/tablet)은 피합니다
  4. 복잡한 선택자 피하기: 컨테이너 쿼리 안의 선택자는 단순할수록 좋습니다. 복잡한 선택자는 성능에 영향을 줄 수 있습니다
  5. 애니메이션에서 사용하지 않기: 컨테이너 크기가 자주 변한다면 애니메이션에서 컨테이너 쿼리에 의존하지 않습니다

마지막으로, 코드를 작성할 때는 충분히 테스트하세요. 컴포넌트를 서로 다른 너비의 컨테이너에 넣어 결과를 확인하고 기본 레이아웃에서만 테스트하지 마세요. 많은 문제는 출시한 뒤에야 발견되며, 그때 수정하려면 훨씬 번거롭습니다.

마무리

이제 핵심 내용은 거의 모두 살펴봤습니다. 다시 한번 정리해 보겠습니다.

컨테이너 쿼리가 해결하는 문제는 컴포넌트가 뷰포트 너비만 바라보는 대신 실제 컨테이너 크기에 따라 레이아웃을 조정하게 하는 것입니다. 그래야 컴포넌트를 진정으로 재사용할 수 있습니다.

사용 원칙은 간단합니다.

  • 페이지 단위 레이아웃 → 미디어 쿼리(md:, lg:)
  • 재사용 컴포넌트 → 컨테이너 쿼리(@sm:, @lg:)

성능 면에서는 두 가지를 주의하세요. 너무 깊게 중첩하지 말고, 지나치게 사용하지 마세요.

지금 재사용해야 할 컴포넌트가 있다면 컨테이너 쿼리를 한번 적용해 보세요. 카드 컴포넌트부터 시작해 보면, 위치마다 중복 코드를 작성할 필요가 없다는 사실을 금방 체감할 수 있을 것입니다.

더 자세히 알고 싶다면 Tailwind 공식 문서의 Container Queries 항목이나 MDN의 CSS Container Queries를 참고하세요.

궁금한 점이 있다면 댓글로 이야기해 주세요.

Tailwind 컨테이너 쿼리로 반응형 컴포넌트 구현하기

Tailwind CSS 컨테이너 쿼리를 처음부터 사용해 컴포넌트가 컨테이너 크기에 따라 자동으로 레이아웃을 조정하도록 만듭니다

⏱️ Estimated time: 30 min

  1. 1

    Step 1: 부모 컨테이너에 @container 클래스 추가하기

    반응형 조정이 필요한 컴포넌트의 바깥 컨테이너를 찾아 `@container` 클래스를 추가합니다:

    ```html
    <div class="@container">
    <!-- 자식 요소에서 컨테이너 브레이크포인트를 사용할 수 있습니다 -->
    </div>
    ```

    이 단계는 브라우저에 해당 요소가 쿼리 컨테이너임을 알려 줍니다.
  2. 2

    Step 2: 자식 요소에 컨테이너 브레이크포인트 스타일 사용하기

    자식 요소에는 `@sm:`, `@md:`, `@lg:` 같은 컨테이너 브레이크포인트를 사용할 수 있습니다:

    ```html
    <div class="@container">
    <article class="flex flex-col @sm:flex-row @lg:gap-6">
    <!-- 컨테이너 < 384px: 세로 레이아웃 -->
    <!-- 컨테이너 >= 384px: 가로 레이아웃 -->
    <!-- 컨테이너 >= 512px: 간격 확대 -->
    </article>
    </div>
    ```

    컨테이너 브레이크포인트 값은 @xs(320px), @sm(384px), @md(448px), @lg(512px), @xl(576px) 등입니다.
  3. 3

    Step 3: 이미지와 텍스트 크기 조정하기

    컨테이너 너비에 따라 이미지 크기와 텍스트 스타일을 동적으로 조정합니다:

    ```html
    <img class="w-full @sm:w-32 @lg:w-48 h-48 @sm:h-32 object-cover" />
    <p class="text-sm @lg:text-base line-clamp-2 @lg:line-clamp-3">
    ```

    이미지는 작은 컨테이너에서는 전체 너비, 중간 컨테이너에서는 128px, 큰 컨테이너에서는 192px로 표시됩니다.
  4. 4

    Step 4: 미디어 쿼리와 컨테이너 쿼리 함께 사용하기

    페이지 단위 레이아웃에는 미디어 쿼리를, 컴포넌트 단위에는 컨테이너 쿼리를 사용합니다:

    ```html
    <!-- 페이지 레이아웃: 미디어 쿼리 -->
    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
    <main class="md:col-span-2">
    <!-- 컴포넌트: 컨테이너 쿼리 -->
    <div class="@container">
    <article class="flex flex-col @sm:flex-row">...</article>
    </div>
    </main>
    <aside class="@container">...</aside>
    </div>
    ```

    각각의 역할을 구분하면 혼란을 피할 수 있습니다.
  5. 5

    Step 5: 성능 최적화와 디버깅

    주의할 점:

    • 성능 문제를 피하려면 `@container`를 최대 2~3단계까지만 중첩합니다
    • 재사용이 필요한 컴포넌트에만 사용하고, 고정된 상황에는 미디어 쿼리를 사용합니다
    • Chrome DevTools에서 디버깅하려면 Elements → 컨테이너 요소 선택 → Styles 패널에서 @container 규칙을 확인합니다
    • Computed 패널에서 `container-type`을 검색하면 해당 요소가 컨테이너인지 확인할 수 있습니다

FAQ

컨테이너 쿼리와 미디어 쿼리는 무엇이 다른가요?
미디어 쿼리는 뷰포트(브라우저 창) 너비에 따라 스타일을 조정하므로 페이지 단위 레이아웃에 적합합니다. 컨테이너 쿼리는 요소가 속한 컨테이너의 너비에 따라 스타일을 조정하므로 재사용 컴포넌트에 적합합니다. 컨테이너 쿼리를 사용하면 위치마다 여러 스타일을 작성하지 않고도 컴포넌트를 어디서나 사용할 수 있습니다.
Tailwind 컨테이너 쿼리의 브라우저 지원 범위는 어느 정도인가요?
컨테이너 쿼리는 2023년에 주요 브라우저에서 폭넓게 지원되기 시작했습니다. 2024년 기준 Chrome, Firefox, Safari, Edge가 모두 지원하며 지원율은 90%를 넘습니다. 오래된 브라우저까지 지원해야 하는 프로젝트라면 먼저 대상 사용자층의 브라우저 버전을 확인하는 것이 좋습니다.
언제 컨테이너 쿼리를 쓰고 언제 미디어 쿼리를 써야 하나요?
간단한 판단 기준은 다음과 같습니다:

• 미디어 쿼리: 페이지 전체 레이아웃, 전역 내비게이션, Hero 영역, 뷰포트의 고정 위치에 있는 요소
• 컨테이너 쿼리: 재사용 컴포넌트(카드, 목록 항목), 사이드바 위젯, 모달 콘텐츠, 중첩 컴포넌트

실제 프로젝트에서는 두 방식을 자주 함께 사용합니다. 페이지 단위에는 미디어 쿼리를, 컴포넌트 단위에는 컨테이너 쿼리를 적용합니다.
Tailwind 컨테이너 브레이크포인트와 뷰포트 브레이크포인트의 값은 같은가요?
다릅니다. 컨테이너 브레이크포인트는 더 작은 값으로 구성되어 320px(@xs)부터 1024px(@5xl)까지입니다. 뷰포트 브레이크포인트는 640px(sm)부터 1536px(2xl)까지입니다. 컨테이너는 페이지 안에 중첩되어 뷰포트보다 넓을 수 없으므로 더 작고 세밀한 브레이크포인트가 필요합니다.
컨테이너 쿼리에 성능 문제가 있나요?
컨테이너 쿼리에는 약간의 성능 비용이 있지만 일반적으로 무시할 수 있는 수준입니다. 성능 문제는 주로 지나친 중첩에서 발생합니다. 모든 목록 항목에 @container를 추가하고 여러 단계로 중첩하면 스크롤할 때 끊김이 생길 수 있습니다. 컨테이너는 최대 2~3단계까지만 중첩하고, 실제로 재사용이 필요한 컴포넌트에만 사용하는 것이 좋습니다.
컨테이너 쿼리에서 높이 쿼리를 사용할 수 있나요?
현재는 지원되지 않습니다. CSS Container Queries 명세는 현재 너비 쿼리(container size queries for inline size)만 지원합니다. 높이 쿼리는 아직 명세 논의 단계이며 브라우저에 구현되지 않았습니다. 높이에 따라 스타일을 조정해야 한다면 당분간 기존 방식을 사용해야 합니다.
Chrome DevTools에서 컨테이너 쿼리를 어떻게 디버깅하나요?
DevTools를 열고 Elements 패널에서 @container가 있는 요소를 선택한 뒤 Styles 패널에서 @container 규칙을 찾습니다. 브레이크포인트 위에 마우스를 올리면 Chrome이 해당 컨테이너를 강조 표시합니다.

또 다른 방법은 Computed 패널에서 `container-type`을 검색해 요소가 쿼리 컨테이너인지 확인하는 것입니다.

3분 읽기 · 게시일: 2026년 3월 27일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog