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

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

게시일: 2023년 10월 4일

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 146.
  • Safari: 17.4.

Source

선택기를 작성할 때 두 가지 세계 사이에서 갈등을 느낄 수 있습니다. 한편으로는 선택하는 요소를 매우 구체적으로 지정해야 합니다. 반면 선택기는 쉽게 재정의할 수 있어야 하며 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> 요소만 타겟팅하려면 @scope at-rule의 범위 지정 루트로 .card을 설정합니다.

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

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

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

@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를 선택기로 사용하여 일치시킵니다. 이 프로세스를 desugaring이라고 합니다.

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

&:is()를 사용하여 desugar되므로 &의 구체성은 :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

CSS 캐스케이드 내에서 @scope범위 지정 근접성이라는 새로운 기준도 추가합니다. 이 단계는 구체성 다음, 표시 순서 앞에 옵니다.

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 범위 지정 루트까지는 홉이 하나이지만 .dark까지는 홉이 두 개입니다. 따라서 .lighta 선택자가 우선합니다.

스타일 격리가 아닌 선택기 격리

@scope는 선택기의 도달범위를 제한합니다. 스타일 격리를 제공하지 않습니다. 하위 요소로 상속되는 속성은 @scope의 하한을 넘어 계속 상속됩니다. 이러한 속성 중 하나가 color입니다. 도넛 범위 내에서 이를 선언하면 color는 도넛의 구멍 내에 있는 하위 요소로 계속 상속됩니다.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

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