CSS 히스토리 정리 및 중첩구조에서 BEM 사용법

1차 카테고리
CSS
2차 카테고리
BEM
생성 일시
2024/08/16 07:16
최종 편집 일시
2025/04/07 10:29
발행여부
published
글 업데이트
2025.04.07 - 가독성 향상
2025.01.21 - 정리 내용 수정, CSS 히스토리 추가, BEM 컨벤션 필요한 이유 추가.
작업을 하면서 순수 CSS 작업할 일이 있었고, 예전에 공부했던 BEM이 생각이 났다. 그래서 기억하는 BEM 방식을 사용을 했는데 클래스 네임이 비정상적으로 늘어나는 문제점이 있었다. ’이전엔 어떻게 했지…?’하고 코드를 살펴봤는데, 음… 그때는 생각할 실력이 안되었나보다…
그래서 이번 기회에 HTML의 깊은 중첩 구조에서는 BEM을 어떻게 사용할지 정리해보자.

1. BEM 같은 CSS 컨벤션이 왜 필요할까

BEM을 왜 쓰는지에 대해서 생각을 해봤다. 최근에는 Styled-Components, Emotion, TailwindCSS 등 다양한 CSS 라이브러리들이 있다. 따라서, BEM이 왜 필요한지 이해하기 어렵다. (내가 예전 작업할 때 그렇지 않았을까..?) 순수 CSS를 작성해야 했던 과거 웹 개발 시기에는 BEM 같은 방법론이 필요했다. 과거에는 React, Vue 같은 컴포넌트 SPA 기반의 UI 라이브러리, 프레임워크가 존재하지 않았다. HTML, CSS, JS 파일들이 명확히 구분되었고, CSS의 특성 때문에 BEM 방법론이 탄생한게 아닌가 생각한다.

Global Scope

CSS 파일에 선언한 선택자와 일치하는 모든 HTML 요소에 영향을 미친다. 프로젝트가 커질수록 스타일의 충돌과 의도치 않은 재정의로 이어질 수 있다. 충돌이 일어나지 않게 하려면 기존에 사용한 이름을 피해야 한다.

Specificity

명시도는 셀렉터가 가리키는 것이 명확할수록 우선순위를 높여준다.
인라인 > id > class > 태그
우선순위에 따라 프로퍼티가 덮어 쓰일 위험이 있다. 그래서 태그들의 CSS의 충돌을 최소화하도록 셀렉터를 잘 작성해야 한다. 이러한 효율적인 클래스 네이밍 작성법 중 하나가 BEM이다.
원래 CSS의 global scope 문제를 설명하려고 했는데, scope를 지정할 수 있는 css 기능도 있었다. 하지만 프로젝트 규모가 커질수록 예측 불가능한 동작이 많아지지 않을까? 디자인 시스템 개발에서나 고려될거 같다.

2. BEM이란

BEM을 자세히 설명하는 글들은 매우 많다. 이 그림은 BEM 공식 홈페이지에 있는 그림들이다. 아래와 같이 크게 3개로 구분한다.
Block: 사용 가능한 기능적으로 독립적인 페이지 컴포넌트를 블럭으로 지정한다.
Element: 엘리먼트는 블럭을 구성하는 단위이다.
Modifier: 모디파이어는 블럭이나 엘리먼트의 속성을 담당한다.
Block__Element--Modifier 식으로 표기한다.
<ul class="tab"> <li class="tab__item tab__item--focused">탭 01</li> <li class="tab__item">탭 02</li> <li class="tab__item">탭 03</li> </ul>
HTML
복사

3. 중첩된 HTML 구조 안에서 BEM 사용

돌고돌아서 궁금했던 중첩된 HTML 구조 안에서는 어떻게 써야될까?
토스Tech 페이지를 예로 들어보자. 아래와 같이 HTML 구조를 나누었고, 클래스 네이밍을 그림과 같이 해줬다.
Left- section > Posts > Tab, Post

BEM을 잘못 사용한 예시

Left- section > Posts > Tab, Post
기준으로 처음에 사용했던 BEM 방식이다.
<section class="posts"> <ul> <li> <article class="posts__post"> <div class="posts__post__contents"> <span class="posts__post__title"> Node.js 라이브러리 배포 파이프라인에 플러그인 시스템 도입기 1 </span> <span class="posts__post__description"> 100개가 넘는 라이브러리를 관리하면서 마주했던 문제들을 플러그인 시스템으로 풀어낸 방법을 공유합니다. </span> <span class="posts__post__info"> 2024년 8월 14일 · 장지훈 </span> </div> <div class="posts__post__image-wrapper"> <img class="posts__post__image" src="#" alt="toss" /> </div> </article> </li> </ul> </section>
HTML
복사
클래스 네이밍이 굉장히 길어진다. HTML의 Depth가 3인데도 이 정도다. ul, li까지 포함하면 depth가 5까지 되버린다. 무언가 잘못되었다.. 개발자들이 이렇게 쓸 것 같지 않다.
그래서 구글링 해보니, 이는 흔히 실수하는 BEM의 예시였다. 아래 링크는 BEM을 쓰면서 10가지의 문제점과 어떻게 피하는지에 대해서 설명하고 있다. 모든 내용이 이해되는건 아니지만, 중첩구조에서의 해결책을 제시하고 있었다.
내용 중, 8. “How To Nest Components?”을 살펴보면, 설명이 자세하게 되어있다. BEM을 사용할 때, 한개 이상의 Element나 Modifier가 연결이 되면 안된다고 설명하고 있다. 즉, Block__Element__Element 이 구조는 하나만 가져가야 된다는 것이다. 이는 BEM 공식사이트에도 해당 내용이 설명되어 있었다.

