본문 바로가기

CSS 아키텍처와 방법론

OOCSS·BEM·SMACSS의 분리와 이름 규칙을 비교하고 블로그 스타일을 예측 가능한 모듈 구조로 재편합니다.

지금까지 CSS의 기본 스타일부터 Flexbox와 Grid, 트랜지션과 애니메이션, 변수와 calc()까지 익혔습니다. 속성을 잘 쓰는 것도 중요하지만, 프로젝트가 커지고 함께 일하는 사람이 늘어나면 CSS를 어떻게 나누고 관리할지가 더 중요해집니다. 아무 규칙 없이 CSS를 쓰다 보면 다음과 같은 문제가 생깁니다.

  • 비슷한 스타일을 여러 번 반복해서 씁니다.
  • 한 곳의 스타일을 고쳤는데 예상하지 못한 다른 곳이 함께 바뀝니다.
  • 여러 사람이 같은 클래스 이름을 써서 서로의 스타일을 덮어씁니다.
  • 어떤 스타일이 어디에 적용되는지 알기 어려워집니다.

이런 문제를 줄이고 고치기 쉬운 CSS를 쓰기 위해 만든 것이 CSS 아키텍처(architecture)와 방법론(methodology)입니다. 이 글에서는 재사용할 단위를 나누는 OOCSS, 클래스 이름에 관계를 담는 BEM, 규칙을 목적에 따라 분류하는 SMACSS를 살펴보고, 이 셋을 함께 적용해 블로그 페이지의 CSS를 정리해 봅니다.

CSS 방법론: 분리·명명·분류를 함께 사용한다

CSS ARCHITECTURE · RESPONSIBILITIES

CSS 방법론: 분리·명명·분류를 함께 사용한다

OOCSS는 재사용 책임을 나누고, BEM은 클래스 관계에 이름을 붙이며, SMACSS는 규칙의 목적을 분류한다. 세 방법은 서로 다른 설계 질문에 답한다.

세 방법론은 하나의 컴포넌트에 서로 다른 책임을 보탠다OOCSS 구조·스킨, BEM 관계 이름, SMACSS 목적·파일 분류의 세 노드가 같은 게시물 카드로 연결된다. 화살표는 기여 관계이며 OOCSS, BEM, SMACSS의 실행 순서가 아니다. 실제 CSS 우선순위와 상속을 별도로 확인하며 아래의 다섯 범주 표와 함께 읽는다.OOCSS구조 · 스킨 분리BEMpost-card · __contentSMACSSModule · State 분류같은 게시물 카드분리·이름·파일 책임을 함께 정함실제 cascade와 상속은 별도 확인
세 방법론은 하나의 컴포넌트에 서로 다른 책임을 보탠다OOCSS 구조·스킨, BEM 관계 이름, SMACSS 목적·파일 분류의 세 노드가 같은 게시물 카드로 연결된다. 화살표는 기여 관계이며 OOCSS, BEM, SMACSS의 실행 순서가 아니다. 실제 CSS 우선순위와 상속을 별도로 확인하며 아래의 다섯 범주 표와 함께 읽는다.OOCSS구조 · 스킨 분리BEMpost-card · __contentSMACSSModule · State 분류같은 게시물 카드분리·이름·파일 책임우선순위 자동격리 없음

OOCSS · 재사용 책임

구조와 스킨을 나누고 콘텐츠가 특정 컨테이너 선택자에 강하게 묶이지 않도록 한다. 예: button의 배치·간격과 색상 테마를 조합한다.

BEM · 관계를 이름으로

card는 Block, card__title은 Element, card--featured는 Modifier다. 이름으로 컴포넌트·부분·대안을 구분하며 modifier는 기본 클래스와 함께 쓴다.

SMACSS · 규칙의 범주

Base·Layout·Module·State·Theme로 규칙의 목적을 구분한다. BEM으로 이름 붙인 Module에도 상태와 테마 규칙을 조합할 수 있다.

SMACSS의 다섯 범주: 우선순위가 아니라 변경 목적
범주담당하는 변경예시
Base요소 기본값·resetbody · a · img
Layout페이지 주요 영역l-header · l-main-content
Module재사용 컴포넌트post-card · button
State현재 활성·오류 상태is-active · is-new
Theme시각 테마·스킨theme-dark · 색상 토큰

