Logo

CSS Nesting으로 선택자 중첩하기

CSS 네스팅(Nesting)은 어떤 선택자 내부에 다른 선택자를 넣어서 계층적인 스타일이 가능케하는 문법입니다. 예전에는 SCSS, SASS, LESS와 같은 CSS 전처리기를 사용해야 쓸 수 있던 기능이 었지만, 최근에는 CSS 표준에도 CSS Nesting이 정식으로 도입되어 Vanilla CSS에서도 사용이 가능해졌습니다.

이번 포스팅에서는 CSS Nesting을 왜 사용해야하고 어떻게 사용하는지에 대해서 알아보겠습니다.

본 포스팅은 CSS 선택자(selector)와 결합자(combinator)에 대한 기본적인 이해가 필요합니다. 관련해서는 제가 별도의 포스팅에서 다루고 있으니 아래 링크를 참조 바랍니다.

CSS Nesting을 쓰면 뭐가 좋을까?

기존에 CSS Nesting을 사용하지 않았을 때 어떤 문제가 있었고, CSS Nesting이 어떻게 이러한 문제를 해결해주는지 간단히 짚고 넘어가면 좋을 것 같아요.

아래는 CSS Nesting이 지원되지 않던 시절에 웹사이트의 네비게이션을 스타일하는 코드입니다.

nav {
  /* 네비게이션 스타일 */
}

nav.light {
  /* 라이트 모드 네비게이션 스타일 */
}

nav.dark {
  /* 다크 모드 네비게이션 스타일 */
}

nav > li {
  /* 네비게이션 안에 있는 리스트 스타일 */
}

nav > li > ul {  /* 네비게이션 안에 있는 리스트 안에 있는 일반 아이템 스타일 */
}

nav > li > ul.new {  /* 네비게이션 안에 있는 리스트 안에 있는 새로운 아이템 스타일 */
}

nav a {
  /* 네비게이션 안에 있는 링크 스타일 */
}

nav a:visited {
  /* 네비게이션 안에 있는 링크의 방문 상태 스타일 */
}

nav a:focus,
nav a:hover {
  /* 네비게이션 안에 있는 링크의 포커스, 호버 상태 스타일 */
}

선택자들 간에 중복되는 부분이 참 많다는 것을 느낄 수 있으실텐데요.

예를 들어, 일반 아이템(ul)과 새로운 아이템의(ul.new) 선택자 간에는 .new가 마지막에 붙었다는 차이 밖에 없지만 동일한 부모 선택자(nav > li > )를 앞에 붙여줘야 합니다.

동일한 스타일을 이번에는 CSS Nesting을 적용하여 현대적인 스타일로 다시 작성해보겠습니다.

nav {
  /* 네비게이션 스타일 */

  &.light {
    /* 라이트 모드 네비게이션 스타일 */
  }

  &.dark {
    /* 다크 모드 네비게이션 스타일 */
  }

  > li {
    /* 네비게이션 안에 있는 리스트 스타일 */

    > ul {      /* 네비게이션 안에 있는 리스트 안에 있는 일반 아이템 스타일 */

      &.new {        /* 네비게이션 안에 있는 리스트 안에 있는 새로운 아이템 스타일 */
      }
    }
  }

  a {
    /* 네비게이션 안에 있는 링크 스타일 */

    &:visited {
      /* 네비게이션 안에 있는 링크의 방문 상태 스타일 */
    }

    &:focus,
    &:hover {
      /* 네비게이션 안에 있는 링크의 포커스, 호버 상태 스타일 */
    }
  }
}

어떤가요? 🤩

선택자 간에 중복이 사라진 것은 물론이고, 스타일 계층이 한 눈에 파악이 되지 않나요? CSS Nesting의 주요 장점에 대해서 간단히 정리를 해보면요.

  • 동일한 선택자를 반복할 필요가 없어집니다.
  • 관련있는 스타일이 가까운 곳에 묶어 집니다.
  • CSS 스타일과 HTML 마크업이 비슷한 계층적인 구조를 갖게 됩니다.
  • 스타일을 찾거나 편집, 삭제하는 것이 수월해 집니다.
  • 불필요한 클래스 사용을 줄이는데 도움이 됩니다.

위와 같은 장점들로 인해서 스타일 버그가 얼마나 줄고, 개발자 경험이 얼마나 향상되고, 유지보수가 얼마나 쉬워지는지는… 구구절절 말씀 안 드려도 되겠죠? 😉

마지막 장점에 대해서만 부연 설명드리자면, CSS Nesting가 지원되지 않을 때는 HTML 코드에 클래스를 과도하기 사용하는 관행이 있었습니다.

<article class="question">
  <header class="question__header">
    <h3 class="question__header__heading">질문</h3>
  </header>
  <p class="question__body">CSS Nesting을 써보셨나요?</p>
  <footer class="question__footer">
    <button class="btn secondary">아니오</button>
    <button class="btn primary"></button>
  </footer>
</article>

