게시일: 2024년 10월 8일
CSS 중첩의 몇 가지 이상한 버그를 수정하기 위해 CSS 작업 그룹은 CSSNestedDeclarations
인터페이스를 CSS 중첩 사양에 추가하기로 결정했습니다. 이번 추가로 스타일 규칙 뒤에 오는 선언이 더 이상 위로 이동하지 않으며, 그 밖에도 몇 가지 개선사항이 있습니다.
이러한 변경사항은 Chrome 버전 130부터 사용할 수 있으며 Firefox Nightly 132 및 Safari Technology Preview 204에서 테스트할 수 있습니다.
브라우저 지원
CSSNestedDeclarations
없이 CSS 중첩 문제
CSS 중첩의 문제 중 하나는 원래 다음 스니펫이 처음 예상한 대로 작동하지 않는다는 것입니다.
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
코드를 보면 <div class=foo>
요소에 green
background-color
가 있다고 가정할 수 있습니다. background-color: green;
선언이 마지막에 나오기 때문입니다. 하지만 버전 130 이전의 Chrome에서는 그렇지 않습니다. CSSNestedDeclarations
를 지원하지 않는 버전에서 요소의 background-color
는 red
입니다.
130 이전의 Chrome에서 실제 규칙을 파싱한 후 사용되는 방식은 다음과 같습니다.
.foo {
width: fit-content;
background-color: green;
@media screen {
& {
background-color: red;
}
}
}
파싱 후 CSS에 두 가지 변경사항이 적용되었습니다.
background-color: green;
가 위로 이동하여 다른 두 선언과 합류했습니다.- 중첩된
CSSMediaRule
가&
선택기를 사용하여 선언을 추가CSSStyleRule
로 래핑하도록 다시 작성되었습니다.
여기서 볼 수 있는 또 다른 일반적인 변경사항은 파서가 지원하지 않는 속성을 삭제하는 것입니다.
CSSStyleRule
에서 cssText
를 다시 읽어 '파싱 후 CSS'를 직접 검사할 수 있습니다.
이 대화형 플레이그라운드에서 직접 사용해 보세요.
이 CSS가 재작성되는 이유는 무엇인가요?
이 내부 재작성이 발생한 이유를 이해하려면 이 CSSStyleRule
가 CSS 객체 모델 (CSSOM)에서 어떻게 표현되는지 이해해야 합니다.
Chrome 130 이전에서는 이전에 공유된 CSS 스니펫이 다음과 같이 직렬화됩니다.
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 1) =
- background-color: red
CSSStyleRule
에 있는 모든 속성 중에서 이 경우 다음 두 가지 속성이 관련이 있습니다.
- 선언을 나타내는
CSSStyleDeclaration
인스턴스인style
속성 - 중첩된 모든
CSSRule
객체를 보유하는CSSRuleList
인cssRules
속성
CSS 스니펫의 모든 선언이 CSStyleRule
의 style
속성에 끝나므로 정보가 손실됩니다. style
속성을 살펴보면 background-color: green
가 중첩된 CSSMediaRule
뒤에 선언되었는지 알 수 없습니다.
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ …
이는 CSS 엔진이 제대로 작동하려면 스타일 규칙 콘텐츠의 시작 부분에 표시되는 속성을 다른 규칙과 섞여 표시되는 속성과 구분할 수 있어야 하기 때문에 문제가 됩니다.
CSSMediaRule
내부의 선언이 갑자기 CSSStyleRule
로 래핑되는 경우: CSSMediaRule
가 선언을 포함하도록 설계되지 않았기 때문입니다.
CSSMediaRule
는 cssRules
속성을 통해 액세스할 수 있는 중첩된 규칙을 포함할 수 있으므로 선언은 자동으로 CSSStyleRule
로 래핑됩니다.
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 1) =
- background-color: red
이 문제를 해결하려면 어떻게 해야 하나요?
CSS 작업 그룹은 이 문제를 해결하기 위한 몇 가지 옵션을 살펴봤습니다.
제안된 해결 방법 중 하나는 중첩 선택자 (&
)를 사용하여 모든 노출 선언을 중첩된 CSSStyleRule
로 래핑하는 것이었습니다. 이 아이디어는 &
가 :is(…)
로 디슈가링될 때 발생하는 다음과 같은 원치 않는 부작용 등의 여러 가지 이유로 폐기되었습니다.
- 이는 특이성에 영향을 미칩니다. 이는
:is()
가 가장 구체적인 인수의 특정성을 인계받기 때문입니다. - 원래 외부 선택기의 가상 요소와는 잘 작동하지 않습니다. 이는
:is()
가 선택자 목록 인수에서 가상 요소를 허용하지 않기 때문입니다.
다음과 같은 예시를 살펴보겠습니다.
#foo, .foo, .foo::before {
width: fit-content;
background-color: red;
@media screen {
background-color: green;
}
}
Chrome 130 이전에는 이 스니펫을 파싱하면 다음과 같이 됩니다.
#foo,
.foo,
.foo::before {
width: fit-content;
background-color: red;
@media screen {
& {
background-color: green;
}
}
}
&
선택기로 중첩된 CSSRule
는 다음과 같은 이유로 문제가 됩니다.
:is(#foo, .foo)
까지 아래로 평면화되며, 그 과정에서.foo::before
가 선택기 목록에서 삭제됩니다.- 나중에 덮어쓰기가 더 어려워지는
(1,0,0)
의 특이성을 갖습니다.
규칙이 직렬화되는 항목을 검사하여 이를 확인할 수 있습니다.
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "#foo, .foo, .foo::before"
.resolvedSelectorText = "#foo, .foo, .foo::before"
.specificity = (1,0,0),(0,1,0),(0,1,1)
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: red
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(#foo, .foo, .foo::before)"
.specificity = (1,0,0)
.style (CSSStyleDeclaration, 1) =
- background-color: green
시각적으로는 .foo::before
의 background-color
가 green
대신 red
임을 의미합니다.
CSS 작업 그룹에서 고려한 또 다른 접근 방식은 모든 중첩 선언을 @nest
규칙으로 래핑하는 것이었습니다. 이로 인해 개발자 환경이 저하되어 취소되었습니다.
CSSNestedDeclarations
인터페이스 소개
CSS 작업 그룹이 결정한 해결 방법은 중첩 선언 규칙을 도입하는 것입니다.
이 중첩 선언 규칙은 Chrome 130부터 Chrome에 구현됩니다.
브라우저 지원
중첩 선언 규칙을 도입하면 CSS 파서가 연속된 직접 중첩 선언을 CSSNestedDeclarations
인스턴스로 자동으로 래핑하도록 변경됩니다. 직렬화되면 이 CSSNestedDeclarations
인스턴스는 CSSStyleRule
의 cssRules
속성에 포함됩니다.
다음 CSSStyleRule
를 예로 다시 들겠습니다.
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Chrome 130 이상에서 직렬화하면 다음과 같이 표시됩니다.
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = (0,1,0)
.style (CSSStyleDeclaration, 1) =
- width: fit-content
.cssRules (CSSRuleList, 2) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- background-color: red
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- background-color: green
CSSNestedDeclarations
규칙은 CSSRuleList
에서 끝나므로 파서는 background-color: green
선언의 위치를 유지할 수 있습니다. 즉, CSSMediaRule
의 일부인 background-color: red
선언 뒤에 있습니다.
또한 CSSNestedDeclarations
인스턴스가 있으면 현재 삭제된 잠재적인 솔루션이 될 수 있는 다른 부작용이 발생하지 않습니다. 중첩 선언 규칙은 동일한 특정성 동작으로 상위 스타일 규칙과 정확히 동일한 요소 및 의사 요소와 일치합니다.
CSSStyleRule
의 cssText
를 다시 읽으면 이를 확인할 수 있습니다. 중첩 선언 규칙 덕분에 입력 CSS와 동일합니다.
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
정책에 대한 이해
즉, Chrome 130부터 CSS 중첩이 훨씬 개선되었습니다. 그러나 중첩 규칙이 있는 기본 선언을 인터리빙하는 경우 일부 코드를 검토해야 할 수도 있습니다.
멋진 @starting-style
를 사용하는 다음 예를 살펴보세요.
/* This does not work in Chrome 130 */
#mypopover:popover-open {
@starting-style {
opacity: 0;
scale: 0.5;
}
opacity: 1;
scale: 1;
}
Chrome 130 이전에는 이러한 선언이 호이스팅되었습니다. opacity: 1;
및 scale: 1;
선언이 CSSStyleRule.style
로 이동하고 그 뒤에 CSSStyleRule.cssRules
에 CSSStartingStyleRule
(@starting-style
규칙을 나타냄)가 표시됩니다.
Chrome 130부터 선언이 더 이상 호이스팅되지 않으며 CSSStyleRule.cssRules
에 중첩된 CSSRule
객체가 2개 생성됩니다. 순서대로 CSSStartingStyleRule
(@starting-style
규칙을 나타냄) 1개와 opacity: 1; scale: 1;
선언을 포함하는 CSSNestedDeclarations
1개가 있습니다.
이러한 동작이 변경되어 @starting-style
선언이 CSSNestedDeclarations
인스턴스에 포함된 선언으로 덮어쓰여져 진입 애니메이션이 삭제됩니다.
코드를 수정하려면 @starting-style
블록이 일반 선언 뒤에 오는지 확인합니다. 다음과 같습니다.
/* This works in Chrome 130 */
#mypopover:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.5;
}
}
CSS 중첩을 사용할 때 중첩된 선언을 중첩된 규칙 위에 유지하면 CSS 중첩을 지원하는 모든 브라우저의 모든 버전에서 코드가 대부분 제대로 작동합니다.
마지막으로 CSSNestedDeclarations
의 사용 가능 여부를 기능 감지하려면 다음 JavaScript 스니펫을 사용하면 됩니다.
if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
// CSSNestedDeclarations is not available
}