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

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

브라우저 지원

  • 118
  • 118
  • x
  • x

CSS 선택자를 작성하는 섬세한 기술

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

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

  • 이 선택기는 (0,3,1)특이성이 매우 높아 코드가 커짐에 따라 재정의하기 어렵습니다.
  • 직접 하위 요소 연결자에 의존하여 DOM 구조와 긴밀하게 결합됩니다. 마크업이 변경되면 CSS도 변경해야 합니다.

그러나 이 요소의 선택기로 img만 작성해서는 안 됩니다. 이렇게 하면 페이지 전체의 모든 이미지 요소가 선택되기 때문입니다.

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

  • BEM과 같은 방법에서는 이 요소에 card__img card__img--hero 클래스를 지정하여 특이성을 낮게 유지하면서 선택한 항목을 구체적으로 지정할 수 있도록 합니다.
  • 범위가 지정된 CSS 또는 스타일 구성요소와 같은 자바스크립트 기반 솔루션은 무작위로 생성된 문자열(예: 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 */
  }
  :root :root { /* ❌ 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으로 가는 홉은 2개입니다. 따라서 .lighta 선택기가 선택됩니다.

맺음말: 스타일 격리가 아닌 선택기 격리

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

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

위 예에서 .card__content 요소와 그 하위 요소는 .card에서 값을 상속받기 때문에 hotpink 색상을 사용합니다.

(커버 사진: Unsplashrustam burkhanov)