CSS Container Query 실전: 화면 크기가 아니라 컴포넌트 크기에 반응하는 반응형 UI 만들기
반응형 기준은 viewport만이 아니다
오랫동안 반응형 웹은 viewport media query 중심으로 설계됐습니다. 화면 너비가 768px보다 작으면 모바일 레이아웃, 1024px보다 크면 데스크톱 레이아웃 같은 방식입니다. 하지만 현대 UI는 같은 컴포넌트가 사이드바, 카드 그리드, 모달, 대시보드 패널 안에서 서로 다른 크기로 렌더링됩니다. 화면은 넓어도 컴포넌트가 들어간 영역은 좁을 수 있습니다.
Container Query는 이 문제를 해결합니다. 컴포넌트가 viewport가 아니라 자신을 감싸는 컨테이너 크기에 반응하게 합니다. 같은 ProductCard라도 280px 폭에서는 세로 레이아웃, 520px 폭에서는 이미지와 텍스트를 가로 배치할 수 있습니다. 컴포넌트가 놓이는 위치와 상관없이 자신의 공간에 맞게 동작합니다.
이 글에서는 Container Query를 실제 디자인 시스템에 적용하는 기준을 정리합니다. container-type을 어디에 지정할지, media query와 어떻게 함께 쓸지, 카드/툴바/테이블 같은 컴포넌트에서 어떤 패턴이 유용한지 살펴봅니다.

