CSS @scope at-rule로 선택자의 도달범위 제한

@scope를 사용하여 DOM의 제한된 하위 트리 내에서만 요소를 선택하는 방법을 알아봅니다.

브라우저 지원

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 플래그 뒤에 있습니다.
  • Safari: 17.4

소스

CSS 선택자 작성의 섬세한 기술

선택기를 작성할 때 두 가지 세계 사이에서 갈등하게 될 수 있습니다. 한편으로는 선택할 요소를 매우 구체적으로 지정해야 합니다. 반면에 선택기를 쉽게 재정의할 수 있고 DOM 구조와 긴밀하게 결합하지 않도록 하는 것이 좋습니다.

예를 들어 '카드 구성요소의 콘텐츠 영역에 있는 대표 이미지'를 선택하려는 경우(다소 구체적인 요소 선택) .card > .content > img.hero와 같은 선택기를 작성하는 것은 바람직하지 않습니다.

  • 이 선택기는 (0,3,1)특이성이 매우 높기 때문에 코드가 커질수록 재정의하기 어렵습니다.
  • 직접 하위 요소 결합자를 사용하면 DOM 구조에 밀접하게 결합됩니다. 마크업이 변경되면 CSS도 변경해야 합니다.

하지만 img만 해당 요소의 선택기로 작성하면 페이지의 모든 이미지 요소가 선택되므로 좋지 않습니다.

적절한 균형을 찾는 것은 종종 매우 어려운 일입니다. 지난 몇 년간 일부 개발자가 이러한 상황에서 도움이 되는 솔루션과 해결 방법을 고안해 왔습니다. 예를 들면 다음과 같습니다.

  • BEM과 같은 방법에서는 특정 요소에 card__img card__img--hero 클래스를 제공하여 특이성을 낮게 유지하는 동시에 선택한 항목을 구체적으로 지정할 수 있도록 합니다.
  • 범위 지정 CSS 또는 스타일 지정된 구성요소와 같은 JavaScript 기반 솔루션은 페이지의 다른 쪽에 있는 요소를 타겟팅하지 않도록 선택자에 무작위로 생성된 문자열(예: sc-596d7e0e-4)을 추가하여 모든 선택자를 재작성합니다.
  • 일부 라이브러리는 선택기를 완전히 삭제하기도 하므로 스타일 지정 트리거를 마크업 자체에 직접 배치해야 합니다.

하지만 이 중 어느 것도 필요하지 않았다면 어떻게 해야 할까요? CSS에서 높은 특이성의 선택자나 DOM에 밀접하게 결합된 선택자를 작성하지 않고도 선택할 요소를 매우 구체적으로 지정할 수 있는 방법을 제공한다면 어떨까요? 이때 @scope가 DOM의 하위 트리 내에서만 요소를 선택하는 방법을 제공합니다.

@scope 소개

@scope를 사용하면 선택기의 도달범위를 제한할 수 있습니다. 이렇게 하려면 타겟팅할 하위 트리의 상한을 결정하는 조정 루트를 설정합니다. 범위 지정 루트 세트를 사용하면 범위가 지정된 스타일 규칙이라는 포함된 스타일 규칙을 DOM의 제한된 하위 트리에서만 선택할 수 있습니다.

예를 들어 .card 구성요소에서 <img> 요소만 타겟팅하려면 .card@scope at-rule의 범위 지정 루트로 설정합니다.

@scope (.card) {
    img {
        border-color: green;
    }
}

범위 지정된 스타일 규칙 img { … }는 일치하는 .card 요소의 범위 내에 있는 <img> 요소만 선택할 수 있습니다.

카드의 콘텐츠 영역 (.card__content) 내 <img> 요소가 선택되지 않도록 하려면 img 선택기를 더 구체적으로 지정하면 됩니다. 이렇게 하는 또 다른 방법은 @scope at-rule이 하한을 결정하는 범위 제한도 허용한다는 사실을 사용하는 것입니다.

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

이 범위 지정된 스타일 규칙은 조상 트리의 .card.card__content 요소 사이에 배치된 <img> 요소만 타겟팅합니다. 상한과 하한이 있는 이러한 유형의 범위 지정은 종종 도넛 범위라고 합니다.

:scope 선택기

기본적으로 모든 범위 지정된 스타일 규칙은 범위 지정 루트를 기준으로 합니다. 범위 지정 루트 요소 자체를 타겟팅할 수도 있습니다. 이렇게 하려면 :scope 선택기를 사용합니다.

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

범위가 지정된 스타일 규칙 내의 선택자는 암시적으로 :scope가 접두사로 추가됩니다. 원하는 경우 :scope를 직접 접두사로 추가하여 명시적으로 지정할 수 있습니다. 또는 CSS 중첩에서 & 선택자를 앞에 추가할 수 있습니다.

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

범위 제한은 :scope 가상 클래스를 사용하여 범위 루트와의 특정 관계를 요구할 수 있습니다.

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

범위 제한은 :scope를 사용하여 범위 루트 외부의 요소를 참조할 수도 있습니다. 예를 들면 다음과 같습니다.

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

