현대 CSS가 이렇게 강력해졌다: Container Queries, :has(), Cascade Layers 실전 활용

프론트엔드

CSSContainer QueriesCascade Layershas 선택자모던 CSS

이 글은 누구를 위한 것인가

  • 미디어 쿼리 기반 반응형 CSS의 한계를 느끼는 프론트엔드 개발자
  • Sass/SCSS 없이도 네이티브 CSS만으로 강력한 스타일링을 하고 싶은 분
  • 서드파티 CSS 라이브러리와 충돌하는 스타일 문제를 겪고 있는 팀

들어가며

"CSS가 드디어 쓸만해졌다."

몇 년 전까지만 해도 CSS는 한계가 명확한 언어였다. 부모 선택이 안 되고, 컴포넌트 자체 크기 기반 스타일링이 안 되고, 스타일 우선순위 충돌은 !important와 선택자 특이성 전쟁으로 해결해야 했다.

2024~2025년 사이 이 상황이 극적으로 바뀌었다. Container Queries, :has() 선택자, Cascade Layers, CSS Nesting이 모든 주요 브라우저(Chrome, Firefox, Safari, Edge)에서 지원되기 시작했다. Sass가 해결해주던 문제들을 이제 네이티브 CSS로 해결할 수 있다.


1. Container Queries: 부모 크기에 반응하는 컴포넌트

미디어 쿼리(Media Query)는 뷰포트(viewport) 크기를 기준으로 한다. 문제는 현실에서 컴포넌트가 놓이는 위치와 크기가 고정적이지 않다는 것이다.

<!-- 메인 콘텐츠 영역의 카드: 넓다 -->
<main>
  <ProductCard />
</main>

<!-- 사이드바의 카드: 좁다 -->
<aside>
  <ProductCard />
</aside>

같은 ProductCard 컴포넌트인데, 놓인 위치에 따라 다른 스타일이 필요하다. 미디어 쿼리는 뷰포트 기준이라 이 상황을 처리하기 어렵다.

Container Query 사용:

/* 카드를 감싸는 컨테이너 정의 */
.card-container {
  container-type: inline-size;
  container-name: card; /* 선택사항 */
}

/* 컨테이너 크기가 500px 이상일 때 */
@container card (min-width: 500px) {
  .product-card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
  .product-card img {
    height: 100%;
  }
}

/* 컨테이너 크기가 300px 미만일 때 */
@container card (max-width: 300px) {
  .product-card {
    flex-direction: column;
  }
  .product-card img {
    width: 100%;
  }
}

이제 ProductCard는 뷰포트가 아닌 자신이 담긴 컨테이너 크기에 반응한다. 메인 콘텐츠에서는 가로 배치, 사이드바에서는 세로 배치가 자동으로 된다. 하나의 컴포넌트 CSS 코드로.

Container Query Units

/* cqw: 컨테이너 너비의 1% */
.card-title {
  font-size: clamp(14px, 3cqw, 24px); /* 컨테이너 크기에 비례하는 폰트 */
}

2. :has() 선택자: CSS에서 드디어 부모를 선택할 수 있다

CSS에서 "자식 요소를 기준으로 부모를 선택"하는 것은 오랫동안 불가능했다. JavaScript로 처리하거나, 부모에 별도 클래스를 추가해야 했다. :has()가 이것을 가능하게 한다.

/* 체크박스가 선택된 경우 부모 label 스타일 변경 */
label:has(input[type="checkbox"]:checked) {
  background: #e8f5e9;
  font-weight: bold;
}

/* img 태그를 포함하는 figure */
figure:has(img) {
  border: 1px solid #ddd;
  padding: 8px;
}

/* 에러가 있는 폼 필드 그룹 */
.form-group:has(.error-message) {
  background: #fff0f0;
}

/* 자식이 없는 컨테이너 숨기기 */
.sidebar:has(:not(*)) {
  display: none; /* 사이드바가 비어있으면 숨김 */
}

실용적인 활용 예: 드롭다운 메뉴

/* 하위 메뉴가 있는 nav 아이템에 화살표 표시 */
nav li:has(ul) > a::after {
  content: ' ▾';
}

/* 하위 메뉴가 펼쳐진 경우 화살표 방향 변경 */
nav li:has(ul:hover) > a::after {
  content: ' ▴';
}

JavaScript 없이 순수 CSS만으로 드롭다운 상태에 따른 스타일 변경이 가능해진다.


3. Cascade Layers: 스타일 충돌의 종말

CSS 우선순위 충돌은 모든 프론트엔드 개발자가 겪는 문제다. !important를 쓰거나, 선택자를 더 구체적으로 만들어 우선순위를 높이는 "특이성 전쟁"이 일어난다.

Cascade Layers는 스타일시트를 명확한 계층으로 나눠, 계층 간 우선순위를 명시적으로 정의한다.

