04 — @scope

답하는 질문: 컴포넌트 스타일이 트리 밖으로 새지 않도록 CSS만으로 격리할 수 있는가?


한 줄 답

@scope선택자가 적용되는 DOM 범위를 루트경계로 명시하는 at-rule이다. 루트 안의 모든 후손에 매칭되되, 경계 선택자에 닿으면 매칭을 끊는다 — 이 모양이 도넛처럼 가운데가 비어 있어 “도넛 스코프” 라 부른다. CSS Modules·BEM·CSS-in-JS가 우회로 해결한 격리를 언어 자체가 푼다.


Why — 왜 격리가 필요했나

1) CSS의 글로벌 네임스페이스

.card { ... }

이 한 줄은 문서 전체의 모든 .card 에 적용된다. 컴포넌트 A의 .card와 컴포넌트 B의 .card가 같은 이름이면 충돌. 28년간 CSS의 가장 큰 약점.

2) 우회로 — 모두 이름 규칙

해법격리 방식한계
BEM.block__element--modifier 명명사람이 지키는 규칙 — 깨지기 쉬움
CSS Modules빌드 시 .card → .Card_card_a1b2로 hash빌드 도구 필수
Shadow DOM진짜 격리 — 트리 분리DOM이 격리되어 selector 공유 불가
CSS-in-JS런타임에 unique 클래스명런타임 비용, SSR 복잡

모두 이름을 unique하게 만드는 우회였다. CSS 자체에는 범위 개념이 없었다.

3) 2024년 — @scope의 합류

2024년 Chrome 118, Safari 17.4가 구현. Firefox는 진행 중(2026-05 기준 미지원). 이로써 CSS는 언어 차원의 스코프를 처음 갖게 됐다.


How — 동작 모델

문법

@scope (root-selector) to (limit-selector) {
  .target { ... }
}
  • (root) — 스코프의 시작점(포함). 이 요소와 후손에 매칭.
  • to (limit) — 스코프의 경계(제외). 이 선택자에 닿으면 매칭을 끊음. 생략 가능.
  • 내부에서 &root 자기 자신을 가리킴 — nesting과 동일.

What — 사용 패턴

1) 기본 — 범위 한정

@scope (.card) {
  .title { font-size: 1.25rem; }   /* .card 안의 .title만 */
}

전역의 다른 .title은 영향 없음.

2) 도넛 스코프 — to 경계

<article class="post">
  <h1 class="title">Outer</h1>
  <section class="embedded-quote">
    <h1 class="title">Embedded — 다른 스타일 원함</h1>
  </section>
</article>
@scope (.post) to (.embedded-quote) {
  .title { color: blue; }   /* .post 안이되, .embedded-quote 안은 제외 */
}

→ 외부 title은 파랑, embedded-quote 안의 title은 상속만 받고 매칭 안 됨.

3) & — root 참조

@scope (.card) {
  & { padding: 1rem; }              /* .card 자신 */
  & > .header { border-bottom: 1px solid; }
  & .title { font-weight: 700; }
}

4) :scope — 명시적 root

@scope (.card) {
  :scope { background: white; }     /* &와 동일 */
  :scope .button { ... }
}

5) Nesting과 결합

@scope (.card) {
  & {
    padding: 1rem;
    & .title { font-size: 1.25rem; }
    & .body { line-height: 1.6; }
  }
}

6) Inline scope — <style> 안의 자기 자신

<div class="widget">
  <style>
    @scope {
      :scope { background: #fff; }    /* 가장 가까운 부모로 자동 한정 */
      .title { font-size: 1.5rem; }
    }
  </style>
  <h2 class="title">Inline-scoped</h2>
</div>

이 패턴은 컴포넌트 안에 스타일을 넣는 사용처에 강력 — Vue/Svelte의 scoped styles를 표준 CSS로.

7) Specificity

@scope 자체는 specificity를 추가하지 않는다. :scope는 단일 :where()와 같은 0 specificity. 외부에서 덮어쓰기 쉽다는 의도.

8) Cascade Proximity

같은 specificity일 때 가장 가까운 root에 정의된 규칙이 이긴다 — 중첩된 scope에서 직관적.

@scope (.outer) { .item { color: red; } }
@scope (.inner) { .item { color: blue; } }   /* .inner가 더 가까우면 이김 */

What-if — 잘못 쓰면

Case 1: Firefox 미지원

2026-05 기준 Firefox는 @scope 진행 중 — 안정 채널 미지원. 가드:

@supports at-rule(@scope) {
  @scope (.card) { ... }
}
/* 폴백: BEM 또는 CSS Modules */

production 도입 시 기능 감지 + 폴백 필수.

Case 2: to (limit)의 함정 — 상속은 안 끊긴다

@scope (.post) to (.embedded) {
  & { color: blue; }
}

.embedded 안의 텍스트도 blue로 보일 수 있다@scope매칭만 끊고 상속은 막지 않음. 명시적으로 .embedded { color: initial; } 같은 reset 필요할 수 있음.

Case 3: & 없이 쓰면

@scope (.card) {
  .title { ... }   /* .card 안의 .title — OK */
}

위는 결합자(descendant) 가 암묵적. > 같은 콤비네이터가 필요하면 & > .title처럼 & 필수.

Case 4: 도넛 스코프와 :has() 조합 — 강력하지만 위험

@scope (.post:has(.embedded)) to (.embedded) {
  .title { color: red; }
}

:has()embedded가 있을 때만 스코프 활성화 — 강력하지만 둘 다 동적 재평가 비용이 있어 함께 쓰면 성능 위험.

Case 5: SPA 라우팅에서 스타일 누적

@scope로 격리해도 전역 CSS는 그대로 누적 — SPA에서 페이지 단위 스코프를 원하면 루트를 페이지 컨테이너에 두기:

@scope (#page-checkout) { ... }

Insight — “도넛”이라는 메타포의 정확함

@scope (.card) to (.button)을 생각해보자.

       .card  ← 루트 (포함)
      ┌──────┐
      │ ░░░░ │  ← 매칭됨
      │ ░░░░ │
      │ ┌──┐ │
      │ │  │ │  ← .button = 경계 (제외)
      │ │  │ │
      │ └──┘ │
      │ ░░░░ │
      └──────┘

가운데가 비어있는 도넛. 이 모양이 정확히 재사용 가능 컴포넌트의 요구와 일치한다.

  • .card 안의 일반 텍스트 — 카드 스타일 적용 ✓
  • .card 안의 .button부모 카드와 무관한 별도 컴포넌트 → 스타일 격리 ✓
  • 이는 React/Vue에서 <Card> 안에 <Button>을 넣을 때 Button이 Card 스타일을 상속받지 않기를 바라는 본능과 일치.

@scope는 React 컴포넌트 격리의 CSS 번역이다.

Miriam Suzanne(@scope 명세 공동 작성자)은 이를 “자연어의 단락 vs 인용 블록” 에 비유했다 — “이 문단의 규칙은 이 문단 안에서만 적용되되, 인용된 부분은 자기 규칙을 따른다.”

CSS가 문서를 모델링하는 언어에서 컴포넌트를 모델링하는 언어로 옮겨가는 또 하나의 증거.


요약 + Mermaid

  • @scope (root) to (limit)선택자 범위 명시 — 도넛 스코프.
  • &/:scope로 root 참조.
  • Specificity 추가 없음 — 덮어쓰기 쉬움.
  • 상속은 안 끊긴다 — 색·폰트는 root에서 흘러내림.
  • Firefox 미지원(2026-05) — @supports로 가드.

다음: 05-viewport-units — iOS Safari 100vh 문제와 dvh.