BEM, 이름이 곧 설계다
클래스명 충돌에 지쳤다면, BEM이 제안하는 '이름으로 구조를 말하는 법'.
프로젝트가 커지면 CSS에서 가장 먼저 부서지는 건 레이아웃이 아니라 이름이에요.
.title { font-size: 24px; color: #111; }
.card .title { font-size: 16px; }
.modal .title { font-size: 20px; color: #fff; }.title이 세 곳에서 다른 의미로 쓰이고 있어요. 누군가 .title의 font-size를 고치면, 의도하지 않은 곳이 깨져요. 이름이 같으니까. 캐스케이드가 어떻게 동작하는지 정확히 알아도, 이름 자체가 충돌하면 구조적으로 막을 방법이 없어요.
문제는 더 있어요. 시간이 지나면 .card-inner-wrap, .title2, .new-title 같은 클래스가 늘어나고, 어떤 클래스가 어떤 컴포넌트에 속하는지 이름만 봐서는 알 수 없게 돼요. 이름에 구조가 없으면, 코드를 읽는 사람은 매번 HTML을 뒤져야 해요.
BEM은 이 문제를 도구가 아니라 이름 짓는 규칙으로 풀려는 시도예요.
B, E, M - 세 글자가 담는 것
BEM은 Block, Element, Modifier의 약자예요. Yandex에서 만들었고, 핵심 아이디어는 단순해요. 클래스명 하나에 "이건 어떤 컴포넌트의 어떤 부분이고 지금 어떤 상태인가"를 전부 담겠다는 거예요.
Block - 독립적인 컴포넌트
그 자체로 의미를 갖는 단위. 다른 곳에 옮겨놔도 동작해야 한다. 예: card, menu, search-form.
Element - 블록의 부분
블록 밖에서는 의미가 없는 하위 요소. 블록 이름 뒤에 __로 연결한다. 예: card__title, menu__item.
Modifier - 외형이나 상태의 변형
블록이나 엘리먼트의 변형. --로 연결한다. 예: card--highlighted, menu__item--active.
실제 코드로 보면 이래요.
<div class="card card--highlighted">
<h2 class="card__title">제목</h2>
<p class="card__description">설명 텍스트</p>
<button class="card__button card__button--primary">확인</button>
</div>.card { padding: 24px; border: 1px solid #ddd; border-radius: 8px; }
.card--highlighted { border-color: #3b82f6; }
.card__title { font-size: 18px; margin: 0 0 8px; }
.card__description { color: #666; }
.card__button { padding: 8px 16px; border: none; border-radius: 4px; }
.card__button--primary { background: #3b82f6; color: #fff; }클래스명만 읽어도 구조가 보여요. .card__button--primary는 "card 블록 안의 button 엘리먼트인데, primary 변형"이라는 뜻이에요. HTML을 뒤질 필요가 없어요.
실전에서 자주 깨지는 규칙
BEM을 쓰기 시작하면 금세 부딪히는 질문들이 있어요. 공식 문서에서 명확하게 금지하는 것들을 정리해볼게요.
엘리먼트의 엘리먼트는 만들지 않아요. card__header__title 같은 이중 중첩은 BEM이 아니에요. 엘리먼트는 항상 블록에 직접 속해요. DOM 구조가 깊어도 클래스는 평평하게 유지해요.
/* ✗ 금지 */
.card__header__title { }
/* ✓ 올바름 - 블록에 직접 연결 */
.card__title { }Modifier는 단독으로 쓰지 않아요. --primary만 덜렁 클래스에 붙이는 건 안 돼요. 반드시 원래 블록이나 엘리먼트 클래스와 함께 써야 해요.
<!-- ✗ modifier 단독 사용 -->
<button class="card__button--primary">확인</button>
<!-- ✓ 기본 클래스 + modifier -->
<button class="card__button card__button--primary">확인</button>외부 기하학(margin 등)은 블록 자체에 넣지 않아요. 블록은 독립적이어야 하니까, 자기 바깥 여백은 블록이 결정할 일이 아니에요. 블록을 배치하는 부모 쪽에서, 혹은 Mix 기법으로 처리해요.
Mix 기법은 하나의 DOM 노드에 서로 다른 블록의 클래스를 함께 거는 방식이에요. 예를 들어 헤더 안에 검색 폼을 넣을 때, <form class="search-form header__search"> 처럼 쓰면 search-form은 자기 내부 스타일만, header__search는 배치 스타일만 담당합니다. 블록의 독립성을 지키면서 레이아웃에 끼워 넣는 방법이죠.
Modifier에는 두 가지 유형이 있어요. Boolean 타입(menu__item--active)과 Key-Value 타입(menu__item--theme-dark)이에요. 상태 하나를 켜고 끄는 경우는 Boolean, 여러 값 중 하나를 고르는 경우는 Key-Value를 써요.
BEM이 해결하는 것과 못하는 것
첫째, 모듈성. 블록 스타일이 다른 요소에 의존하지 않으니 cascading 문제가 줄어들어요. 블록을 다른 프로젝트로 옮겨도 스타일이 깨지지 않아요. 둘째, 재사용성. 독립적인 블록을 조합하면 CSS 양이 줄고, 라이브러리처럼 관리할 수 있어요. 셋째, 구조성. BEM 클래스명은 곧 설계 문서예요. 이름만 읽어도 컴포넌트 구조를 이해할 수 있어요.
디자인 시스템에서 일관된 네이밍 규약이 중요한 이유도 여기에 있어요. 이름에 구조가 담기면 팀 전체가 같은 언어로 대화할 수 있어요.
하지만 한계도 뚜렷해요.
BEM의 가장 큰 단점은 장황함이에요. .search-form__input--size-large 같은 클래스명이 HTML에 빼곡히 들어차면 마크업 가독성이 떨어져요. CSS 파일도 커져요. 모든 변형을 Modifier로 풀어야 하니까, 비슷한 선언이 반복돼요. 그리고 BEM은 순전히 합의에 의존하는 규칙이에요. 도구가 강제하지 않으면 팀원 한 명의 실수로 규칙이 무너질 수 있어요.
현대 프론트엔드에서 BEM의 위치
CSS Modules는 빌드 도구가 클래스명을 해싱해서 스코프를 자동으로 격리해요. Tailwind는 아예 커스텀 클래스를 만들지 않게 해서 이름 충돌 자체를 없앴고요. 각 스타일링 도구가 어떤 문제를 풀려고 만들어졌는지 보면, BEM이 이름으로 풀던 문제를 도구가 대신 해결해준 셈이에요.
그렇다고 BEM이 낡은 유물인 건 아니에요.
BEM의 핵심은 네이밍 규칙이 아니라 설계 사고예요. "이 컴포넌트는 독립적인가?", "이 요소는 어디에 속하는가?", "이 변형은 외형인가 상태인가?" 같은 질문은 CSS Modules를 쓰든 Tailwind를 쓰든 여전히 유효해요. 클래스명 해싱이 충돌을 막아주더라도, 컴포넌트 경계를 어디에 그을지는 개발자가 결정해야 하니까요.
BEM은 클래스명 컨벤션이 아니라, 컴포넌트를 이름으로 설계하는 방법론이에요. 도구가 바뀌어도 그 사고방식은 남아요.