@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 제공)