카드 컴포넌트를 만들었다. 메인 페이지에서는 3열 그리드로, 사이드바에서는 1열로, 모달 안에서도 쓰인다. 미디어 쿼리로 반응형을 잡았더니 금방 문제가 생겼다.
미디어 쿼리는 뷰포트 너비를 기준으로 한다. 그런데 카드의 너비는 뷰포트가 아니라 부모 컨테이너가 결정한다. 데스크톱 뷰포트(1440px)에서 사이드바 안의 카드는 좁은데, 미디어 쿼리는 "뷰포트가 넓으니까 가로 레이아웃으로 가자"고 판단한다.
/* 이게 문제다 */
@media (min-width: 768px) {
.card {
flex-direction: row; /* 뷰포트가 넓으면 가로 레이아웃 */
}
}사이드바(300px) 안에 있는 카드가 가로 레이아웃으로 렌더링되면서 텍스트가 찌그러진다. 뷰포트는 넓지만 카드가 놓인 공간은 좁으니까.
이 문제를 해결하려면 .sidebar .card, .modal .card 같은 컨텍스트별 스타일을 추가해야 한다. 컴포넌트가 자신이 어디에 놓이는지 알아야 한다는 뜻이다. 컴포넌트의 재사용성이 깨진다.
Container Queries로 해결하기
Container Queries는 뷰포트 대신 부모 컨테이너의 크기를 기준으로 스타일을 적용한다.
/* 부모를 컨테이너로 지정 */
.card-wrapper {
container-type: inline-size;
}
/* 컨테이너 너비에 따라 스타일 적용 */
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
@container (max-width: 399px) {
.card {
flex-direction: column;
}
}이제 카드가 어디에 놓이든 — 메인 그리드든, 사이드바든, 모달이든 — 자신이 차지하는 공간에 맞게 스스로 레이아웃을 결정한다.
실제 마이그레이션 과정
기존 미디어 쿼리 기반 컴포넌트를 Container Queries로 바꾸는 건 기계적인 작업은 아니었다. 사고방식을 바꿔야 했다.
미디어 쿼리 사고방식: "화면이 768px 이상이면 이 레이아웃"
Container Query 사고방식: "이 컴포넌트가 400px 이상의 공간을 가지면 이 레이아웃"
브레이크포인트의 기준이 달라진다. 뷰포트의 768px과 컨테이너의 400px은 완전히 다른 기준이다. 컴포넌트를 다양한 크기의 컨테이너에 넣어보면서 적절한 브레이크포인트를 새로 찾아야 했다.
Storybook이 여기서 도움이 됐다. 스토리 하나에 여러 너비의 래퍼를 만들어서 카드가 각 크기에서 어떻게 보이는지 한 눈에 확인했다.
export const ResponsiveCard: Story = {
render: () => (
<div style={{ display: 'flex', gap: '24px', flexWrap: 'wrap' }}>
{[200, 300, 400, 600].map((width) => (
<div key={width} style={{ width, containerType: 'inline-size' }}>
<p style={{ fontSize: 12, color: '#888' }}>{width}px</p>
<Card title="제목" description="설명..." />
</div>
))}
</div>
),
};container-type 선택
container-type에는 세 가지 값이 있다:
inline-size: 가로 방향만 쿼리 가능. 대부분 이걸 쓴다size: 가로·세로 모두 쿼리 가능. 높이 기반 레이아웃 변경이 필요할 때normal: 기본값. 컨테이너 쿼리 대상이 아님
size를 쓰면 브라우저가 해당 요소의 크기를 고정해야 해서 레이아웃 계산이 달라질 수 있다. 명시적으로 높이를 지정하지 않은 요소에 container-type: size를 넣으면 높이가 0이 될 수 있다. 대부분의 경우 inline-size로 충분하다.
Container Query Units
컨테이너 크기를 기준으로 한 단위도 사용할 수 있다.
.card-title {
font-size: clamp(14px, 3cqi, 24px);
}cqi는 컨테이너 인라인 크기의 1%다. 컨테이너가 좁으면 글자가 작아지고, 넓으면 커진다. clamp와 함께 쓰면 최소/최대를 제한할 수 있다.
| 단위 | 의미 |
|---|---|
cqw | 컨테이너 너비의 1% |
cqh | 컨테이너 높이의 1% |
cqi | 컨테이너 인라인 크기의 1% |
cqb | 컨테이너 블록 크기의 1% |
미디어 쿼리를 완전히 대체하나?
아니다. 페이지 전체 레이아웃(네비게이션 숨기기, 사이드바 접기)은 여전히 미디어 쿼리가 맞다. 이건 뷰포트에 대한 판단이니까.
경험적으로 이런 구분이 잘 맞았다:
- 미디어 쿼리: 페이지 레벨 레이아웃. 네비게이션, 사이드바, 전체 그리드 구조
- Container Queries: 컴포넌트 레벨 레이아웃. 카드, 리스트 아이템, 위젯
둘을 섞어 쓰면 된다. 페이지 레이아웃은 미디어 쿼리로 잡고, 그 안의 컴포넌트들은 Container Queries로 자율적으로 반응하게.
2026년 기준 모든 주요 브라우저에서 지원하니까, 폴리필 걱정 없이 프로덕션에 쓸 수 있다. 재사용 가능한 컴포넌트를 만든다면, Container Queries는 선택이 아니라 기본이 되어야 한다.