Base

요소 기본값·reset · body · a · img

Layout

페이지 주요 영역 · l-header · l-main-content

Module

재사용 컴포넌트 · post-card · button

State

현재 활성·오류 상태 · is-active · is-new

Theme

시각 테마·스킨 · theme-dark · 색상 토큰

방법론 이름만으로 CSS 우선순위가 정해지지는 않는다

BEM 클래스와 SMACSS 범주가 자동으로 cascade layer를 만들지는 않는다. 실제 선언의 cascade·상속·specificity를 확인하면서 팀의 이름 규칙과 파일 소유 범위를 일관되게 적용한다.

위 그림은 세 방법론이 하나의 게시물 카드에 각각 다른 것을 보탠다는 것을 보여 줍니다. OOCSS는 구조와 겉모양(skin)을 나눠 재사용할 단위를 정하고, BEM은 post-card와 post-card__content처럼 이름으로 부품의 관계를 나타내며, SMACSS는 그 규칙이 Module인지 State인지 분류해 어느 파일에 둘지 정합니다. 화살표는 함께 기여한다는 뜻일 뿐, 세 방법을 이 순서로 실행한다는 뜻이 아닙니다.

그림 아래의 표는 SMACSS의 다섯 범주를 정리합니다. Base는 body, a, img 같은 요소의 기본값을, Layout은 헤더나 본문 같은 페이지의 큰 영역을, Module은 카드와 버튼 같은 재사용 부품을, State는 is-active 같은 현재 상태를, Theme은 어두운 테마 같은 시각 테마를 맡습니다. 그림 맨 아래의 설명처럼 이 범주와 BEM 이름이 CSS의 우선순위를 자동으로 정해 주지는 않으므로, 실제 cascade와 명시도는 따로 확인합니다.


CSS 아키텍처가 필요한 이유

CSS 아키텍처는 CSS 코드를 나누고 정리하는 방식으로, 건물을 짓기 전에 그리는 설계도와 같습니다. 이 글의 세 방법은 하나를 골라 나머지를 버리는 경쟁 관계가 아닙니다. OOCSS는 무엇을 나눠 재사용할지, BEM은 이 클래스가 어떤 부품의 어느 부분인지, SMACSS는 이 규칙을 어떤 목적의 파일에 둘지를 묻습니다. 그래서 버튼 하나를 구조와 겉모양으로 나누고, BEM으로 이름을 붙인 뒤, Module과 State 파일에 나눠 두는 식으로 함께 쓸 수 있습니다.

좋은 CSS 아키텍처는 새 기능이나 페이지를 더하기 쉽게 하고, 고칠 곳과 그 영향을 빨리 찾게 하며, 이미 쓴 스타일을 다른 곳에서 다시 쓰게 합니다. 또 모두가 같은 규칙으로 작업하므로 충돌이 줄고, 새로 합류한 사람도 코드의 흐름을 빨리 파악할 수 있습니다.


OOCSS

OOCSS(Object-Oriented CSS)는 객체 지향 프로그래밍처럼 CSS를 재사용할 수 있는 객체 단위로 구성하자는 원칙입니다. 두 가지 원칙이 있습니다.

첫째는 구조(structure)와 겉모양(skin)을 나누는 것입니다. 구조는 레이아웃, 너비, 높이, 여백처럼 요소의 크기와 위치를 정하는 속성이고, 겉모양은 색, 글꼴, 테두리, 그림자처럼 보이는 모습을 정하는 속성입니다. 둘을 서로 다른 클래스로 만들어 조합하면 같은 구조를 여러 색으로, 같은 색을 여러 크기로 다시 쓸 수 있습니다.

<button class="btn btn--large btn--primary">클릭</button>
<button class="btn btn--small btn--secondary">취소</button>
/* 구조 (재사용 가능한 기본 버튼 스타일) */
.btn {
    display: inline-block;
    padding: 10px 20px;
    border: none;
    cursor: pointer;
    text-align: center;
    border-radius: 5px;
}
.btn--large {
    padding: 15px 30px;
    font-size: 1.2em;
}
.btn--small {
    padding: 8px 15px;
    font-size: 0.9em;
}

