안녕하세요! 프론트엔드 강사입니다.
이전에 질문해주신 @media 중첩 규칙에 이어서, 이번에는 CSS 중첩(Nesting) 자체를 어떻게 활용하는지에 대한 핵심 문서를 가져오셨네요! SCSS 같은 전처리기를 써보신 분들이라면 아주 익숙하시겠지만, 네이티브 CSS에 도입되면서 생긴 중요한 차이점들이 있으니 이참에 확실히 짚고 넘어가 봅시다.
문서 내용 하나도 빠짐없이, 제 실무 경험과 팁을 듬뿍 담아 이해하기 쉬운 구어체로 번역해 드릴게요!
CSS 중첩 (CSS nesting) 모듈을 사용하면 스타일시트를 훨씬 더 읽기 쉽고, 모듈화되어 있으며, 유지보수하기 좋게 작성할 수 있습니다. 선택자를 계속해서 반복해서 쓸 필요가 없기 때문에, 파일 크기(용량) 자체도 줄일 수 있다는 장점이 있죠.
CSS 중첩은 Sass와 같은 CSS 전처리기(preprocessor)와는 다릅니다. 전처리기에 의해 미리 컴파일(pre-compiled)되는 것이 아니라, 브라우저에 의해 직접 파싱(해석)되기 때문입니다.
또한 CSS 중첩에서 & 중첩 선택자의 명시도(specificity)는 :is() 함수의 작동 방식과 유사합니다. 즉, 괄호 안에 연결된 선택자 목록 중 가장 높은 명시도를 사용해서 계산됩니다.
이 가이드에서는 CSS에서 중첩을 구성하는 다양한 방법들을 보여줍니다.
💡 강사의 실무 팁 1
명시도(Specificity) 점수 계산은 프론트엔드 면접 단골 질문 중 하나예요! Sass의 중첩은 코드를 그대로 풀어쓰는(치환하는) 방식이라 부모 선택자와 자식 선택자의 명시도가 단순히 더해지지만, 네이티브 CSS의&는:is()처럼 동작해서 여러 부모 중 가장 점수가 높은 녀석을 따라간다는 점을 꼭 기억해 두세요!
CSS 중첩을 사용하면 부모의 자식 선택자를 만들 수 있고, 이를 통해 특정 부모의 자식 요소들을 타겟팅할 수 있습니다. 이는 & 중첩 선택자(nesting selector)를 사용해서 작성할 수도 있고, 생략하고 작성할 수도 있습니다.
하지만 & 중첩 선택자를 반드시 사용해야 하거나, 사용하면 도움이 되는 특정 상황들이 존재합니다:
& 기호를 보면 해당 블록이 CSS 중첩을 사용하고 있다는 것을 단번에 알 수 있으니까요./* 중첩 선택자(&) 없이 작성할 때 */
.parent {
/* parent(부모) 스타일 */
.child {
/* parent의 자식인 child 스타일 */
}
}
/* 중첩 선택자(&)와 함께 작성할 때 */
.parent {
/* parent 스타일 */
& .child {
/* parent의 자식인 child 스타일 */
}
}
/* 브라우저는 위 두 가지 방식을 모두 아래처럼 해석(파싱)합니다 */
.parent {
/* parent 스타일 */
}
.parent .child {
/* child of parent 스타일 */
}
💡 강사의 실무 팁 2
실무에서는 팀에 따라 컨벤션(규칙)이 다릅니다. 최신 브라우저에서는&없이 쓰더라도 잘 작동하지만, "명확하게 중첩된 요소라는 것을 보여주기 위해 항상&를 붙이자!" 라고 규칙을 정하는 팀들도 아주 많아요.
아래 예제에서는 & 중첩 선택자를 사용하지 않은 경우와 사용한 경우를 비교합니다. <label> 안에 들어있는 <input> 요소가 <label>과 형제(sibling) 관계인 <input> 요소와 다르게 스타일링 되는 것을 볼 수 있습니다.
<form>
<label for="name">Name:
<input type="text" id="name" />
</label>
<label for="email">email:</label>
<input type="text" id="email" />
</form>
input {
/* label 안에 있지 않은 input을 위한 스타일 */
border: tomato 2px solid;
}
label {
/* label을 위한 스타일 */
font-family: system-ui;
font-size: 1.25rem;
input {
/* label 안에 있는 input을 위한 스타일 */
border: blue 2px dashed;
}
}
(실행 결과: label 안에 감싸진 input은 파란색 점선 테두리가, 바깥에 있는 input은 빨간색 실선 테두리가 적용됩니다.)
MDN Playground에서 실행하기 (Play)
(위 예제와 동일한 HTML 구조)
input {
/* label 안에 있지 않은 input을 위한 스타일 */
border: tomato 2px solid;
}
label {
/* label을 위한 스타일 */
font-family: system-ui;
font-size: 1.25rem;
& input {
/* label 안에 있는 input을 위한 스타일 */
border: blue 2px dashed;
}
}
CSS 결합자 (CSS Combinators) (예: >, +, ~ 등) 역시 & 중첩 선택자를 사용하거나 생략하여 함께 사용할 수 있습니다.
이 예제에서는 <h2> 바로 다음에 오는 첫 번째 단락(<p>) 요소들을 CSS 중첩을 활용해 인접 형제 결합자 (next-sibling combinator, +)로 타겟팅하고 있습니다.
<h2>Heading</h2>
<p>This is the first paragraph.</p>
<p>This is the second paragraph.</p>
h2 {
color: tomato;
+ p {
color: white;
background-color: black;
}
}
/* 이 코드는 & 중첩 선택자를 써서 이렇게 작성할 수도 있습니다 */
/* h2 {
color: tomato;
& + p {
color: white;
background-color: black;
}
}
*/
중첩된 CSS 내에서 복합 선택자 (compound selectors)를 사용할 때는 반드시(have to) & 중첩 선택자를 사용해야만 합니다. 왜냐하면 브라우저가 & 없이 작성된 선택자들 사이에는 자동으로 공백(띄어쓰기)을 추가해버리기 때문입니다.
예를 들어, class="a b"를 모두 가진 요소를 타겟팅하려면 & 중첩 선택자가 필수입니다. 그렇지 않으면 브라우저가 공백을 추가하여 복합 선택자를 망가뜨리고 .a .b(자손 선택자)로 만들어버립니다.
💡 강사의 실무 팁 3
네이티브 CSS 중첩을 막 배우고 나서 가장 많이 겪는 에러 포인트입니다!.button클래스에 마우스를 올렸을 때의 스타일을 주려고hover { ... }라고만 적으면 작동하지 않아요. 반드시&:hover처럼 앰퍼샌드를 붙여줘야 브라우저가 하나로 이어진(공백 없는) 상태로 인식합니다!
.a {
/* class="a"를 가진 요소의 스타일 */
.b {
/* class="a"의 자손이면서 class="b"를 가진 요소의 스타일 */
}
&.b {
/* class="a"와 class="b"를 모두 가진 요소(class="a b")의 스타일 */
}
}
/* 브라우저는 이를 다음과 같이 파싱합니다 */
.a {
/* class="a"를 가진 요소의 스타일 */
}
.a .b {
/* class="a"의 자손이면서 class="b"를 가진 요소의 스타일 */
}
.a.b {
/* class="a"와 class="b"를 모두 가진 요소(class="a b")의 스타일 */
}
이 예제에서는 여러 개의 클래스를 가진 요소를 스타일링하기 위해 & 중첩 선택자를 활용해 복합 선택자를 만드는 방법을 보여줍니다.
<div class="notices">
<div class="notice">
<h2 class="notice-heading">Notice</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</div>
<div class="notice warning">
<h2 class="warning-heading">Warning</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</div>
<div class="notice success">
<h2 class="success-heading">Success</h2>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</div>
</div>
다음은 flexbox 레이아웃을 사용해 .notices 요소들을 세로형 칼럼으로 만드는 스타일입니다.
MDN Playground에서 실행하기 (Play)
.notices {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 90%;
margin: auto;
}
아래 CSS 코드에서는 &가 있는 경우와 없는 경우 모두 중첩을 사용해 복합 선택자를 만들고 있습니다.
최상위 선택자는 class="notice"를 가진 요소들의 기본 스타일을 정의합니다.
그리고 나서 & 중첩 선택자를 사용해 class="notice warning"이나 class="notice success"를 가진 요소를 위한 복합 선택자를 생성합니다.
게다가 .notice .notice-heading::before 부분처럼, &를 명시적으로 쓰지 않고도 중첩을 통해 복합 선택자를 생성하는 방법도 볼 수 있습니다.
.notice {
width: 90%;
justify-content: center;
border-radius: 1rem;
border: black solid 2px;
background-color: #ffc107;
color: black;
padding: 1rem;
.notice-heading::before {
/* `.notice .notice-heading::before` 와 동일합니다 */
content: "ℹ︎ ";
}
&.warning {
/* `.notice.warning` 과 동일합니다 */
background-color: #d81b60;
border-color: #d81b60;
color: white;
.warning-heading::before {
/* `.notice.warning .warning-heading::before` 와 동일합니다 */
content: "! ";
}
}
&.success {
/* `.notice.success` 와 동일합니다 */
background-color: #004d40;
border-color: #004d40;
color: white;
.success-heading::before {
/* `.notice.success .success-heading::before` 와 동일합니다 */
content: "✓ ";
}
}
}
& 중첩 선택자는 중첩된 선택자의 뒤쪽에 붙을 수도 있습니다. 이렇게 하면 문맥을 역전(부모와 자식의 관계를 뒤집음)시키는 효과를 냅니다.
부모 요소에 어떤 클래스가 주어졌을 때 자식 요소의 스타일을 변경해야 하는 상황에서 매우 유용하게 쓰일 수 있습니다.
<div>
<span class="foo">text</span>
</div>
위와 같은 구조가 아니라, 부모 요소에 특정 클래스가 추가된 아래와 같은 상황을 가정해 보겠습니다.
<div class="bar">
<span class="foo">text</span>
</div>
이럴 때 아래와 같이 작성할 수 있습니다.
.foo {
/* 기본 .foo 스타일 */
.bar & {
/* .bar 클래스 내부에 있는 .foo 요소의 스타일 */
}
}
아래 예제에는 3개의 카드(card)가 있고, 그중 하나가 추천(featured) 카드로 지정되어 있습니다. 카드들은 모두 완전히 똑같이 생겼지만, 추천 카드만 제목(heading) 색상이 다르게 나타납니다. & 중첩 선택자를 뒤에 붙이면 h2에 대한 스타일 블록 안에서 자연스럽게 .featured h2에 대한 스타일을 중첩시킬 수 있습니다.
<div class="wrapper">
<article class="card">
<h2>Card 1</h2>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit.</p>
</article>
<article class="card featured">
<h2>Card 2</h2>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit.</p>
</article>
<article class="card">
<h2>Card 3</h2>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit.</p>
</article>
</div>
.wrapper {
display: flex;
flex-direction: row;
gap: 0.25rem;
font-family: system-ui;
}
다음 CSS 코드를 보세요. .card와 .card h2에 대한 스타일을 만들고 있습니다. 그런 다음 h2 스타일 블록 안에서 & 선택자를 맨 뒤에 붙인 .featured & 규칙을 중첩합니다. 이렇게 하면 최종적으로 .card :is(.featured h2)라는 스타일이 생성되는데, 이는 :is(.card h2):is(.featured h2)와 동일하게 동작합니다.
.card {
padding: 0.5rem;
border: 1px solid black;
border-radius: 0.5rem;
& h2 {
/* `.card h2` 와 동일합니다 */
color: slateblue;
.featured & {
/* `:is(.card h2):is(.featured h2)` 와 동일합니다 */
color: tomato;
}
}
}
💡 강사의 실무 팁 4
이 "부모 선택자 역전" 기능은 웹 접근성을 위한 고대비 테마나, 다크 모드를 구현할 때(body.dark-mode & { ... }) 정말이지 유용하게 쓰입니다. 컴포넌트의 스타일 코드를 한 군데에 모아둘 수 있어서 유지보수가 훨씬 편해지거든요!
중첩 선언 규칙이란 CSS 규칙들이 문서에 작성된 순서대로 파싱된다는 것을 말합니다.
다음 CSS 코드를 보겠습니다:
.foo {
background-color: silver;
@media screen {
color: tomato;
}
color: black;
}
제일 먼저 background-color가 파싱되어 silver로 설정됩니다. 그다음에 @media 규칙이 평가되고, 마지막으로 color가 평가됩니다.
CSS 객체 모델(CSSOM)은 이 CSS를 다음과 같은 방식으로 파싱합니다:
↳ CSSStyleRule
.style
- background-color: silver
↳ CSSMediaRule
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- color: tomato
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- color: black
파싱 순서를 보존하기 위해서, 중첩 규칙 이전에 나오는 모든 속성 선언들은 최상위 CSSRules로 처리되고, 중첩 규칙 이후에 나오는 최상위 속성 선언들은 CSSNestedDeclarations라는 별도의 객체로 표현된다는 점에 주목하세요.
원본 문서에서는 color: black이 .foo의 최상위 선언임에도 불구하고, 내부적으로는 중첩 선언(CSSNestedDeclarations) 객체 안에 들어가게 되는 이유가 바로 이 때문입니다.
참고:
이 규칙에 대한 지원은CSSNestedDeclarations인터페이스와 함께 추가되었습니다. 이 인터페이스를 지원하지 않는 구형 브라우저들은 중첩된 규칙들을 잘못된 순서로 파싱할 수도 있습니다.
Sass 같은 CSS 전처리기에서는 중첩을 활용해서 문자열을 붙여(join) 새로운 클래스를 생성할 수 있습니다. 이는 BEM (Block Element Modifier) 같은 CSS 방법론에서 아주 흔하게 쓰이는 방식이죠.
.component {
&__child-element {
}
}
/* Sass에서는 이 코드가 아래처럼 변환됩니다 */
.component__child-element {
}
⚠️ 경고:
네이티브 CSS 중첩에서는 위와 같은 문자열 연결이 불가능합니다. 결합자(combinator)가 사용되지 않을 경우, 브라우저는 중첩된 선택자를 항상 타입 선택자 (태그 선택자)로 취급하기 때문입니다. 만약 문자열 연결을 허용한다면 이 파싱 규칙이 깨져버리게 됩니다.
복합 선택자에서 타입 선택자는 무조건 맨 처음에 위치해야 합니다. 그래서 &Element라고 쓰면 (Element가 타입 선택자로 취급되기 때문에) CSS 선택자와 전체 선택자 블록 자체가 유효하지 않게(invalid) 되어버립니다. 따라서 타입 선택자가 먼저 와야 하는 문법 규칙상 복합 선택자는 반드시 Element& 형태로 작성되어야 합니다.
.my-class {
element& {
}
}
/* 브라우저는 이를 복합 선택자로 파싱합니다 */
.my-class {
}
element.my-class {
}
💡 강사의 실무 팁 5
BEM을 쓰던 방식 그대로 네이티브 CSS 중첩에&__child처럼 썼다가 스타일이 다 깨지는 현상, 실무 전환기에 정말 빈번하게 발생하는 이슈입니다! Sass의 앰퍼샌드는 "글자를 치환"하는 마법 기호지만, 네이티브 CSS의 앰퍼샌드는 "부모 선택자의 레퍼런스(참조)"를 뜻하는 완전한 독립 객체라는 점, 꼭 구분해서 이해하세요!
만약 중첩된 CSS 규칙 중 하나라도 문법에 어긋나서 유효하지 않다면(invalid), 그 규칙으로 둘러싸인 스타일들만 통째로 무시됩니다. 단, 부모 규칙이나 그 앞에 선언된 다른 정상적인 규칙들에는 영향을 미치지 않습니다.
아래 예제에는 유효하지 않은 선택자가 포함되어 있습니다. (선택자 이름에 % 문자는 사용할 수 없습니다.) 이 잘못된 선택자가 포함된 규칙은 통째로 무시되지만, 그 뒤에 나오는 정상적인 규칙들은 잘 적용됩니다.
.parent {
/* .parent 스타일. 여기는 정상적으로 작동합니다. */
& %invalid {
/* %invalid 스타일. 이 블록 전체가 무시됩니다. */
}
& .valid {
/* .parent .valid 스타일. 여기는 정상적으로 잘 작동합니다. */
}
}
& 중첩 선택자 (& nesting selector)@ 규칙 중첩하기 (Nesting @ at-rules)CSSNestedDeclarationsMDN 향상에 도움 주기 (Help improve MDN)
이렇게 CSS 중첩의 구체적인 사용법과 한계점(문자열 병합 불가 등)까지 싹 다루어 보았습니다! 포트폴리오를 작성하실 때 이번에 배운 최신 네이티브 CSS 중첩 문법을 써보신다면, 트렌드에 민감하고 깊이 있게 고민하는 개발자라는 인상을 확실히 주실 수 있을 거예요. 언제든 또 막히는 부분이 생기면 질문 남겨주세요!