범위 지정된 스타일 규칙 자체는 하위 트리를 이스케이프할 수 없습니다. :scope + p와 같은 선택 항목은 범위 내에 없는 요소를 선택하려고 시도하므로 유효하지 않습니다.

@scope 및 특이성

@scope의 프레루드에서 사용하는 선택자는 포함된 선택자의 특이성에 영향을 미치지 않습니다. 아래 예에서 img 선택기의 특이성은 여전히 (0,0,1)입니다.

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        
    }
}

:scope의 특이성은 일반 가상 클래스((0,1,0))의 특이성과 같습니다.

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        
    }
}

다음 예에서 내부적으로 &:is() 선택기 내에 래핑된 범위 지정 루트에 사용되는 선택기로 재작성됩니다. 결국 브라우저는 :is(#sidebar, .card) img를 선택기로 사용하여 일치를 실행합니다. 이 프로세스를 디슈가링이라고 합니다.

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        
    }
}

&:is()를 사용하여 디슈가링되므로 &의 특이성은 :is() 특이성 규칙에 따라 계산됩니다. &의 특이성은 가장 구체적인 인수의 특이성입니다.

이 예에 적용되는 :is(#sidebar, .card)의 특수성은 가장 구체적인 인수, 즉 #sidebar의 특수성이므로 (1,0,0)이 됩니다. 이를 img의 특이성((0,0,1))과 결합하면 전체 복잡한 선택기의 특이성으로 (1,0,1)가 됩니다.

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        
    }
}

@scope 내의 :scope&의 차이

특이성이 계산되는 방식의 차이 외에, :scope&의 또 다른 차이점은 :scope는 일치하는 범위 지정 루트를 나타내는 반면 &는 범위 지정 루트를 일치시키는 데 사용되는 선택자를 나타낸다는 것입니다.

따라서 &를 여러 번 사용할 수 있습니다. 이는 한 번만 사용할 수 있는 :scope와 대조됩니다. 스코핑 루트 내에서 스코핑 루트를 일치시킬 수 없기 때문입니다.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    
  }
}

프렐류드가 없는 범위

<style> 요소로 인라인 스타일을 작성할 때 범위 지정 루트를 지정하지 않고 스타일 규칙의 범위를 <style> 요소의 주변을 둘러싼 상위 요소로 지정할 수 있습니다. 이렇게 하려면 @scope의 서문을 생략합니다.

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

위 예에서 범위가 지정된 규칙은 클래스 이름이 card__headerdiv 내의 요소만 타겟팅합니다. 이 div<style> 요소의 상위 요소이기 때문입니다.

캐스케이드의 @scope

@scopeCSS 계층 구조 내부에 새로운 기준인 범위 근접성도 추가합니다. 이 단계는 구체성 다음, 노출 순서 전에 표시됩니다.

CSS 계층 구조를 시각화한 모습입니다.

사양에 따라:

스타일 규칙에 표시되는 선언을 서로 다른 범위 지정 루트와 비교할 때 범위 지정 루트와 범위 지정된 스타일 규칙 주제 간에 가장 적은 세대 또는 형제 요소 홉이 있는 선언이 우선합니다.

이 새로운 단계는 구성요소의 여러 변형을 중첩할 때 유용합니다. @scope를 아직 사용하지 않는 다음 예시를 살펴보세요.

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

이 약간의 마크업을 볼 때 세 번째 링크는 .light 클래스가 적용된 div의 하위 요소이지만 black가 아닌 white입니다. 이는 폭포식 구조에서 실적이 가장 우수한 항목을 결정하는 데 사용되는 표시 순서 기준 때문입니다. .dark a가 마지막으로 선언되었으므로 .light a 규칙에서 이기게 됩니다.

를 참고하세요.

범위 지정 근접성 기준을 사용하면 이 문제를 해결할 수 있습니다.

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

두 범위 지정된 a 선택기의 특이성이 동일하므로 범위 지정 근접성 기준이 적용됩니다. 두 선택기를 범위 지정 루트에서의 근접도에 따라 가중치로 지정합니다. 세 번째 a 요소의 경우 .light 범위 지정 루트까지는 1홉이지만 .dark 루트까지는 2홉입니다. 따라서 .lighta 선택기가 이기게 됩니다.

를 참고하세요.

마무리 문구: 스타일 격리가 아닌 선택기 격리

한 가지 중요한 점은 @scope가 선택기의 도달범위를 제한하며 스타일 격리를 제공하지 않는다는 것입니다. 하위 요소로 상속되는 속성은 @scope의 하한값을 넘어 계속 상속됩니다. 이러한 속성 중 하나는 color 속성입니다. 도넛 범위 내에서 이를 선언할 때 color는 여전히 도넛 범위 안에 있는 하위 요소로 상속됩니다.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}
를 참고하세요.

위 예에서 .card__content 요소와 그 하위 요소는 .card에서 값을 상속하므로 hotpink 색상을 갖습니다.

(표지 사진: UnsplashRustam Burkhanov 제공)