/* 스킨 (색상 테마) */
.btn--primary {
    background-color: #3498db;
    color: white;
}
.btn--secondary {
    background-color: #2ecc71;
    color: white;
}

btn이 모든 버튼의 공통 구조를, btn--large와 btn--small이 크기를, btn--primary와 btn--secondary가 색을 맡습니다. 큰 초록 버튼이 필요하면 새 CSS를 쓰지 않고 btn btn--large btn--secondary처럼 클래스만 조합합니다.

둘째는 내용과 담는 곳을 나누는 것입니다. 어떤 요소의 스타일이 특정 부모 안에서만 적용되도록 묶지 않고, 어디에 놓아도 같은 모습이 되는 독립된 클래스를 씁니다.

/* 나쁜 예시 (컨테이너 종속적) */
.sidebar .menu-list li a {
    color: blue;
}

/* 좋은 예시 (재사용 가능한 컴포넌트) */
.menu-item {
    display: block;
    padding: 10px;
}
.menu-item-link {
    color: blue;
}

첫 번째 규칙은 .sidebar 안의 목록에서만 적용되므로, 같은 메뉴를 헤더에 놓으면 스타일이 사라집니다. 두 번째 규칙의 menu-item은 사이드바든 헤더든 푸터든 어디서나 같은 모습으로 다시 쓸 수 있고, 선택자도 짧아 명시도가 낮게 유지됩니다.


BEM

BEM(Block, Element, Modifier)은 가장 널리 쓰이는 CSS 클래스 이름 규칙 가운데 하나입니다. 클래스 이름만 보고도 그 스타일이 어디에 적용되고 무엇을 뜻하는지 알 수 있게, 화면을 블록, 요소, 수정자의 세 가지로 나눕니다.

  • 블록(block): 혼자서도 의미가 있는 재사용 부품입니다. 헤더, 메뉴, 버튼, 카드가 블록이며, 클래스 이름은 card처럼 씁니다.
  • 요소(element): 블록의 일부로, 블록 밖에서는 따로 쓰이지 않는 부분입니다. 카드의 제목이나 이미지가 요소이며, 블록 이름 뒤에 밑줄 두 개(__)를 붙여 card__title처럼 씁니다.
  • 수정자(modifier): 블록이나 요소의 모양, 상태, 동작이 다른 변형입니다. 블록이나 요소 이름 뒤에 하이픈 두 개(--)를 붙여 card--featured, card__button--primary처럼 씁니다. 수정자는 기본 클래스를 대신하지 않고 class="card card--featured"처럼 함께 씁니다.
<div class="card">
    <img class="card__image" src="image.jpg" alt="제품 이미지">
    <h3 class="card__title">제품명</h3>
    <p class="card__description">제품 설명입니다.</p>
    <button class="card__button card__button--primary">구매</button>
</div>

<div class="card card--featured">
    <img class="card__image" src="image2.jpg" alt="특별 제품 이미지">
    <h3 class="card__title">특별 제품</h3>
    <p class="card__description">특별 할인 중!</p>
    <button class="card__button card__button--secondary">자세히 보기</button>
</div>
/* Block */
.card {
    border: 1px solid #ccc;
    padding: 15px;
    border-radius: 8px;
    background-color: white;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    width: 300px;
}

/* Element */
.card__image {
    max-width: 100%;
    height: auto;
    display: block;
    margin-bottom: 10px;
}
.card__title {
    font-size: 1.5em;
    color: #333;
    margin-bottom: 5px;
}
.card__description {
    font-size: 0.9em;
    color: #666;
    margin-bottom: 15px;
}
.card__button {
    padding: 8px 15px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 1em;
}

/* Modifier (Block level) */
.card--featured {
    border-color: #f1c40f;
    box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}

/* Modifier (Element level) */
.card__button--primary {
    background-color: #3498db;
    color: white;
}
.card__button--secondary {
    background-color: #2ecc71;
    color: white;
}

