@scope 를 사용하여 DOM의 제한된 하위 트리 내에서만 요소를 선택하는 방법을 알아봅니다.
브라우저 지원
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <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__header
인 div
내부의 요소만 타겟팅합니다. div
가 <style>
요소의 상위 요소이기 때문입니다.
캐스케이드의 @scope
CSS 캐스케이드 내부에 @scope
는 새 기준인 범위 지정 근접도도 추가합니다. 이 단계는 특정성 이후에 나오지만 표시 순서 이전에 나옵니다.
범위 지정 루트가 서로 다른 스타일 규칙에 나타나는 선언을 비교할 때 범위 지정 루트와 범위가 지정된 스타일 규칙 대상 사이의 세대 또는 동위 요소 홉이 가장 적은 선언이 우선합니다.
이 새로운 단계는 구성요소의 여러 변형을 중첩할 때 유용합니다. 아직 @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 홉입니다. 따라서 .light
의 a
선택기가 우선합니다.
맺음말: 스타일 격리가 아닌 선택기 격리
한 가지 중요한 점은 @scope
이 선택기의 도달범위를 제한하며 스타일 격리를 제공하지 않는다는 것입니다. 하위 요소로 상속되는 속성은 @scope
의 하한값을 넘어 계속 상속됩니다. 이러한 속성 중 하나가 color
속성입니다. 도넛 범위 내에서 이를 선언할 때 color
는 여전히 도넛 범위 안에 있는 하위 요소로 상속됩니다.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
위의 예에서 .card__content
요소와 그 하위 요소는 .card
의 값을 상속하므로 hotpink
색상이 지정됩니다.
(표지 사진: rustam burkhanov Unsplash)