그리고 CSS 코드에서 원하는 요소를 선택하기 위해서 클래스 선택자를 주로 사용하였죠.

.question__header__heading {
  /* 어떤 스타일 */
}

.question__footer .btn.secondary {
  /* 어떤 스타일 */
}

반면에 CSS Nesting을 사용하면 모든 요소에 일일이 클래스를 붙여주지 않아도 스타일하고 싶은 요소로 범위를 좁혀나갈 수 있습니다.

article {
  > header {
    h3 {
      /* 어떤 스타일 */
    }
  }
  > footer {
    button.btn.secondary {
      /* 어떤 스타일 */
    }
  }
}

따라서 스타일 목적으로 HTML 요소에 클래스를 붙일 이유를 덜 느끼고 되고, 자연스럽게 클래스 작명에 대한 고민이나 스트레스도 사라지게 됩니다.

자, 그럼 지금부터 본격적으로 CSS Nesting을 사용하는 방법을 알아보겠습니다.

기본 선택자에 활용

선택자로 다른 선택자를 감싸게 되면 내부에 있는 선택자는 외부의 선택자의 문맥 안에서 적용이 됩니다. 다시 말해서, 내부 선택자는 외부 선택자의 자식 요소를 선택할 수 있게 됩니다.

예를 들어, <section> 요소의 자식인 <header>의 후손인 <h1><h2> 요소를 다음과 같이 선택할 수 있습니다. 여기서 > 기호는 자식 결합자(Child Combinator)이고, 공백 기호는 후손 결합자(Descendant Combinator)입니다.

section > header h1,
section > header h2 {
  /* 어떤 스타일 */
}

이 코드를 그대로 CSS Nesting으로 재작성해보면 다음과 같은 모습이 됩니다.

section {
  > header {
    h1,
    h2 {
      /* 어떤 스타일 */
    }
  }
}

동일한 코드를 & 기호를 사용하여 좀 더 명시적으로 작성할 수도 있습니다. & 기호는 요소 자신을 가리킵니다.

section {
  & > header {
    & h1,
    & h2 {
      /* 어떤 스타일 */
    }
  }
}

처음에 CSS Nesting 기능이 표준으로 논의가 되었을 때는 가급적 & 기호를 쓰라고 권장했습니다. 그럼에도 불구하고 많은 웹 개발자들이 & 기호가 반드시 필요한 상황이 아니면 생략하는 성향을 보이는 것 같아요. SCSS, SASS, LESS와 같은 CSS 전처리기를 사용했을 때 & 기호를 잘 쓰지 않았던 습관이 남아있기 때문일 것입니다.

& 기호는 스타일 목적에 다라서 매우 유용하게 쓰이기도 합니다. 특히 하나의 요소가 여러 클래스나 속성에 대해서 복잡하게 스타일이 되야할 때 유용합니다.

예를 들어, <input> 요소를 CSS Nesting 없이는 보통 아래와 같이 스타일하죠.

input.loading {
  /* 어떤 스타일 */
}

input.error {
  /* 어떤 스타일 */
}

input[disabled] {
  /* 어떤 스타일 */
}

input[disabled] {
  /* 어떤 스타일 */
}

input[type="text"] {
  /* 어떤 스타일 */
}

input[type="radio"] {
  /* 어떤 스타일 */
}

input[type="checkbox"] {
  /* 어떤 스타일 */
}

CSS Nesting을 적용하면 다음과 같이 & 기호가 많이 필요하게 됩니다.

input {
  /* 어떤 스타일 */

  &.error {
    /* 어떤 스타일 */
  }

  &.loading {
    /* 어떤 스타일 */
  }

  &[disabled] {
    /* 어떤 스타일 */
  }

  &[type="text"] {
    /* 어떤 스타일 */
  }

  &[type="radio"] {
    /* 어떤 스타일 */
  }

  &[type="checkbox"] {
    /* 어떤 스타일 */
  }
}

만약에 & 기호를 생략한다면 엉뚱하게 자식 요소에게 스타일이 적용될 것입니다. 선택자를 중첩할 때, 자신을 위한 스타일인지 자식을 위한 스타일인지 한 번 더 생각해보시면 도움이 됩니다.

의사 클래스에 활용

CSS Nesting으로 의사 클래스를 감싸줄 때는 & 기호를 더 자주 볼 수 있는데요. 의사 클래스는 자식이 아닌 자신의 상태를 스타일하기 위해서 사용되는 경우가 많기 때문입니다.

예를 들어, primary 클래스와 secondary 클래스가 붙은 <a> 요소의 4가지 상태에 대한 스타일을 CSS Nesting없이 작성해보겠습니다.

a.primary {
  font-weight: 500;
  color: var(--text-100);
  background-color: var(--bg-400);
}

a.primary:focus {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
  border-radius: 10px;
}

a.primary:focus-visible {
  outline: 3px solid var(--secondary);
  outline-offset: 2px;
  border-radius: 10px;
}

a.primary:hover {
  font-weight: var(--font-weight-bold);
  background-color: var(--bg-300);
}