BEM을 쓰면 card__title이라는 이름만으로 카드의 제목이라는 것을 알 수 있어 코드를 읽기 쉽고, 블록마다 이름이 다르므로 우연히 같은 이름을 써서 생기는 충돌이 줄어듭니다. 선택자가 모두 클래스 하나라 명시도가 같고 낮게 유지되어, 어느 규칙이 이길지도 예측하기 쉽습니다. 다만 일반 CSS 클래스는 여전히 문서 전체에서 맞는 요소를 찾으므로, BEM 이름이 브라우저 차원에서 스타일을 격리해 주는 것은 아닙니다.


SMACSS

SMACSS(Scalable and Modular Architecture for CSS)는 CSS 규칙을 목적에 따라 다섯 범주로 나눠 정리하는 지침입니다.

  • Base: HTML 요소 자체의 기본 스타일입니다. 리셋이나 normalize CSS, 글꼴과 기본 색, 링크의 상태처럼 요소의 기본 모습을 정하며, html, body, h1, a, p, ul 같은 요소 선택자를 주로 씁니다. 예를 들어 a { color: blue; text-decoration: none; }입니다.
  • Layout: 헤더, 푸터, 사이드바, 본문처럼 페이지의 큰 영역을 정합니다. 예전에는 ID 선택자를 많이 썼지만, 요즘은 l-header처럼 l-이나 layout-을 붙인 클래스를 씁니다.
  • Module: 버튼, 카드, 폼, 메뉴 같은 재사용 부품입니다. BEM의 블록과 비슷하며 접두사를 붙이지 않고, CSS의 대부분이 여기에 속합니다.
  • State: 숨김, 활성, 비활성, 오류처럼 모듈이나 레이아웃의 현재 상태입니다. is-hidden, is-active, is-error처럼 is-를 붙인 클래스를 쓰며, 자바스크립트로 붙이고 떼는 경우가 많습니다.
  • Theme: 사이트의 색이나 테마를 정합니다. OOCSS의 겉모양과 비슷하며, theme-dark처럼 theme-을 붙인 클래스를 씁니다.

SMACSS로 파일을 나누면 다음과 같은 구조가 됩니다.

_reset.css
_typography.css
_base.css
_grid.css
_header-footer.css
_button.css
_card.css
_form.css
_navigation.css
_states.css
_dark-theme.css
style.css (모든 파일을 import하는 메인 파일)

규칙의 목적이 분명히 나뉘므로, 새 스타일을 더할 때 어느 범주에 둘지 바로 정할 수 있고, 문제가 생겼을 때도 찾아볼 파일이 좁혀집니다.


그 밖의 방법론

  • ITCSS(Inverted Triangle CSS): SMACSS처럼 CSS를 계층으로 나누되, 넓게 적용되는 일반 규칙에서 좁은 범위의 구체적인 규칙으로 더 엄격한 순서를 지킵니다. cascade의 특성을 그 순서에 맞춰 활용합니다.
  • Atomic CSS(Functional CSS): .text-center, .mt-10, .flex처럼 속성과 값 하나를 나타내는 작은 클래스를 HTML에 직접 여러 개 붙입니다. Tailwind CSS가 대표적이며, 빠르게 만들고 디자인을 일관되게 지키기 쉽지만 HTML의 클래스가 길어집니다.
  • CSS-in-JS: 자바스크립트 코드 안에서 CSS를 씁니다. React 같은 프레임워크의 컴포넌트와 함께 쓰며, 컴포넌트마다 스타일을 감싸고 상태에 따라 스타일을 바꾸기 쉽습니다. 이 교재에서는 개념만 소개합니다.

실습: BEM과 SMACSS로 정리한 블로그

세 방법론을 함께 적용해 블로그의 게시물 카드 목록을 만들어 봅니다. 다음 구조로 파일을 준비합니다. 이 실습의 css/style.css는 외부 스타일 시트와 선택자 실습에서 만든 같은 이름의 파일을 덮어쓰므로, 그 실습의 결과를 남겨 두려면 다른 위치에 새 web-dev-practice 폴더를 만들어 시작합니다. 파일 이름 앞의 밑줄은 다른 파일에 포함되는 부분 파일이라는 표시일 뿐, 브라우저가 따로 처리하는 문법은 아닙니다.

css_architecture.html
_base.css
_layout.css
_module.css
_state.css
style.css (이 파일에서 모든 CSS를 import)