1. 컨테이너를 명시해야 query가 동작한다
Container Query를 쓰려면 부모 요소에 container-type을 지정해야 합니다. 보통 inline-size를 사용해 가로 크기 기준으로 반응하게 만듭니다. 예를 들어 카드 래퍼에 container-type: inline-size를 주고, 내부 요소에서 @container (min-width: 420px) 조건을 사용할 수 있습니다.
중요한 점은 모든 요소를 컨테이너로 만들 필요가 없다는 것입니다. 너무 많은 컨테이너를 지정하면 스타일 흐름이 복잡해집니다. 컴포넌트가 실제로 자신의 폭에 따라 레이아웃을 바꿔야 하는 경계에만 지정합니다. 카드 리스트의 각 카드, 사이드 패널, 위젯 영역처럼 재사용 컴포넌트의 외곽이 좋은 후보입니다.
Container Query는 부모의 스타일에 의존하므로 컴포넌트 문서화가 중요합니다. 어떤 요소가 container가 되어야 하는지, 최소/최대 폭에서 어떤 레이아웃이 보장되는지 디자인 시스템 문서에 적어두면 재사용 시 깨지는 일을 줄일 수 있습니다.
2. Media Query와 경쟁하지 말고 역할을 나눈다
Container Query가 Media Query를 완전히 대체하지는 않습니다. 페이지 전체 구조, 내비게이션 표시 방식, viewport 기반 여백 조정은 여전히 media query가 적합합니다. 반면 카드 내부 배치, 버튼 그룹 줄바꿈, 위젯 밀도 조정은 container query가 적합합니다. 기준은 "화면 전체 조건인가, 컴포넌트가 받은 공간 조건인가"입니다.
예를 들어 대시보드 페이지는 viewport가 넓을 때 3열 그리드로 바뀔 수 있습니다. 이 결정은 페이지 레이아웃이므로 media query가 자연스럽습니다. 하지만 각 위젯 내부에서 차트 범례를 오른쪽에 둘지 아래에 둘지는 위젯의 실제 폭에 따라 달라져야 하므로 container query가 낫습니다.
이 역할 분리가 잘 되면 컴포넌트 재사용성이 좋아집니다. 같은 컴포넌트를 좁은 사이드바와 넓은 본문에 동시에 배치해도 내부 레이아웃이 각자 맞게 변합니다. 반대로 viewport 기준만 쓰면 넓은 화면의 좁은 컴포넌트가 데스크톱 레이아웃을 강제로 적용받아 깨질 수 있습니다.
3. 컴포넌트 밀도를 단계적으로 설계한다
Container Query는 단순히 모바일/데스크톱 두 단계로 나누는 것보다 밀도 조절에 강합니다. 240px 이하에서는 보조 설명을 숨기고, 360px 이상에서는 메타 정보를 보여주고, 520px 이상에서는 액션 버튼을 가로로 배치하는 식입니다. 컴포넌트가 가진 정보 우선순위를 반영할 수 있습니다.
다만 너무 많은 breakpoint는 유지보수를 어렵게 합니다. 컴포넌트마다 2~3개의 의미 있는 전환점을 정하는 것이 좋습니다. 전환점은 디바이스 크기가 아니라 콘텐츠가 답답해지는 지점에서 잡아야 합니다. 제목이 두 줄을 넘어가거나, 버튼이 줄바꿈되는 실제 폭을 보고 결정합니다.
테이블과 리스트도 유용한 대상입니다. 좁은 컨테이너에서는 덜 중요한 컬럼을 숨기고, 넓어지면 다시 보여줄 수 있습니다. 하지만 접근성과 정보 손실을 고려해야 합니다. 숨긴 정보가 반드시 필요한 경우 상세 패널이나 확장 행으로 접근할 수 있게 해야 합니다.
4. 실무 체크리스트
- 컴포넌트가 viewport가 아니라 할당된 폭에 따라 깨지는 사례가 있는가
- container-type을 컴포넌트 외곽의 안정적인 요소에 지정했는가
- media query와 container query의 역할이 문서화되어 있는가
- breakpoint가 콘텐츠가 실제로 깨지는 지점을 기준으로 정해졌는가
- 숨기는 정보가 사용자에게 필요한 경우 대체 접근 경로가 있는가
- Storybook이나 테스트 페이지에서 좁은/중간/넓은 컨테이너를 모두 확인했는가
- 컨테이너 지정이 중첩되어 예측하기 어려운 스타일을 만들지 않는가
5. 디자인 토큰과 함께 써야 유지보수된다
Container Query를 컴포넌트마다 임의 breakpoint로 쓰기 시작하면 디자인 시스템이 금방 흩어집니다. 어떤 카드는 380px에서 바뀌고, 어떤 패널은 412px에서 바뀌고, 어떤 리스트는 450px에서 바뀌면 일관된 경험을 만들기 어렵습니다. 모든 breakpoint를 통일할 필요는 없지만, small, medium, wide 같은 의미 있는 토큰을 정의하면 관리가 쉬워집니다.
토큰은 픽셀 값보다 의도를 표현해야 합니다. compact는 보조 정보를 숨기는 폭, comfortable은 주요 정보와 액션을 함께 보여주는 폭, expanded는 비교나 보조 패널을 노출할 수 있는 폭처럼 정의할 수 있습니다. 컴포넌트는 이 토큰을 기준으로 밀도를 조절하고, 필요한 경우에만 예외를 둡니다.
간격, 글자 크기, 이미지 비율도 함께 관리해야 합니다. 컨테이너가 넓어졌다고 모든 값을 키우면 카드가 과하게 커질 수 있습니다. 반응형 변화는 정보 구조를 바꾸는 것이지 무조건 확대하는 것이 아닙니다. 텍스트 계층, 액션 위치, 미디어 비율을 함께 조정해야 자연스럽습니다.
6. Grid와 Container Query를 조합한다
페이지 레이아웃은 CSS Grid가 담당하고, 컴포넌트 내부 적응은 Container Query가 담당하는 조합이 강력합니다. 예를 들어 대시보드 그리드는 화면 폭에 따라 1열, 2열, 3열로 바뀌고, 각 위젯은 자신이 받은 칸의 폭에 따라 내부 레이아웃을 바꿉니다. 이렇게 하면 같은 위젯이 넓은 메인 영역과 좁은 사이드 영역에서 모두 자연스럽게 보입니다.
auto-fit, minmax와 함께 쓰면 카드 그리드도 유연해집니다. 컨테이너 폭이 충분할 때 카드가 넓어지고, 카드 내부는 container query로 이미지 위치나 버튼 배치를 바꿉니다. 이 방식은 viewport breakpoint만 쓰는 것보다 재사용성이 높습니다. 페이지가 바뀌어도 컴포넌트는 자신의 폭만 보고 판단하기 때문입니다.
주의할 점은 중첩 컨테이너입니다. 부모와 자식이 모두 container query를 사용하면 어떤 컨테이너 기준으로 동작하는지 헷갈릴 수 있습니다. container-name을 사용해 명시적으로 참조하면 복잡한 컴포넌트에서 예측 가능성이 좋아집니다. 이름 없는 container query는 간단한 카드에는 좋지만, 대규모 디자인 시스템에서는 명명 전략을 고려해야 합니다.
7. 접근성과 콘텐츠 우선순위를 함께 검토한다
좁은 컨테이너에서 정보를 숨기는 것은 흔한 패턴이지만, 숨긴 정보가 사용자에게 필요한지 검토해야 합니다. 예를 들어 가격, 상태, 오류 메시지, 마감일 같은 정보는 공간이 좁아도 사라지면 안 됩니다. 반면 보조 설명, 장식 이미지, 부가 메타 정보는 줄이거나 접을 수 있습니다. Container Query는 정보 우선순위를 코드로 표현하는 도구입니다.
키보드 탐색도 확인해야 합니다. 레이아웃이 바뀌면서 버튼 순서와 시각적 순서가 어긋나면 사용자는 혼란을 겪습니다. CSS order만으로 순서를 바꾸는 경우 DOM 순서와 포커스 순서를 점검해야 합니다. 좁은 컨테이너에서 메뉴가 접히면 aria-expanded, aria-controls 같은 상태도 정확히 연결되어야 합니다.
시각 회귀 테스트도 도움이 됩니다. 같은 컴포넌트를 280px, 420px, 640px 컨테이너에 넣은 스토리를 만들고 스냅샷을 비교하면 의도치 않은 줄바꿈과 겹침을 빨리 찾을 수 있습니다. Container Query는 브라우저 크기만 바꿔서는 충분히 테스트되지 않습니다.
8. 지원 브라우저와 폴백 전략을 확인한다
Container Query는 현대 브라우저에서 널리 사용할 수 있지만, 제품의 지원 범위는 항상 확인해야 합니다. 특정 사내 브라우저나 오래된 WebView를 지원해야 한다면 폴백이 필요할 수 있습니다. 폴백은 완전히 같은 경험을 제공하는 것이 아니라, 정보가 깨지지 않고 기본 레이아웃으로 사용 가능한 수준이면 충분한 경우가 많습니다.
@supports를 사용하면 container query 지원 여부에 따라 스타일을 나눌 수 있습니다. 기본 스타일은 단순한 세로 레이아웃으로 두고, 지원 브라우저에서만 컨테이너 기반 향상을 적용하는 방식이 안전합니다. 이를 progressive enhancement로 보면 도입 부담이 줄어듭니다.
디자인 시스템 문서에는 지원 브라우저와 폴백 동작을 함께 적어야 합니다. 컴포넌트 사용자가 "이 컴포넌트는 좁은 영역에서 자동으로 바뀐다"는 사실뿐 아니라, 지원하지 않는 환경에서 어떤 모습인지도 알아야 합니다. 반응형 컴포넌트의 품질은 예쁜 데모가 아니라 예외 환경에서 깨지지 않는 데서 결정됩니다.
9. 실제 콘텐츠로 폭을 검증한다
반응형 컴포넌트는 더미 텍스트로는 잘 동작해 보일 수 있습니다. 실제 서비스에는 긴 한국어 제목, 영문 URL, 숫자 단위, 상태 배지, 다국어 번역이 들어갑니다. Container Query breakpoint는 이런 실제 콘텐츠를 넣고 확인해야 합니다. 특히 긴 단어가 줄바꿈되지 않아 버튼이나 카드 밖으로 튀어나가는 문제를 봐야 합니다.
디자인 시스템에서는 각 컴포넌트에 최소 폭, 권장 폭, 긴 콘텐츠 사례를 함께 제공하는 것이 좋습니다. 카드 제목이 세 줄까지 허용되는지, 버튼은 줄바꿈되는지, 메타 정보는 숨겨지는지 기준이 있어야 합니다. 기준이 없으면 사용하는 화면마다 임시 CSS가 늘어나고 컴포넌트 일관성이 깨집니다.
컨테이너 기반 반응형은 컴포넌트가 똑똑해지는 만큼 테스트 경우도 늘어납니다. 좁은 폭, 중간 폭, 넓은 폭, 긴 텍스트, 액션 버튼 많은 상태를 스토리로 고정하면 회귀를 줄일 수 있습니다.
10. 레이아웃 책임을 컴포넌트 밖으로 새지 않게 한다
Container Query를 쓰는 이유는 컴포넌트가 자신이 받은 공간 안에서 스스로 적응하게 만들기 위해서입니다. 그런데 사용하는 페이지마다 내부 요소를 덮어쓰는 CSS를 추가하면 다시 결합도가 높아집니다. 컴포넌트는 외부에서 width와 placement만 받고, 내부 밀도와 줄바꿈은 자체 규칙으로 처리하는 편이 좋습니다.
이 원칙이 지켜지면 같은 컴포넌트를 카드, 사이드바, 모달, 대시보드에 배치해도 별도 수정이 줄어듭니다. 반응형 설계의 목표는 화면별 예외를 늘리는 것이 아니라 예외 없이 재사용 가능한 컴포넌트를 만드는 것입니다.
결론: 컴포넌트는 자신이 놓인 공간을 알아야 한다
반응형 UI는 더 이상 화면 크기 하나만으로 설명되지 않습니다. 같은 컴포넌트가 여러 레이아웃 안에서 재사용되는 시대에는 컴포넌트가 받은 공간에 맞춰 스스로 조정되어야 합니다. Container Query는 그 기준을 CSS 수준에서 제공합니다.
좋은 설계는 viewport media query와 container query를 함께 씁니다. 페이지 구조는 화면 기준으로, 컴포넌트 내부는 컨테이너 기준으로 다루면 더 견고하고 재사용 가능한 UI를 만들 수 있습니다.