a.primary:active {
  color: var(--text-300);
  background-color: var(--bg-900);
}

a.secondary {
  /* 어떤 스타일 */
}

a.secondary:focus {
  /* 어떤 스타일 */
}

a.secondary:focus-visible {
  /* 어떤 스타일 */
}

a.secondary:hover {
  /* 어떤 스타일 */
}

a.secondary:active {
  /* 어떤 스타일 */
}

CSS Nesting을 적용하면 클래스 별로 스타일을 묶어줄 수 있습니다. primary 클래스가 붙은 <a> 요소에 대한 스타일과 secondary 클래스가 붙은 <a> 요소에 대한 스타일이 서로 격리되어 간에 구분이 참 쉬어지죠?

a.primary {
  font-weight: 500;
  color: var(--text-100);
  background-color: var(--bg-400);

  &:focus {
    outline: 3px solid var(--primary);
    outline-offset: 2px;
    border-radius: 10px;
  }

  &:focus-visible {
    outline: 3px solid var(--secondary);
    outline-offset: 2px;
    border-radius: 10px;
  }

  &:hover {
    font-weight: var(--font-weight-bold);
    background-color: var(--bg-300);
  }

  &:active {
    color: var(--text-300);
    background-color: var(--bg-900);
  }
}

a.secondary {
  /* 어떤 스타일 */

  &:focus {
    /* 어떤 스타일 */
  }

  &:focus-visible {
    /* 어떤 스타일 */
  }

  &:hover {
    /* 어떤 스타일 */
  }

  &:active {
    /* 어떤 스타일 */
  }
}

미디어 쿼리에 활용

CSS Nesting은 미디어 쿼리나 컨테이너 쿼리를 사용할 때도 빛을 발합니다.

예를 들어, 반응형(Responsive) 글꼴을 CSS Nesting 없이 스타일하면 보통 아래와 같이 작성을 하는데요.

h1 {
  font-size: 2rem;
}

h2 {
  font-size: 1.5rem;
}

p {
  font-size: 1rem;
}

@media (min-width: 480px) {
  h1 {
    font-size: 2.75rem;
  }

  h2 {
    font-size: 2rem;
  }

  p {
    font-size: 1.25rem;
  }
}

@media (min-width: 1024px) {
  h1 {
    font-size: 3.5rem;
  }

  h2 {
    font-size: 2.5rem;
  }

  p {
    font-size: 1.5rem;
  }
}

여기서 한번 <h2> 요소에 대한 스타일만 찾아보시겠어요? 화면 폭에 따른 스타일이 흩어져 있어서 찾기가 좀 불편하실 거에요. 실제 프로젝트에서는 이보다 스타일이 훨씬 더 많기 때문에 동일한 HTML 요소에 대한 스타일을 찾는 게 더 고통스러울 수 있죠.

이번에는 CSS Nesting을 활용하여 미디어 쿼리를 각 요소에 대한 선택자 속으로 넣어보겠습니다.

h1 {
  font-size: 2rem;

  @media (min-width: 480px) {
    font-size: 2.75rem;
  }

  @media (min-width: 1024px) {
    font-size: 3.5rem;
  }
}

h2 {
  font-size: 1.5rem;

  @media (min-width: 480px) {
    font-size: 2rem;
  }

  @media (min-width: 1024px) {
    font-size: 2.5rem;
  }
}

p {
  font-size: 1rem;

  @media (min-width: 480px) {
    font-size: 1.25rem;
  }

  @media (min-width: 1024px) {
    font-size: 1.5rem;
  }
}

어떤가요? 각 HTML 요소의 글꼴 크기가 화면 폭에 따라서 어떻게 변하는지 훨씬 직관적으로 파악이 되죠?

우리 개발자들은 화면 폭에 따라 사고하기 보다는 요소 별로 사고하는 경우가 많기 때문에, 이러한 방식으로 스타일을 묶어 두면 스타일 작성 뿐만 아니라 디버깅이 훨씬 수월해집니다.

실전 예제

좀 더 현실에서 볼 법한 예제를 제공해드리기 위해서, CSS Nesting을 활용하여 웹에서 쉽게 볼 수 있는 4가지 종류(기본, 성공, 오류, 경고)의 버튼을 스타일해보았습니다.

마치면서

지금까지 CSS Nesting의 기본 사용법을 알아보고 다양한 상황에서 어떻게 실제 스타일에 활용할 수 있는지 배워보았습니다.

Nesting을 CSS 자체적으로 지원되기를 기다리던 개발자들이 너무나 많았으며, SCSS나 SASS와 같은 CSS 전처리기를 가장 큰 이유 중 하나 였습니다. 그래서 개인적으로 CSS Variables과 더불이 CSS Nesting이 최근에 CSS에 있었던 가장 영향력이 큰 변화라고 생각합니다.

읽기 편하고 유지 보수가 쉬운 현대적인 CSS를 작성하시는데 본 포스팅이 도움이 되었으면 좋겠습니다.