css_architecture.html에는 다음 코드를 씁니다. 현재 페이지의 메뉴 링크에는 상태 클래스 is-active와 함께, 스크린 리더에 현재 페이지임을 알리는 aria-current="page"를 붙였습니다.

css_architecture.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS 아키텍처 실습</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header class="l-header">
        <h1 class="l-header__title">블로그</h1>
        <nav class="main-nav">
            <ul class="main-nav__list">
                <li class="main-nav__item"><a href="#" class="main-nav__link is-active" aria-current="page">홈</a></li>
                <li class="main-nav__item"><a href="#" class="main-nav__link">카테고리</a></li>
                <li class="main-nav__item"><a href="#" class="main-nav__link">문의</a></li>
            </ul>
        </nav>
    </header>

    <main class="l-main-content">
        <section class="l-section">
            <h2 class="l-section__title">최신 게시물</h2>
            <div class="post-grid">
                <article class="post-card">
                    <div class="post-card__content">
                        <h3 class="post-card__title">CSS 아키텍처의 중요성</h3>
                        <p class="post-card__excerpt">대규모 프로젝트에서 CSS를 효과적으로 관리하는 방법론을 알아봅니다.</p>
                        <a href="#" class="post-card__read-more button button--primary">더 읽기</a>
                    </div>
                </article>

                <article class="post-card post-card--featured is-new">
                    <div class="post-card__content">
                        <h3 class="post-card__title">Flexbox와 Grid 비교 분석</h3>
                        <p class="post-card__excerpt">언제 Flexbox를 쓰고, 언제 Grid를 써야 할까?</p>
                        <a href="#" class="post-card__read-more button button--secondary">더 읽기</a>
                    </div>
                </article>

                <article class="post-card">
                    <div class="post-card__content">
                        <h3 class="post-card__title">자바스크립트 비동기 프로그래밍</h3>
                        <p class="post-card__excerpt">콜백, Promise, Async/Await 완벽 이해하기.</p>
                        <a href="#" class="post-card__read-more button button--primary">더 읽기</a>
                    </div>
                </article>
            </div>
        </section>
    </main>

    <footer class="l-footer">
        <p class="l-footer__text">&copy; 2026 CSS 아키텍처 실습. 모든 권리 보유.</p>
    </footer>
</body>
</html>

css/style.css는 네 파일을 차례로 불러오고 공통 변수를 선언합니다. @import는 다른 규칙보다 앞에 있어야 하며, 경로는 style.css가 있는 css 폴더를 기준으로 해석됩니다.

css/style.css
/* style.css */

@import url('_base.css');
@import url('_layout.css');
@import url('_module.css');
@import url('_state.css');

/* CSS 변수는 :root에 정의하는 것이 일반적이며, 이곳이나 _base.css에 포함 가능 */
:root {
    --primary-color: #3498db;
    --secondary-color: #2ecc71;
    --accent-color: #e67e22;
    --text-color: #333;
    --bg-color: #f0f2f5;
    --card-bg: white;
    --border-color: #ddd;
    --shadow-color: rgba(0,0,0,0.1);
    --box-shadow-light: 0 2px 10px rgba(0,0,0,0.1);
    --gap-size: 20px;
}

_base.css가 먼저 불러와지고 :root의 변수 선언은 나중에 나오지만, _base.css의 var(--bg-color)는 정상적으로 값을 찾습니다. CSS는 위에서부터 한 줄씩 실행되는 코드가 아니라, 모든 규칙을 모은 뒤 cascade로 각 요소의 값을 정하기 때문입니다.

css/_base.css에는 요소의 기본 스타일을 씁니다.

css/_base.css
/* _base.css */

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: var(--bg-color);
    color: var(--text-color);
    line-height: 1.6;
    padding-bottom: 50px;
}

a {
    color: var(--primary-color);
    text-decoration: none;
    transition: color 0.2s ease;
}
a:hover {
    color: #2980b9;
}

ul {
    list-style: none;
}

img {
    max-width: 100%;
    height: auto;
    display: block;
}

css/_layout.css에는 페이지의 큰 영역을 씁니다. 게시물 목록의 열 최솟값에는 min(300px, 100%)를 써서, 휴대폰처럼 좁은 화면에서는 300px보다 작아질 수 있게 했습니다.

