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

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

브라우저 지원

  • Chrome: 118 <ph type="x-smartling-placeholder">
  • Edge: 118. <ph type="x-smartling-placeholder">
  • Firefox: 깃발 뒤쪽에 있습니다.
  • Safari 17.4. <ph type="x-smartling-placeholder">

소스

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

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

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

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

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

이 과정에서 적절한 균형을 찾는 것은 쉬운 일이 아닙니다. 지난 몇 년 동안 일부 개발자들은 이러한 상황에서 도움이 되는 솔루션과 해결 방법을 고안해 왔습니다. 예를 들면 다음과 같습니다.

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

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

@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

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 색상이 지정됩니다.

(표지 사진: rustam burkhanov Unsplash)