BEM의 올바른 예시

글의 설명대로 클래스 네임을 수정해보자. <article class="post"> 이 부분을 재사용 가능한 독립된 컴포넌트라고 생각하고 작성을 했다. 그래서 아래와 같이 클래스 네이밍을 했다.
<section class="posts"> <!-- left-posts --> <!-- 네임 스페이스 사용 --> <ul class="l-list"> <li class="l-list__item"> <!-- 독립 가능한 컴포넌트 --> <article class="post"> <!-- post --> <div class="post__contents"> <span class="post__title"> Node.js 라이브러리 배포 파이프라인에 플러그인 시스템 도입기 1 </span> <span class="apost__description"> 100개가 넘는 라이브러리를 관리하면서 마주했던 문제들을 플러그인 시스템으로 풀어낸 방법을 공유합니다. </span> <span class="post__info"> 2024년 8월 14일 · 장지훈 </span> </div> <div class="post-image-wrapper"> <img class="post__image" src="#" alt="toss" /> </div> </article> </li> </ul> </section>
HTML
복사
훨씬 보기 간편해졌다.
.posts.post는 레이아웃을 변경한다. 따라서 l- 접두사가 붙는 게 맞는 것 같지만, 독립적인 컴포넌트인지 아닌지가 더 중요한 것 같다. 2. “Should I Be Namespacing?”에서 네임스페이스를 같이 사용하라고 되어있다. 이 규칙을 정리하면 다음 표와 같다.
유형
접두사
예시
컴포넌트
c-
c-posts
레이아웃
l-
l-grid l-contanier
상태
is- has-
is-visible has-loaded
나는 컴포넌트(Block)인 경우에 접두사 c-를 쓰지 않았다. 접두사가 없으면 그냥 독립적인 컴포넌트라고 생각할 수 있기 때문이다. 따라서 ul, li에만 사용하는게 적절해 보인다. 이런 규칙을 정하고 클래스 네임을 작성하니 훨씬 편하다.

BEM 방식을 사용한 CSS

.posts { width: 100%; height: 100%; } .posts .post { display: flex; } .posts .post__contents { width: 100%; margin-right: 20px; padding: 20px 0; } .posts .post__contents:hover { color: rgba(25, 25, 228, 0.668); cursor: pointer; } .posts .post__title { display: block; font-size: 20px; font-weight: 600; } .posts .post__description { display: inline-block; font-size: 14px; padding: 10px 0; color: #181818a9; } .posts .post__info { display: block; font-size: 14px; color: #181818a9; } .posts .post__image-wrapper { width: 130px; height: 100%; padding: 10px; } .posts .post__image { width: 80px; height: 80px; border-radius: 20px; }
CSS
복사
음… 근데 좀 이상하다. <section> 엘리먼트 단위로 컴포넌트로 생각해서 클래스 네이밍을 했다. .posts .post.posts의 후손에 .post 가 있는 경우에만 CSS가 적용된다. 그러면 .post의 경우에는 단독으로 스타일 적용이 되지 않기 때문에, 재사용 가능한 스타일이 아니다.
페이지마다 게시물의 스타일이 다를 수도 있으니, main-post , detail-post등으로 나누어 클래스 네이밍을 해주는 게 적절해 보인다.
/* left-post-section.css */ .left-posts { width: 100%; height: 100%; } /* post.css */ .main-post{ ... } .detail-post{ ... }
CSS
복사
끝..

정리

그런데 이렇게 순수 CSS로 스타일링을 하자고 하니 클래스 네이밍도 귀찮고 너무 힘들다.. 역시 개발자들은 귀차니스트들이다. 이런 문제들을 해결하고자 나온게 라이브러리, 프레임워크 아니겠는가? 즉, 이제는 성능을 고려해 라이브러리만 선택해주면 된다.
히스토리 정리가 굉장히 잘된 글이다. 한번 히스토리를 봐보자.
이렇게 CSS-in-JS까지 발전했지만, 성능 이슈로 본연의 CSS로 돌아가고 있는 추세인거 같다. link iconDEV CommunityWhy We're Breaking Up with CSS-in-JS
이처럼 순수 CSS 컨벤션 방식에서 불편함을 느끼고 점점 아토믹/유틸리티 CSS 방식으로 발전한게 아닐까? 아토믹 디자인도 관점은 UI에 집중하지만 원리는 비슷한거 같다.
이 포스팅을 계기로 조금이나마 더 기억할지도…?

 Reference