CSS @scope — 울타리 치는 네이티브 스코핑의 시대가 왔다
CSS에서 스타일 충돌은 오래된 숙적이다. 클래스 이름이 겹치거나, 부모의 색상이 손자 요소까지 줄줄 흘러내리는 상황은 마치 아파트 층간소음과 같다 — 내가 아무리 조용히 해도 옆집에서 소리가 새어 들어온다. BEM, CSS Modules, styled-components 같은 방법론은 전부 이 소음을 차단하기 위한 방음재였다. 그런데 이제 건물 자체에 방음벽이 내장됐다. @scope가 바로 그것이다.
@scope 기본 원리
@scope는 DOM 서브트리에 울타리를 두르는 CSS at-rule이다. 울타리 안에서 선언한 스타일은 울타리 바깥에 절대 영향을 주지 않는다.
@scope (.card) {
h2 {
font-size: 1.5rem;
color: #1a1a1a;
}
p {
line-height: 1.6;
color: #555;
}
}.card 내부의 h2와 p만 이 스타일의 영향을 받는다. 외부에 아무리 많은 h2가 있어도 무관하다. @scope (selector)에서 소괄호 안의 셀렉터가 scope root, 즉 울타리의 시작점이 된다.
[💡 잠깐! 이 용어는?]
scope root: @scope 규칙이 적용되는 최상위 요소. 이 요소의 하위 DOM 트리가 스타일 적용 범위가 된다. 자바스크립트의 함수 스코프에서 함수 자체에 해당하는 개념이다.
하한(lower boundary) — to 키워드
@scope의 진짜 무기는 to 키워드다. 상한뿐 아니라 하한까지 설정할 수 있다.
@scope (.card) to (.card__content) {
/* .card 안이면서 .card__content 바깥 요소에만 적용 */
color: #333;
font-weight: bold;
}scope root(.card)에서 시작해 하한(.card__content)이 매칭되는 요소와 그 하위 요소는 스타일 적용 대상에서 제외된다. 비유하면, 빌딩에서 1층부터 5층까지만 난방하고, 6층 이상은 별도 보일러를 쓰게 하는 것이다.
도넛 스코핑 — 가운데를 비워라
[💡 잠깐! 이 용어는?]
도넛 스코핑(Donut Scoping): 도넛처럼 바깥 고리에만 스타일을 적용하고, 가운데 구멍(내부 영역)은 건드리지 않는 패턴. @scope ... to ... 구문의 대표적 활용 사례다.
도넛 스코핑은 웹 컴포넌트의 슬롯이나 CMS가 삽입하는 사용자 콘텐츠를 보호할 때 빛을 발한다.
@scope (.theme-dark) to (.slot) {
background: #1a1a2e;
color: #e0e0e0;
border-color: #333;
}<div class="theme-dark">
<header>다크 테마 적용됨</header>
<nav>다크 테마 적용됨</nav>
<div class="slot">
<!-- 여기는 다크 테마 미적용 -->
<p>기본 스타일 유지</p>
</div>
</div>.theme-dark 내부이지만 .slot 바깥의 header, nav에만 다크 테마가 입혀진다. .slot 안쪽은 자체 스타일을 유지한다.
근접성(Proximity) 우선순위
CSS 캐스케이드에 @scope가 도입한 완전히 새로운 규칙이 있다. 같은 specificity를 가진 두 규칙이 충돌할 때, scope root가 대상 요소에 더 가까운 쪽이 이긴다.
@scope (.outer) {
p { color: red; }
}
@scope (.inner) {
p { color: blue; }
}<div class="outer">
<div class="inner">
<p>이 텍스트는 blue — .inner가 더 가깝다</p>
</div>
<p>이 텍스트는 red — .outer 직계 자식이다</p>
</div>[💡 잠깐! 이 용어는?] 근접성 우선순위(Proximity Priority): DOM 트리에서 대상 요소와 scope root 사이의 거리가 가까울수록 우선순위가 높아지는 규칙. CSS 캐스케이드의 기존 계층(origin, importance, specificity, order)에 추가된 새로운 단계다.
회사 조직에 비유하면 이해가 쉽다. CEO(.outer)가 "빨간 펜으로 써라"라고 했더라도, 직속 팀장(.inner)이 "파란 펜으로 써라"라고 하면 팀장 지시를 따르는 것과 같다.
BEM·CSS-in-JS·Tailwind와 정면 비교
| 기준 | BEM | CSS-in-JS | Tailwind | @scope |
|---|---|---|---|---|
| 격리 방식 | 네이밍 컨벤션 (약속) | 자동 생성 클래스 | 유틸리티 클래스 | 네이티브 CSS 규칙 |
| HTML 결합도 | 높음 (구조 반영) | 중간 | 높음 (클래스 나열) | 낮음 |
| 런타임 비용 | 0 | JS 번들 증가 | 0 | 0 |
| 하한 제한 | 불가 | 불가 | 불가 | to 키워드 |
| 도구 의존성 | 없음 | 번들러 필수 | PostCSS 필요 | 없음 |
| 강제력 | 팀원 의지 | 자동 | 자동 | 브라우저 강제 |
| 학습 곡선 | 컨벤션 학습 | API 학습 | 클래스 암기 | CSS 문법 |
BEM은 "약속"이고, @scope는 "법률"이다. 약속은 어길 수 있지만 법률은 시스템이 강제한다. CSS-in-JS는 격리 문제를 해결하지만 런타임 비용이라는 대가를 치른다. @scope는 런타임 비용이 제로다.
/* @scope — 네이티브 CSS, 런타임 비용 제로 */
@scope (.product-card) {
.title { font-size: 1.2rem; }
.price { color: green; font-weight: bold; }
}
/* CSS-in-JS — JS 런타임에서 클래스 생성, 번들 크기 증가 */
/* const Title = styled.h3`font-size: 1.2rem;` */
/* const Price = styled.span`color: green; font-weight: bold;` */실전 — 중첩 위젯에서의 스타일 격리
대시보드처럼 위젯 안에 위젯이 들어가는 구조에서 @scope의 가치가 극대화된다.
@scope (.widget) to (.widget) {
.header {
background: var(--widget-bg);
padding: 1rem;
border-bottom: 1px solid #eee;
}
.body {
padding: 1rem;
}
}@scope (.widget) to (.widget)의 의미는 명확하다. "바깥 위젯의 스타일은 안쪽 위젯의 경계에서 멈춘다." 재귀적 컴포넌트 구조에서 스타일이 새는 문제를 원천 차단한다.
브라우저 지원 현황
2024년 12월 Firefox 146의 지원을 마지막으로 @scope는 Baseline 호환에 도달했다. Chrome 118(2023.10), Edge 118(2023.10), Safari 17.4(2024.03)에서 이미 지원 중이었다.
@supports (selector(:scope)) {
@scope (.card) {
.title { color: navy; }
}
}
/* 폴백 — @scope 미지원 브라우저 */
.card .title {
color: navy;
}마무리
@scope가 BEM을 완전히 대체하느냐는 질문에는 "단독으로는 아직 아니다"가 답이다. BEM의 명확한 네이밍 체계가 주는 팀 커뮤니케이션 효과는 @scope의 영역이 아니다. 하지만 스타일 격리라는 본질에서는 @scope가 압도적이다.
가장 현실적인 조합은 BEM의 네이밍 철학 + @scope의 격리 능력이다. 이름은 의미 있게 짓되, 스코핑은 브라우저에게 맡기는 방식이다. @scope, @layer, :has(), @container — 네이티브 CSS만으로 복잡한 스타일링 문제를 해결할 수 있는 시대가 열리고 있다.
관심 있을 만한 포스트
CSS @property — 커스텀 속성에 타입을 부여하는 방법
CSS @property at-rule로 커스텀 속성에 타입 정의, 상속 제어, 폴백을 추가해 렌더링 안정성과 애니메이션 가능성을 확보하는 방법을 다룬다.
CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스
CSS Working Group에 제안된 :near() 의사 클래스는 포인터 근접성을 감지해 호버 전에 UI를 활성화하는 새로운 상호작용 패턴을 연다.
CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간
Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.
sibling-index()로 만드는 CSS 스크롤 소용돌이 — JavaScript 없이 수백 개 요소 애니메이션
CSS sibling-index()와 scroll-driven animations를 결합해 순수 CSS만으로 텍스트 보텍스 효과를 구현하는 기법을 다룬다.
Interop 2026 — 브라우저 전쟁이 끝나고 표준 전쟁이 시작됐다
Chrome, Safari, Firefox가 합의한 20개 웹 표준 집중 영역과 프론트엔드 개발자가 주목해야 할 핵심 기능을 정리한다.
CSS Stacking Context — z-index: 99999를 줬는데 왜 안 올라올까
CSS 쌓임 맥락의 생성 조건, z-index의 실제 작동 원리, 그리고 레이아웃 버그를 디버깅하는 실전 전략을 정리한다.
OHP 필름을 겹치듯 — conic-gradient와 attr()로 순수 CSS 파이 차트 만들기
conic-gradient, CSS 커스텀 프로퍼티, 새로운 attr() 타입 구문을 활용해 JavaScript 없이 시맨틱한 파이 차트를 구현하는 방법을 다룬다.
배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법
브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.