Tailwind v4 @container — 사이드바에서도 그리드에서도 깨지지 않는 반응형 컴포넌트
반응형 UI 작업을 하다 보면 어느 순간 이런 벽에 부딪힙니다. 공들여 만든 카드 컴포넌트가 메인 피드에서는 멀쩍멀쩍 잘 동작하는데, 똑같은 코드를 사이드바에 가져다 놓으니 이미지와 텍스트가 뭉개지는 상황이요. 브레이크포인트를 더 잘게 쪼개봐도, 뷰포트를 기준으로 판단하는 미디어 쿼리는 "이 컴포넌트가 지금 얼마나 좁은 곳에 있는지"를 알 방법이 없으니까요.
저도 처음엔 "그냥 미디어 쿼리 더 세밀하게 써서 해결하면 되지 않나?" 했는데, 실제로 써보니 컴포넌트 설계 방식 자체가 달라지더라고요. 컨테이너 쿼리는 컴포넌트가 자신이 놓인 공간을 스스로 인식하게 해주는 CSS 기능으로, Tailwind v4부터 별도 플러그인 없이 핵심 API로 통합됐습니다. 무엇이, 어떻게 달라지는지 한번 같이 살펴봅시다.
이 글에서 다루는 내용
@container의 동작 원리와 미디어 쿼리와의 차이점- v4.0/v4.3의 변경 사항과
container-type: inline-sizevssize의 차이- 기본 카드 → 네임드 컨테이너 →
@max-*→ 대시보드 위젯까지 4가지 예시- 실무에서 실제로 발목 잡힌 함정 3가지
핵심 개념
컨테이너 쿼리가 동작하는 방식
작동 원리 자체는 단순합니다. 부모 요소에 @container 클래스를 붙여 쿼리 컨테이너를 선언하면, 브라우저가 그 요소의 크기를 자식에게 알려주는 컨텍스트가 형성됩니다. 그러면 자식은 @sm:, @md:, @lg: 같이 @ 접두사를 붙인 변형자로 그 컨테이너 너비에 반응할 수 있습니다.
<!-- 부모에 @container 선언 -->
<div class="@container">
<!-- 이 div의 직계 조상(@container)이 448px 이상이면 가로 배치 -->
<div class="flex flex-col @md:flex-row gap-4">
...
</div>
</div>뷰포트 vs 컨테이너:
md:flex-row는 브라우저 창이 768px 이상일 때 적용되고,@md:flex-row는 해당 요소를 감싸는@container요소의 너비가 448px 이상일 때 적용됩니다. 기준이 완전히 다릅니다.
여기서 처음 보면 헷갈리는 부분이 있는데, @md:의 기준값이 일반 md:와 다릅니다. 의도적으로 더 작은 값을 씁니다.
| 변형자 | 컨테이너 기준 | 미디어 쿼리 기준 |
|---|---|---|
@sm: / sm: |
384px | 640px |
@md: / md: |
448px | 768px |
@lg: / lg: |
512px | 1024px |
@xl: / xl: |
576px | 1280px |
컴포넌트는 보통 전체 뷰포트보다 훨씬 좁은 공간에 배치되니까 이게 맞는 방향입니다. 처음에 이걸 모르고 @lg:를 써놓고 "왜 안 되지?" 하고 한참 디버깅한 기억이 납니다.
v4에서 달라진 것들
v3 시절에는 @tailwindcss/container-queries 플러그인을 별도로 설치해야 했습니다. 버전별 변화를 정리하면 이렇습니다.
- v4.0 (2025년 1월): 컨테이너 쿼리 빌트인 통합.
@container,@max-*, 임의값(@[500px]:) 모두 플러그인 없이 사용 가능 - v4.1 (2025년 4월): 안정성 및 성능 개선
- v4.3.0 (2026년 5월):
container-size유틸리티 추가 — 이 버전부터 너비와 높이를 동시에 쿼리할 수 있습니다
한 가지 알아두면 좋은 게 있습니다. Tailwind의 @container 클래스는 내부적으로 container-type: inline-size를 설정합니다. 이 모드에서는 너비만 쿼리 기준으로 삼을 수 있고 높이는 제외됩니다. 높이 기반 쿼리가 필요하다면 v4.3.0에서 추가된 container-size 유틸리티를 써야 하는데, 이건 container-type: size를 설정해 너비와 높이 모두 쿼리 대상으로 만들어줍니다.
CSS Containment:
@container를 선언하면 해당 요소는 CSS Containment 컨텍스트를 형성합니다. 브라우저가 이 요소의 레이아웃을 격리해서 크기를 계산하고, 그 정보를 자식 요소에 전달합니다. 그래서 반드시 조상에@container가 있어야 자식의@md:같은 변형자가 동작합니다.container-type: inline-size(기본)는 가로 크기만,size는 가로·세로 모두 컨테이너 컨텍스트에 포함합니다.
브라우저 지원율도 충분합니다. Chrome 105+, Firefox 110+, Safari 16+ 기준 93% 이상이라 프로덕션 도입에 큰 부담이 없는 시점입니다.
실전 적용
이 개념이 실무에서 어떻게 쓰이는지, 가장 단순한 예시부터 시작해 복잡도를 조금씩 높여가며 살펴봅시다.
예시 1: 기본 컨테이너 쿼리 — 가장 단순한 형태
처음 컨테이너 쿼리를 도입할 때는 이름도 임의값도 없이, 그냥 @container와 @md:flex-row만으로 시작할 수 있습니다.
<div class="@container">
<div class="flex flex-col @md:flex-row gap-4 p-4 bg-white rounded-lg shadow">
<img
class="w-full rounded-lg object-cover"
src="/product.jpg"
alt="상품 이미지"
>
<div>
<h3 class="text-lg font-bold">상품명</h3>
<p class="text-sm text-gray-600">상품 설명이 들어갑니다.</p>
</div>
</div>
</div>컨테이너가 448px(@md:) 미만이면 세로로 쌓이고, 이상이면 가로로 펼쳐집니다. 이 카드를 좁은 사이드바에 놓든 넓은 메인 영역에 놓든, 공간에 맞게 스스로 조정됩니다. 미디어 쿼리로는 구현하기 까다로웠던 동작이죠.
예시 2: 네임드 컨테이너로 실무 카드 완성
실무에서 가장 자주 만나는 패턴입니다. 같은 카드가 3열 그리드에도, 좁은 사이드바에도, 모달 안에도 들어가야 하는 상황이요. 컨테이너에 이름을 붙이면 임의 브레이크포인트와 함께 훨씬 정밀하게 제어할 수 있습니다.
<div class="@container/card">
<div class="flex flex-col @[480px]/card:flex-row @[750px]/card:items-center gap-4 p-4">
<img
class="w-full @[480px]/card:w-32 rounded-lg object-cover"
src="/product.jpg"
alt="상품 이미지"
>
<div class="flex-1">
<h3 class="text-lg @[750px]/card:text-2xl font-bold">상품명</h3>
<p class="text-sm text-gray-600">상품 설명이 들어갑니다.</p>
</div>
</div>
</div>| 클래스 | 역할 |
|---|---|
@container/card |
card라는 이름의 쿼리 컨테이너 선언 |
@[480px]/card:flex-row |
card 컨테이너가 480px 이상이면 가로 배치 |
@[750px]/card:items-center |
750px 이상이면 세로 중앙 정렬 추가 |
@[480px]/card:w-32 |
컨테이너가 넓을 때만 이미지 고정 너비 적용 |
@[750px]/card:text-2xl |
750px 이상이면 큰 제목 크기 |
이 컴포넌트를 어느 레이아웃에 갖다 놓더라도 CSS를 건드릴 필요가 없습니다. 이름(/card)을 붙이는 게 귀찮아 보이지만, 중첩 구조가 생기면 이 이름이 빛을 발합니다.
<!-- 외부 컨테이너 -->
<div class="@container/main">
<!-- 내부 중첩 컨테이너 -->
<aside class="@container/sidebar">
<nav class="flex flex-col @sm/main:flex-row @sm/sidebar:flex-col">
<!-- /main을 기준으로도, /sidebar를 기준으로도 독립적으로 반응 -->
</nav>
</aside>
</div>@container/{name} + @{size}/{name}: 패턴으로 원하는 조상 컨테이너를 정확히 지정할 수 있습니다. 대시보드처럼 레이아웃이 복잡한 페이지에서 이 패턴이 특히 유용합니다.
예시 3: @max-*로 좁을 때를 먼저 처리하기
솔직히 처음엔 @max-* 없이 모바일 퍼스트로만 써보려고 했는데, 이미 기본값을 큰 쪽으로 잡아둔 컴포넌트에 덮어쓸 때 역방향 쿼리가 훨씬 읽기 편하더라고요.
@max-*는 "이 컨테이너가 특정 크기 미만일 때"를 표현합니다. 모바일 퍼스트(@sm:, @md:로 점점 키워나가기)와 반대 방향인데, 선택 기준은 간단합니다. 기본 상태를 넓은 레이아웃으로 정의했고 좁을 때만 예외 처리를 하고 싶다면 @max-*가 더 읽기 쉽습니다. 반대로 기본 상태가 좁은 쪽이라면 @sm:, @md:를 쌓아올리는 게 자연스럽습니다.
<div class="@container">
<!-- 컨테이너가 448px 미만이면 작은 텍스트, 이상이면 기본 크기 -->
<p class="@max-md:text-sm text-base leading-relaxed">
컨테이너 크기에 따라 폰트가 달라집니다.
</p>
<!-- 좁을 때만 단순화되는 버튼 레이블 -->
<button class="@max-sm:px-2 @max-sm:text-xs px-4 py-2 text-sm bg-blue-500 text-white rounded">
<span class="@max-sm:hidden">지금 구매하기</span>
<span class="@sm:hidden">구매</span>
</button>
</div>예시 4: 대시보드 위젯 — 컨테이너가 배치를 스스로 결정할 때
이 패턴을 처음 써봤을 때 "아, 이래서 컨테이너 쿼리가 필요했구나" 싶었습니다. 같은 위젯이 그리드에서 1열을 차지할 때와 2열을 차지할 때 전혀 다른 레이아웃이 필요한데, 페이지 레이아웃 코드를 건드리지 않고 위젯 스스로 결정하게 만들 수 있거든요.
<!-- 그리드에서 1열이면 좁게, 2열이면 넓게 -->
<div class="@container/widget col-span-1 md:col-span-2">
<div class="flex flex-col @lg/widget:flex-row gap-4 p-6 bg-white rounded-xl shadow">
<!-- 요약 수치 영역 -->
<div class="@lg/widget:w-1/3 bg-blue-50 rounded-lg p-4">
<p class="text-4xl font-bold text-blue-600">1,284</p>
<p class="text-sm text-gray-500 mt-1">이번 달 방문자</p>
<p class="text-xs text-green-600 mt-2">↑ 12% 지난달 대비</p>
</div>
<!-- 상세 데이터 테이블 -->
<div class="@lg/widget:w-2/3">
<table class="w-full text-sm">
<thead>
<tr class="border-b text-left text-gray-500">
<th class="pb-2">페이지</th>
<th class="pb-2">방문수</th>
<th class="pb-2">이탈률</th>
</tr>
</thead>
<tbody>
<tr class="border-b">
<td class="py-2">/home</td>
<td>512</td>
<td>34%</td>
</tr>
<tr class="border-b">
<td class="py-2">/docs</td>
<td>389</td>
<td>21%</td>
</tr>
<tr>
<td class="py-2">/pricing</td>
<td>383</td>
<td>45%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>col-span-1일 때(위젯이 좁음)는 수치와 테이블이 세로로 쌓이고, col-span-2로 넓어지면 /widget 컨테이너가 @lg 기준(512px)을 넘는 순간 자동으로 옆으로 펼쳐집니다. 페이지 레이아웃 코드는 그리드 열 수만 결정하고, 위젯 내부 배치는 위젯이 전담합니다.
장단점 분석
직접 써본 소감으로는, 컨테이너 쿼리는 "컴포넌트 재사용이 잦은 프로젝트"에서 효과가 가장 극적으로 나타납니다. 반면 단점 중에서 실무에서 실제로 발목 잡혔던 건 두 가지였습니다 — 높이 쿼리 우회와 중첩 컨테이너 디버깅이요.
장점
- 컴포넌트 이식성: 사이드바·그리드·모달 어디에 놓여도 동일하게 동작
- 플러그인 불필요: v4부터 빌트인 — 별도 패키지 설치·유지보수 부담 없음
@max-*지원: 역방향(최대 너비 기준) 쿼리도 클래스 한 줄로 처리- 네임드 컨테이너: 중첩 구조에서 원하는 조상 컨테이너를 정확히 타겟팅
- 미디어 쿼리와 공존:
@media와@container는 독립 CSS 규칙으로 충돌 없이 혼용 가능
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 높이 쿼리 변형자 미지원 | @container(기본)는 container-type: inline-size라 너비만 쿼리 가능 |
v4.3.0의 container-size 클래스로 container-type: size 설정 후 @[height>200px]: 임의 변형자로 우회 |
| 컨테이너 선언 필수 | 반드시 조상에 @container가 있어야 동작 |
래퍼 div 추가 감수 또는 레이아웃 구조 재설계 |
| 디버깅 복잡도 | 중첩 컨테이너가 많으면 어느 기준인지 추적 어려움 | 네이밍 철저히, Chrome DevTools 컨테이너 패널 활용 |
| 구형 브라우저 미지원 | Chrome 105+ 등 모던 브라우저만 지원 | 레거시 지원 필요 시 폴리필 또는 점진적 적용 검토 |
실무에서 가장 흔한 실수
@container없이@md:쓰기 — 조상에@container가 없으면 변형자가 그냥 무시됩니다. 적용이 안 된다 싶으면 부모 트리를 먼저 확인해보시면 좋습니다.md:와@md:의 브레이크포인트 혼동 —md:는 768px,@md:는 448px입니다. 둘 다 쓰는 컴포넌트에서 값이 다르다는 걸 잊으면 예상치 못한 레이아웃 깨짐이 생깁니다.- 모든 반응형을 컨테이너 쿼리로 대체하려는 시도 — 헤더 높이, 사이드바 존재 여부처럼 페이지 전체 구조를 결정하는 건 여전히 미디어 쿼리가 자연스럽습니다. "페이지 레이아웃은
md:·lg:, 컴포넌트 내부는@md:·@lg:"로 역할을 나누는 게 유지보수에 훨씬 편합니다.
마치며
컨테이너 쿼리는 컴포넌트가 자신이 놓인 공간을 스스로 인식하게 해줌으로써, 재사용성과 이식성의 근본적인 한계를 해결합니다. Tailwind v4에서 별도 설치 없이 쓸 수 있게 됐고, 브라우저 지원도 충분하니 지금 시작해도 좋은 시점입니다.
지금 바로 시작해볼 수 있는 3단계:
-
Tailwind를 v4로 올려볼 수 있습니다.
공식 마이그레이션 문서를 참고하시면 v3 → v4 변경사항을 단계적으로 적용할 수 있습니다. -
가장 많이 재사용되는 카드·리스트 아이템 하나에
@container를 감싸보시면 좋습니다.
Chrome DevTools의 컨테이너 쿼리 패널에서 컨테이너 경계를 시각적으로 확인하면서 실험하면 훨씬 빠르게 감이 잡힙니다. -
사이드바나 모달처럼 너비가 달라지는 컨텍스트에 같은 컴포넌트를 배치해볼 수 있습니다.
CSS 한 줄 수정 없이 레이아웃이 알아서 적응하는 순간, 왜 이 패러다임이 주목받는지 체감이 됩니다.
참고 자료
- Tailwind CSS v4.0 공식 블로그 | tailwindcss.com
- Responsive design 공식 문서 | tailwindcss.com
- Tailwind CSS v4 Container Queries: Modern Responsive Design | SitePoint
- Component-First Responsive Design with Tailwind v4 | Kickstage
- Container Queries in Tailwind CSS | Frontend Notes
- Replace Complex Media Queries With Tailwind Container Queries | Strapi
- Build Smarter Responsive UIs with Tailwind CSS 4 Container Queries | Medium
- CSS Container Queries: A Practical Guide 2026 | Mantlr
- GitHub — tailwindlabs/tailwindcss-container-queries (구 플러그인)