반응형 웹 사이트를 제작할 때 UI 컴포넌트가 위치한 영역에 상관없이 크기가 자연스럽게 변하지 않아 고민해본 경험이 있을 것이다.
대부분 뷰포트 크기를 기준으로 작동하는 미디어 쿼리(@media)를 사용하지만, 사이드바 내부나 메인 콘텐츠 영역처럼 부모 요소의 너비에 맞춰 디자인을 변경해야 하는 상황에서는 기존 미디어 쿼리만으로 한계에 부딪힌다.
이번에는 미디어 쿼리의 구조적 단점을 완벽하게 해결하는 CSS Container Queries(@container)의 핵심 원리와 실무 적용 방법, 흔히 발생하는 실수까지 한눈에 정리해서 소개하겠다.
기존 미디어 쿼리는 오직 브라우저 창(Viewport)의 크기만 감지한다. 예를 들어 메인 영역에 있을 때는 넓게 보이고 사이드바에 있을 때는 좁게 보여야 하는 동일한 '카드 컴포넌트'가 있다고 가정해보자.
미디어 쿼리를 사용하면 뷰포트 너비만 확인하기 때문에 사이드바에 들어간 카드도 뷰포트가 크면 가로로 펼쳐져 디자인이 깨지게 된다. 이를 해결하려면 부모 클래스별로 일일이 스타일을 덮어씌워야 했다.
반면 CSS 컨테이너 쿼리(@container)는 화면 크기가 아니라 요소가 속한 '상위 부모 컨테이너'의 너비나 높이를 기준으로 반응형 스타일을 적용한다.
| 구분 | 미디어 쿼리 (@media) | 컨테이너 쿼리 (@container) |
|---|---|---|
| 기준 대상 | 브라우저 뷰포트(전체 화면) 너비 및 높이 | 상위 부모 요소(컨테이너)의 너비 및 높이 |
| 재사용성 | 배치 위치(메인/사이드바)에 따라 복잡한 예외 스타일 작성 필요 | 어느 위치에 배치하든 부모 너비에 맞춰 자동 대응 |
| 주요 활용처 | 전체 페이지 레이아웃 및 글로벌 브레이크포인트 설정 | 독립적인 UI 컴포넌트(카드, 프로필, 폼 요소 등) |
컨테이너 쿼리를 사용하려면 2가지 단계가 필요하다. 관찰 대상이 될 부모 요소를 컨테이너로 지정하고, 그 후 자식 요소에서 @container 조건문을 작성하는 방식이다.
부모 요소에는 container-type 속성을 지정해야 한다. 주로 사용하는 값은 inline-size로, 요소의 텍스트 방향 축(보통 너비) 변화를 감지한다. 필요에 따라 container-name 속성으로 특정 컨테이너에 이름을 부여할 수 있다.
실제 프로젝트에서 자주 쓰이는 프로필 카드 컴포넌트를 예로 들어보겠다. 좁은 컨테이너에서는 세로 배치, 넓은 컨테이너에서는 가로 배치로 변환하는 스타일 규칙을 작성한다.
뷰포트 크기만 기준 판단하기 때문에 좁은 사이드바에 컴포넌트가 위치해도 화면이 크면 가로로 배치되어 깨짐 현상이 발생한다.
/* 메인 뷰포트 크기만 판단하므로 위치에 따른 대응 불가 */
.card-wrapper {
width: 100%;
}
.card {
display: flex;
flex-direction: column;
gap: 16px;
}
@media (min-width: 768px) {
/* 뷰포트가 768px 이상이면 사이드바 내부 카드도 강제로 가로 배치됨 */
.card {
flex-direction: row;
}
}
부모 요소인 .card-wrapper의 너비를 감지하여 컴포넌트 자체가 위치한 공간에 따라 스스로 형태를 바꾼다.
/* 1. 부모 요소를 컨테이너로 선언 */
.card-wrapper {
container-type: inline-size;
container-name: card-box;
width: 100%;
}
/* 2. 기본 스타일 (좁은 공간: 세로 배치) */
.card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
/* 3. 컨테이너 너비가 450px 이상일 때 스타일 적용 */
@container card-box (min-width: 450px) {
.card {
flex-direction: row;
align-items: center;
}
.card-image {
width: 150px;
height: 150px;
object-fit: cover;
}
}결과: 동일한 HTML 코드를 메인 콘텐츠 영역(너비 800px)과 사이드바 영역(너비 300px)에 각각 넣어보면, 메인 영역의 카드는 가로로 배치되고 사이드바 영역의 카드는 자동으로 세로로 정렬된다.
컨테이너 쿼리를 실무에 적용할 때 개발자들이 흔히 범하는 대표적인 무한 루프 문제와 설정 오류를 파악해 두어야 한다.
✗ 잘못된 사용: 컨테이너로 지정된 자기 자신을 @container 내부에서 크기 변경하려는 경우
컨테이너 쿼리 내부에서 컨테이너 자체의 너비나 높이를 변경하면 무한 재계산 루프에 빠져 브라우저가 개입을 중단하거나 스타일 적용이 무시된다.
/* ✗ 자기 자신의 너비를 직접 조작하면 안 됨 */
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card-wrapper {
width: 300px; /* 루프 발생 원인 */
}
}✓ 올바른 사용: container-type이 선언된 부모의 내부 '자식 요소' 스타일만 조작해야 한다.
/* ✓ 컨테이너 내부 자식 요소의 스타일을 변경 */
@container (min-width: 400px) {
.card-wrapper .card-title {
font-size: 1.5rem;
}
}또한 container-type: size 규칙은 요소의 너비와 높이 모두를 무조건 고정 지정해야 계산이 가능하므로 실무에서는 레이아웃이 깨지기 쉽다. 대부분의 웹 레이아웃 반응형 작업에는 너비 축만 감지하는 container-type: inline-size를 선택하는 것이 안정적이다.
CSS Container Queries는 단순한 최적화 기법이 아니라 컴포넌트 중심의 모듈식 디자인을 완성하는 핵심 도구다. 뷰포트 중심의 구식 미디어 쿼리 방식에서 벗어나 부모 요소 기준의 컴포넌트 설계를 도입하면 코드 재사용성을 극대화할 수 있다. 이 글의 container-type 설정법과 자식 요소 타겟팅 방식을 참고하여 실무 UI 컴포넌트에 적용해 보면, 어떤 위치에서도 완벽하게 동작하는 유연한 인터페이스를 구축할 수 있을 것이다.