@scope를 사용하여 DOM의 제한된 하위 트리 내에서만 요소를 선택하는 방법을 알아봅니다.
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__header
인 div
내의 요소만 타겟팅합니다. 이 div
가 <style>
요소의 상위 요소이기 때문입니다.
캐스케이드의 @scope
@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
범위 지정 루트까지는 1홉이지만 .dark
루트까지는 2홉입니다. 따라서 .light
의 a
선택기가 이기게 됩니다.
마무리 문구: 스타일 격리가 아닌 선택기 격리
한 가지 중요한 점은 @scope
가 선택기의 도달범위를 제한하며 스타일 격리를 제공하지 않는다는 것입니다. 하위 요소로 상속되는 속성은 @scope
의 하한값을 넘어 계속 상속됩니다. 이러한 속성 중 하나는 color
속성입니다. 도넛 범위 내에서 이를 선언할 때 color
는 여전히 도넛 범위 안에 있는 하위 요소로 상속됩니다.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
위 예에서 .card__content
요소와 그 하위 요소는 .card
에서 값을 상속하므로 hotpink
색상을 갖습니다.
(표지 사진: Unsplash의 Rustam Burkhanov 제공)