css/_layout.css
/* _layout.css */

.l-header {
    background-color: var(--primary-color);
    color: white;
    text-align: center;
    padding: calc(var(--gap-size) * 1.5) var(--gap-size);
    box-shadow: 0 3px 6px var(--shadow-color);
    margin-bottom: var(--gap-size);
}
.l-header__title {
    font-size: 2.8em;
    margin-bottom: calc(var(--gap-size) / 2);
}

.l-main-content {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 var(--gap-size);
}

.l-section {
    background-color: var(--card-bg);
    padding: calc(var(--gap-size) * 1.5);
    margin-bottom: var(--gap-size);
    border-radius: 8px;
    box-shadow: 0 2px 10px var(--shadow-color);
}
.l-section__title {
    color: var(--primary-color);
    font-size: 2em;
    margin-bottom: calc(var(--gap-size) * 1.5);
    text-align: center;
}

.post-grid {
    display: grid;
    /* 좁은 화면에서는 300px 대신 부모 폭까지 줄어듦 */
    grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
    gap: var(--gap-size);
}

.l-footer {
    background-color: var(--primary-color);
    color: white;
    text-align: center;
    padding: var(--gap-size);
    margin-top: 50px;
    font-size: 0.9em;
}

이 파일의 클래스는 모두 l- 접두사로 시작하므로, HTML만 보고도 페이지의 큰 틀을 정하는 규칙이라는 것을 알 수 있습니다. 간격과 색은 값을 직접 적지 않고 --gap-size, --primary-color 같은 변수로 가져오므로, style.css의 :root에서 변수 값을 바꾸면 레이아웃 전체의 간격과 색이 함께 바뀝니다.

css/_module.css에는 BEM으로 이름 붙인 재사용 부품을 씁니다. 블록인 .button과 .post-card가 부품의 기본 모양을 정하고, --가 붙은 수정자(modifier) 클래스가 색이나 강조 같은 차이를, __가 붙은 요소(element) 클래스가 부품 안의 각 부분을 정합니다.

css/_module.css
/* _module.css */

