Frontend/CSS

CSS의 판도를 바꾸는 게임 체인저: :has() 선택자와 컨테이너 쿼리 실무 가이드

미니임 2026. 3. 2. 01:15

 

 

웹 레이아웃의 역사는 언제나 "부모가 자식의 상태에 반응할 수 있는가?"와 "브라우저 크기가 아닌, 컴포넌트 자체의 크기에 반응할 수 있는가?"라는 두 가지 숙원 사업을 해결하기 위해 달려왔습니다. 과거에는 이를 위해 무거운 JavaScript 로직을 동원해야 했지만, 이제 현대 CSS는 :has() 선택자와 **컨테이너 쿼리(Container Queries)**를 통해 선언적인 해결책을 제시합니다.


1. CSS의 '부모 선택자', :has() Deep Dive

그동안 CSS는 부모에서 자식으로 흐르는 단방향 구조였습니다. 하지만 :has()의 등장으로 우리는 드디어 특정 자식 요소를 포함하거나, 특정 상태에 있는 자식을 가진 부모 요소를 타겟팅할 수 있게 되었습니다.

작동 원리와 비유

:has()는 일종의 **'조건부 필터'**입니다.

비유하자면: 식당 예약 시스템에서 "손님 전체"를 조회하는 것이 아니라, "메뉴 중 '알레르기 유발 성분'이 포함된 음식을 주문한 테이블"만 골라내어 빨간색 식탁보를 까는 것과 같습니다.


2. 실전 Hands-on: 다이나믹 이커머스 카드 UI

사용자가 상품 카드의 '장바구니 담기' 버튼을 눌렀을 때, 카드 전체의 테두리 색상을 변경하고 배지를 표시하는 로직을 CSS만으로 구현해 보겠습니다.

HTML
 
<div class="product-card">
  <img src="product.jpg" alt="상품 이미지">
  <div class="content">
    <h3>프리미엄 기계식 키보드</h3>
    <label class="cart-checkbox">
      <input type="checkbox" class="add-to-cart">
      <span>장바구니 담기</span>
    </label>
  </div>
</div>
CSS
 
/* 기본 카드 스타일 */
.product-card {
  border: 1px solid #ddd;
  transition: all 0.3s ease;
  padding: 1rem;
  border-radius: 12px;
}

/* 핵심 로직: 
  .product-card 내부의 .add-to-cart가 체크된(checked) 상태라면 
  부모인 .product-card 자체의 스타일을 변경함 
*/
.product-card:has(.add-to-cart:checked) {
  border-color: #007bff;
  background-color: #f0f7ff;
  box-shadow: 0 4px 12px rgba(0, 123, 255, 0.1);
}

/* 체크 시에만 나타나는 가상 요소 비즈니스 로직 */
.product-card:has(.add-to-cart:checked)::before {
  content: "선택됨";
  position: absolute;
  top: 10px;
  right: 10px;
  background: #007bff;
  color: white;
  padding: 4px 8px;
  font-size: 12px;
  border-radius: 4px;
}

💡 트러블슈팅 팁

  • 브라우저 호환성: :has()는 최신 브라우저에서 광범위하게 지원되지만, Firefox는 121 버전부터 기본 지원을 시작했습니다. 구형 브라우저 대응이 필수라면 @supports 쿼리를 활용해 폴백(Fallback) 스타일을 지정하세요.
  • 성능 고려: 너무 복잡한 선택자 조합(예: :has(> div + span :nth-child(2)))은 렌더링 성능에 미세한 영향을 줄 수 있으므로 가급적 간결한 조건을 유지하는 것이 좋습니다.

3. 컨테이너 쿼리: 뷰포트를 넘어 컴포넌트 중심으로

기존 미디어 쿼리(@media)의 한계는 명확합니다. 화면 전체 너비($W_{viewport}$)를 기준으로 스타일을 정하다 보니, 동일한 컴포넌트가 사이드바에 있을 때와 메인 콘텐츠 영역에 있을 때 각각 다른 스타일을 적용하기가 매우 까다로웠습니다.

컨테이너 쿼리는 요소가 놓인 **부모 컨테이너의 너비($W_{container}$)**에 반응합니다.

실전 예제: 반응형 데이터 대시보드 위젯

CSS
 
/* 1. 부모 요소를 컨테이너로 정의 */
.dashboard-grid-item {
  container-type: inline-size;
  container-name: widget;
}

/* 2. 컨테이너 너비에 따른 내부 스타일 정의 */
.widget-content {
  display: flex;
  flex-direction: column; /* 기본은 세로 배열 (좁은 영역) */
  gap: 10px;
}

/* 컨테이너가 400px 이상일 때 가로 배열로 전환 */
@container widget (min-width: 400px) {
  .widget-content {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
  
  .widget-stats {
    font-size: 1.5rem; /* 넓은 곳에선 폰트도 크게 */
  }
}

4. 트레이드오프 (Trade-offs)

기능 장점 단점/한계
:has() JS 없이 복잡한 상태 관리 가능, 코드량 감소 선택자 복잡도 증가 시 디버깅 어려움
컨테이너 쿼리 진정한 의미의 컴포넌트 기반 설계 가능 container-type 설정 등 초기 구조 설계 필요
  • 성능: :has()는 DOM 트리가 변경될 때마다 스타일 재계산 범위가 넓어질 수 있습니다.
  • 복잡성: 너무 많은 로직을 CSS로 옮기면, 나중에 UI 로직을 추적할 때 CSS 파일을 뒤져야 하는 '스타일 시트의 비대화'를 초래할 수 있습니다.

결론: 선언적 UI의 시대

:has()와 컨테이너 쿼리는 단순히 편리한 기능을 넘어, 우리가 UI를 설계하는 철학 자체를 바꿉니다. 이제 "어떤 화면 크기인가?"를 묻기보다 **"이 컴포넌트가 놓인 환경이 어떠한가?"**와 **"자식의 상태가 부모에게 어떤 의미인가?"**를 고민해야 합니다.

반응형