블로그 목록
프론트엔드CSS

CSS Container Queries — 컴포넌트 기준 반응형 디자인으로 전환하기

미디어 쿼리로 반응형을 구현하다 보면 한계가 온다. 뷰포트 기준이라는 게 문제다. 같은 카드 컴포넌트가 메인 그리드에 들어갈 때와 사이드바에 들어갈 때 차지하는 공간이 다른데, 뷰포트는 둘 다 동일하다. 결국 특수케이스용 클래스를 따로 만들거나, 컴포넌트를 복제하게 된다.

Container Queries는 뷰포트가 아니라 부모 컨테이너의 크기를 기준으로 스타일을 조건부로 적용한다. 처음 제안이 나온 지 꽤 됐는데, 2023년쯤부터 주요 브라우저가 전부 지원하기 시작했고 지금은 부담 없이 쓸 수 있다.

기본 사용법

두 단계다. 부모에 컨테이너를 선언하고, 자식에서 @container로 조건을 건다.

.card-wrapper {
  container-type: inline-size;
}

@container (min-width: 480px) {
  .card {
    display: grid;
    grid-template-columns: 160px 1fr;
    gap: 16px;
  }
}

container-type: inline-size를 부모에 넣으면 그게 컨테이너가 된다. inline-size는 가로 방향만 쿼리 가능하고, size는 가로·세로 모두 가능하다. 대부분의 경우엔 inline-size로 충분하다.

이름을 붙이면 중첩된 컨테이너를 구분할 수 있다.

.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}

@container sidebar (min-width: 300px) {
  .sidebar-card { ... }
}

실제로 써보니

같은 ProductCard 컴포넌트를 홈 화면 그리드, 검색 결과, 관련 상품 슬라이더에 다 쓰는 상황이었다. 미디어 쿼리로는 세 곳이 다 같은 뷰포트 크기인데 레이아웃이 달라야 하는 걸 처리하기가 까다로웠다.

컨테이너 쿼리로 바꾸고 나서는 컴포넌트 코드가 깔끔해졌다. 어디에 놓이든 공간에 맞게 알아서 조정되니까.

.product-card-wrapper {
  container-type: inline-size;
}

.product-card {
  display: flex;
  flex-direction: column;
}

.product-card__image {
  aspect-ratio: 4/3;
  object-fit: cover;
}

@container (min-width: 400px) {
  .product-card {
    flex-direction: row;
    align-items: center;
  }

  .product-card__image {
    width: 140px;
    aspect-ratio: 1;
    flex-shrink: 0;
  }
}

Tailwind에서 쓰기

Tailwind v3.2부터 @tailwindcss/container-queries 플러그인을 지원한다. v4에서는 기본 내장됐다.

npm install @tailwindcss/container-queries
// tailwind.config.js (v3)
module.exports = {
  plugins: [require('@tailwindcss/container-queries')],
}

클래스는 @ 접두사를 붙인다.

<div class="@container">
  <div class="flex flex-col @[400px]:flex-row gap-4">
    <img class="w-full @[400px]:w-36 aspect-square object-cover" />
    <div class="flex-1">...</div>
  </div>
</div>

미디어 쿼리와 역할 분리

컨테이너 쿼리가 미디어 쿼리를 완전히 대체하는 건 아니다. 두 가지를 혼용하는 게 자연스럽다.

페이지 레이아웃 전환(사이드바 있는 레이아웃 ↔ 단일 컬럼), 전역 폰트 크기, 다크/라이트 모드 같은 건 여전히 미디어 쿼리가 맞다. 컴포넌트 내부에서 사용 가능한 공간에 따라 달라지는 레이아웃은 컨테이너 쿼리가 훨씬 적합하다.


처음엔 뭔가 복잡할 것 같았는데, 실제로는 미디어 쿼리 쓰는 것과 크게 다르지 않다. 부모에 container-type 하나 추가하고, @media 대신 @container 쓰면 된다. 컴포넌트를 재사용 가능하게 만들어야 한다면 한번쯤 시도해볼 만하다.