/* Button Module */
.button {
    display: inline-block;
    padding: 10px 15px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 0.9em;
    font-weight: bold;
    transition: background-color 0.2s ease, transform 0.2s ease;
}
.button--primary {
    background-color: var(--primary-color);
    color: white;
}
.button--secondary {
    background-color: var(--secondary-color);
    color: white;
}
.button:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.button--primary:hover { background-color: #2980b9; }
.button--secondary:hover { background-color: #27ae60; }

/* Main Navigation Module */
.main-nav {
    background-color: #34495e;
    padding: 10px 0;
    text-align: center;
}
.main-nav__list {
    display: flex;
    flex-wrap: wrap; /* 좁은 화면에서는 메뉴가 줄을 바꿈 */
    justify-content: center;
    gap: 20px;
}
.main-nav__link {
    color: white;
    padding: 8px 15px;
    border-radius: 5px;
    transition: background-color 0.2s ease;
}
.main-nav__link:hover {
    background-color: rgba(255, 255, 255, 0.2);
}

/* Post Card Module */
.post-card {
    background-color: var(--card-bg);
    border: 1px solid var(--border-color);
    border-radius: 8px;
    overflow: hidden; /* 둥근 모서리 밖으로 내용이 나가지 않도록 */
    box-shadow: var(--box-shadow-light);
    transition: transform 0.2s ease-out;
    display: flex;
    flex-direction: column;
}
.post-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 5px 15px rgba(0,0,0,0.15);
}
.post-card__content {
    padding: var(--gap-size);
    display: flex;
    flex-direction: column;
    flex-grow: 1; /* 콘텐츠 영역이 남은 공간 차지 */
}
.post-card__title {
    font-size: 1.3em;
    color: var(--primary-color);
    margin-bottom: calc(var(--gap-size) / 2);
}
.post-card__excerpt {
    font-size: 0.95em;
    color: var(--text-color);
    margin-bottom: var(--gap-size);
    flex-grow: 1; /* 발췌 부분이 남은 공간 차지 */
}
.post-card__read-more {
    align-self: flex-start; /* 버튼만 왼쪽 정렬 */
    margin-top: auto; /* 항상 하단에 붙도록 */
}

/* Post Card Modifier */
.post-card--featured {
    border-color: var(--accent-color);
    box-shadow: 0 5px 20px rgba(230, 126, 34, 0.3);
}

css/_state.css에는 상태 클래스를 씁니다. 같은 명시도의 모듈 규칙을 덮어쓸 수 있도록, style.css에서 이 파일을 마지막에 불러왔습니다.

css/_state.css
/* _state.css */

.is-active {
    background-color: rgba(255, 255, 255, 0.3); /* 활성 상태 */
    font-weight: bold;
}

.is-new::after {
    content: "NEW!";
    order: -1; /* Flex 컨테이너인 카드 안에서 맨 앞에 놓음 */
    align-self: flex-start; /* 카드 폭으로 늘어나지 않게 */
    margin: 10px 0 0 10px;
    background-color: #e74c3c;
    color: white;
    font-size: 0.7em;
    padding: 3px 8px;
    border-radius: 3px;
}

Live Server로 css_architecture.html을 열면 헤더와 메뉴, 세 게시물 카드가 정리된 모습으로 보입니다. 개발자 도구에서 요소를 골라 Styles 영역을 보면, 각 규칙 옆에 그 규칙이 들어 있는 파일 이름이 표시되어 _base.css, _layout.css, _module.css, _state.css 중 어디서 왔는지 확인할 수 있습니다.

두 번째 카드는 기본 모듈 post-card에, 고정된 변형인 post-card--featured와 현재 상태인 is-new를 함께 붙였습니다. 그래서 주황 테두리와 그림자가 생기고, 카드의 맨 위 왼쪽에 빨간 NEW! 표시가 붙습니다. 이 표시는 ::after로 만든 가상 요소이며, 카드가 flex-direction: column인 Flex 컨테이너라 가상 요소도 Flex 아이템이 됩니다. order: -1이 없으면 내용 영역 뒤인 카드 맨 아래에 놓이고, align-self가 없으면 카드 폭만큼 늘어납니다.

메뉴의 is-active를 다른 링크로 옮기는 코드는 아직 없으며, 모든 href="#"는 이 실습의 자리 표시자입니다. 너비 320px 화면에서는 본문의 안쪽 여백 40px과 섹션의 안쪽 여백 60px을 빼면 게시물 목록의 폭이 220px인데, min(300px, 100%) 덕분에 카드가 220px로 줄어들어 가로로 넘치지 않습니다.


더 깊이: 범주의 순서를 CSS가 직접 지키게 하는 @layer

이 글의 실습은 상태 규칙이 모듈 규칙을 이기게 하려고 _state.css를 마지막에 불러오고, 두 규칙의 명시도를 같게 맞췄습니다. 이 방식은 명시도와 소스 순서라는 cascade의 마지막 두 단계에 기대므로, 누군가 모듈에 선택자를 하나 더 붙여 명시도를 높이면 상태 규칙이 다시 집니다. CSS의 계층(cascade layer)은 이런 범주의 순서를 명시도보다 앞선 단계에서 정하게 해 주는 기능입니다.

@layer base, layout, module, state, theme;처럼 계층의 이름을 먼저 나열하면, 나중에 나열한 계층일수록 우선순위가 높아집니다. 그다음 각 규칙을 @layer module { ... } 안에 넣거나, @import url("_state.css") layer(state);처럼 파일 전체를 계층에 넣습니다. 계층 사이의 승부는 명시도와 소스 순서보다 먼저 정해지므로, state 계층의 .is-active 하나가 module 계층의 ID 선택자보다도 우선합니다. 계층의 순서는 처음 선언한 순서로 고정되므로, 파일을 불러오는 순서가 바뀌어도 결과가 달라지지 않습니다.

주의할 점이 두 가지 있습니다. 어느 계층에도 넣지 않은 일반 규칙은 모든 계층의 규칙보다 우선하므로, 계층을 도입할 때 기존 규칙 일부만 옮기면 옮기지 않은 쪽이 의도와 달리 이길 수 있습니다. 또 !important가 붙은 선언끼리는 순서가 뒤집혀, 먼저 선언한 계층이 이깁니다. MDN의 @layer 설명에 따르면 이 기능은 2022년 3월부터 주요 브라우저에서 함께 쓸 수 있습니다.


정리

  • CSS 아키텍처와 방법론은 재사용, 충돌, 고칠 곳 찾기의 문제를 줄이며, OOCSS, BEM, SMACSS는 서로 다른 질문에 답하므로 함께 씁니다.
  • OOCSS는 구조와 겉모양을, 내용과 담는 곳을 나눠 클래스를 조합해 다시 씁니다.
  • BEM은 블록__요소--수정자 형태의 이름으로 부품의 관계를 드러내며, 수정자는 기본 클래스와 함께 붙입니다.
  • SMACSS는 규칙을 Base, Layout, Module, State, Theme로 나눠 파일을 정리합니다.
  • 방법론의 이름과 범주가 우선순위를 정해 주지는 않으므로, 상태 파일을 뒤에 두는 식으로 실제 cascade를 함께 설계합니다.

연습 문제

1. 바꿀 곳 정하기

모든 게시물 카드의 안쪽 간격을 24px로 바꾸기, 특집 카드의 테두리 색만 바꾸기, 두 번째 카드의 NEW 표시 없애기를 각각 어느 파일의 어느 선택자나 클래스에서 해야 할까요? 두 번째 카드를 다른 section으로 옮겨도 카드의 기본 모습이 유지되는 이유는 무엇일까요?

해설 보기

공통 안쪽 간격은 _module.css의 .post-card__content의 padding을 24px로 바꿉니다. --gap-size를 바꾸면 헤더와 섹션의 간격까지 함께 바뀌므로, 카드만 바꾸려면 이 규칙을 고칩니다. 특집 카드의 테두리는 같은 파일의 .post-card--featured에서 바꾸고, NEW 표시는 HTML에서 그 카드의 is-new 클래스를 지웁니다. 공통 상태 규칙인 .is-new::after는 다른 새 글을 위해 남겨 둡니다.

카드의 기본 선택자가 .l-section .post-card처럼 특정 부모에 묶이지 않고 .post-card 하나이므로, 다른 곳으로 옮겨도 똑같이 맞습니다. 다만 옮긴 곳의 폭이나 상속받는 글자색까지 똑같다는 뜻은 아닙니다.

2. 상태 파일의 순서

style.css에서 _state.css를 _module.css보다 먼저 불러오도록 순서를 바꿨습니다. 그리고 .main-nav__link에 background-color: transparent;를 더했습니다. 활성 메뉴 링크의 배경은 어떻게 될까요?

해설 보기

활성 링크에도 배경이 생기지 않습니다. .is-active와 .main-nav__link는 둘 다 클래스 하나라 명시도가 0-1-0으로 같으므로, 소스 순서가 나중인 규칙이 이깁니다. 순서를 바꾸면 _module.css의 transparent가 나중이 되어 .is-active의 배경을 덮어씁니다.

그래서 상태 규칙은 모듈 규칙을 덮어쓸 수 있도록 뒤에 둡니다. SMACSS라는 이름이 이 순서를 자동으로 지켜 주지 않으므로, 파일을 불러오는 순서로 직접 정합니다.

3. BEM으로 이름 붙이기

게시물 카드에 작성일을 보여 주는 부분을 더하고, 여백을 줄인 작은 카드 변형도 만들려고 합니다. BEM 규칙으로 두 클래스의 이름을 지어 보고, 작은 카드의 HTML에서 class 속성을 어떻게 써야 할지 적어 보세요.

해설 보기

작성일은 카드의 일부이므로 요소인 post-card__date, 작은 카드는 카드 전체의 변형이므로 블록 수정자인 post-card--compact입니다.

작은 카드의 HTML은 <article class="post-card post-card--compact">처럼 기본 블록 클래스와 수정자를 함께 씁니다. 수정자만 쓰면 .post-card의 기본 스타일이 빠지므로, 수정자에는 기본과 다른 부분만 적습니다.

다음 글 CSS 전처리기 (Sass) 입문에서는 변수, 중첩, 믹스인으로 CSS를 더 짧고 구조적으로 쓰게 해 주는 Sass를 다룹니다.