/* 계층 순서 선언 (앞에 선언할수록 우선순위 낮음) */
@layer reset, base, components, utilities;

/* reset 계층: 브라우저 기본 스타일 초기화 */
@layer reset {
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
  }
}

/* base 계층: 기본 타이포그래피, 색상 */
@layer base {
  body {
    font-family: 'Pretendard Variable', sans-serif;
    color: #1a1a1a;
  }
}

/* components 계층: 재사용 컴포넌트 */
@layer components {
  .button {
    padding: 8px 16px;
    border-radius: 4px;
    background: blue;
    color: white;
  }
}

/* utilities 계층: 단일 목적 유틸리티 (최고 우선순위) */
@layer utilities {
  .mt-4 { margin-top: 16px; }
  .text-red { color: red; }
}

계층이 선언된 순서가 우선순위를 결정한다. utilities가 마지막에 선언됐으므로, components의 스타일을 !important 없이 덮어쓸 수 있다.

서드파티 라이브러리 충돌 해결

/* 서드파티 CSS를 낮은 계층에 가두기 */
@layer vendors {
  @import url("bootstrap.css");
}

/* 우리 스타일이 항상 bootstrap을 덮어씀 */
@layer components {
  .button { /* bootstrap의 .button보다 이 스타일이 항상 우선 */ }
}

4. CSS Nesting: Sass 없이 중첩 선택자

Sass/SCSS의 가장 편리한 기능 중 하나가 중첩 선택자다. 이제 네이티브 CSS에서도 가능하다.

/* 기존 CSS */
.card { background: white; }
.card:hover { background: #f5f5f5; }
.card .title { font-size: 20px; }
.card .title a { color: blue; }
.card .title a:hover { color: darkblue; }

/* CSS Nesting */
.card {
  background: white;

  &:hover {
    background: #f5f5f5;
  }

  .title {
    font-size: 20px;

    a {
      color: blue;

      &:hover {
        color: darkblue;
      }
    }
  }
}

컴포넌트 단위로 스타일을 한 블록에 모을 수 있어 가독성이 크게 향상된다.

미디어 쿼리도 중첩 가능

.container {
  width: 100%;

  @media (min-width: 768px) {
    width: 750px;
  }

  @media (min-width: 1200px) {
    width: 1170px;
  }
}

5. 실전 예시: 카드 컴포넌트를 Container Query로 설계

이 모든 기능을 조합한 실전 예시다.

/* 계층 구조 선언 */
@layer reset, base, components;

@layer components {
  /* 카드 컨테이너 */
  .card-wrapper {
    container-type: inline-size;
    container-name: card;
  }

  /* 카드 기본 스타일 */
  .card {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 16px;
    border-radius: 8px;
    background: white;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);

    /* 이미지가 있는 카드 스타일 */
    &:has(img) {
      padding: 0;

      .card-content {
        padding: 16px;
      }
    }

    /* 선택된 카드 강조 */
    &:has(input:checked) {
      outline: 2px solid blue;
    }

    /* 넓은 컨테이너: 가로 배치 */
    @container card (min-width: 500px) {
      flex-direction: row;
      align-items: center;

      img {
        width: 200px;
        height: 150px;
        object-fit: cover;
        border-radius: 8px 0 0 8px;
      }
    }
  }
}

6. 브라우저 지원 현황과 Progressive Enhancement

기능ChromeFirefoxSafariEdge
Container Queries105+110+16+105+
:has()105+121+15.4+105+
Cascade Layers99+97+15.4+99+
CSS Nesting120+117+17.2+120+

2026년 기준으로 한국 사용자의 95%+ 이상이 지원 브라우저를 사용한다. IE 지원이 필요하지 않다면 모두 사용 가능하다.

구형 브라우저 대비 Progressive Enhancement:

/* 기본 스타일 (모든 브라우저) */
.card { flex-direction: column; }

/* Container Query 지원 브라우저에서만 */
@supports (container-type: inline-size) {
  .card-wrapper { container-type: inline-size; }

  @container (min-width: 500px) {
    .card { flex-direction: row; }
  }
}

맺으며

지난 10년간 CSS의 한계를 Sass, CSS-in-JS, 여러 도구들이 보완해왔다. 이제 많은 부분에서 도구 없이 네이티브 CSS만으로 해결할 수 있다.

당장 모든 프로젝트에서 Sass를 제거할 필요는 없다. 하지만 새로 시작하는 프로젝트라면 네이티브 CSS만으로 시작해보자. Container Query로 진정한 컴포넌트 기반 반응형 설계를 해보고, :has()로 JavaScript 없이 상태를 표현하고, Cascade Layers로 스타일 우선순위를 명확히 관리해보자.

CSS가 이렇게 강력해진 시대에, 이전 방식에만 머물 이유가 없다.