본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

객체 지향 CSS(OOCSS)

작성 2026. 10. 14.추가
목차18개 항목
출처와 참고

Louis Lazaris의 「객체 지향 CSS 소개」(An Introduction To Object Oriented CSS, 2019.10, 웹액츄얼리 번역)를 바탕으로 재구성하고, 안형우의 「유지보수하기 쉬운 CSS 전략」(WSConf. Seoul 2016 발표)의 실무 전략으로 보강한 노트다.

OOCSS란

객체 지향 CSS(Object Oriented CSS, OOCSS)는 Nicole Sullivan이 2008년 무렵 제안한 CSS 작성 방법론이다. 페이지에서 반복되는 시각 패턴을 찾아 클래스 하나로 표현되는 재사용 단위("객체")로 만든다. 목표는 두 가지다.

  • 작고 빠른 스타일시트: 중복이 줄어 파일이 작아진다. 이미 있는 객체를 조합하면 새 CSS를 한 줄도 쓰지 않고 새 화면을 만들 수 있다.
  • 유지보수하기 쉬운 스타일시트: 덮어쓰기 위해 선택자를 계속 길게 만드는 특정도 전쟁(specificity war) 대신, 낮고 고른 특정도의 모듈이 캐스케이드를 따라 자연스럽게 겹친다.

두 가지 원칙

1. 구조와 겉모양을 분리한다

구분무엇예
구조(structure)보이지 않는 골격너비, 가운데 정렬, 위치 맥락, 안쪽 여백, float 해제
겉모양(skin)브랜드를 드러내는 시각 요소색, 그러데이션, 테두리, 그림자

ID 선택자마다 같은 테두리·그림자를 반복해 쓰는 대신, 공통 겉모양을 .skin 같은 클래스로 뽑아 여러 요소에 붙인다. 골격도 마찬가지다. 헤더·본문·푸터 안쪽 영역이 모두 "고정 너비 + 가운데 정렬 + 좌우 패딩"이라면 그것을 .globalwidth 같은 구조 클래스 하나로 만든다.

<div class="header-inside globalwidth">…</div>
<div class="main globalwidth">…</div>
<div class="footer-inside globalwidth">…</div>

2. 컨테이너와 콘텐츠를 분리한다

/* 나쁨: 사이드바 안의 h3에서만 쓸 수 있다 */
#sidebar h3 { font-size: 1.5rem; color: #333; text-shadow: …; }
 
/* 좋음: 어디에 있든 같은 모양 */
.heading-section { font-size: 1.5rem; color: #333; }
.heading-section--shadow { text-shadow: …; }

자손 선택자로 쓴 스타일은 그 컨테이너에 묶인다. 같은 제목을 푸터에서 쓰려면 규칙을 복사하거나 선택자를 더 늘려야 한다. 클래스로 만든 객체는 문서 구조와 상관없이 어디서나 쓴다.

계층 선택자가 문제를 낳는 과정

"계층 선택자로 문서 구조를 따라 쓰고, 클래스명은 콘텐츠의 성격(모양 말고)으로 지어라"는 오래된 권장 사항은 규모가 커지면 문제가 된다. .issue h2, .news h2, .latest h2처럼 영역마다 같은 스타일을 따로 겨누다 보면, 같은 블록이 다른 곳에 나타날 때마다 #sidebar .latest h2, #article-footer .issue ul li a처럼 더 긴 선택자로 저격하게 된다. 결국 중복과 특정도가 함께 불어난다.

클래스명은 개발자를 위한 것이다

시맨틱에는 두 층이 있다.

층읽는 쪽표현 수단
내용 시맨틱검색 엔진, 보조 기술, 사람HTML 요소, Microdata·구조화 데이터
개발 시맨틱개발자CSS 클래스명

그래서 클래스명이 콘텐츠를 설명할 필요는 없다. <ul class="news">보다 <ul class="simple-list">가 다른 곳에서도 쓸 수 있는 이름이다. 다만 겉모양을 그대로 적은 이름도 피한다.

재사용이 막히는 이름재사용되는 이름
.blue-title (색이 바뀌면 거짓말이 된다).text-primary (역할)
.bottom-items (위치에 묶인다).cards (패턴)

캐스케이드를 흐르게 하고, 필요한 곳만 겨눈다

캐스케이드 자체는 잘못이 없다. 기본값은 넓게 흘려보내고, 객체는 클래스 하나로만 겨눈다.

/* 기본값: 태그 선택자로 한 번만 */
html { font-size: 100%; font-family: "Pretendard", system-ui, sans-serif; }
input, textarea, button { font: inherit; color: inherit; } /* 폼 요소도 기본값을 물려받게 */

과거에는 normalize.css를 깔고 시작했다. 지금 브라우저는 차이가 많이 줄어, 위처럼 꼭 필요한 부분만 다루는 짧은 리셋을 쓰는 경우가 많다(리셋과 노멀라이즈의 차이는 CSS 기초).

타이포그래피 전략

글꼴·크기·색은 객체마다 따로 정하면 금세 제각각이 된다.

  1. 본문의 기본 글꼴·크기·색은 태그 선택자(html, body)에 지정한다.
  2. 제목의 글꼴·크기·색은 h1~h6 선택자에 지정한다.
  3. 그 밖에 쓸 크기·색은 유틸리티 클래스로 정해 둔다(.u-text-small, .u-color-secondary, .u-text-right).
  4. 객체 안에서 글꼴을 직접 지정하는 일은 최소로 줄인다.

제목의 크기와 문서 구조를 떼어 두면 편하다. <h1 class="u-h3">처럼 의미상 h1이어도 크기는 h3로 그릴 수 있다. 지금은 이 크기·색 단계를 CSS 변수(디자인 토큰)로 정의하는 경우가 많다(→ CSS 커스텀 속성으로 색상 체계 짜기).

기본 객체를 갖춘다

그리드, 제목, 링크, 목록, 버튼, 미디어, 테이블, 구분선처럼 어느 사이트에나 있는 객체를 먼저 만들어 두면, 새 화면은 대부분 조합으로 끝난다. Bootstrap의 <button class="btn btn-lg btn-primary">가 구조(btn)·크기(btn-lg)·겉모양(btn-primary)을 나눈 대표 예다.

객체의 범위

객체는 제한된 역할을 맡는 관련 코드를, 재사용을 염두에 두고, 추상화해 묶은 것이다. 높이를 고정하지 않고 아이템 높이를 맞추는 .flex-grid처럼 역할 하나에 집중한다. 그 안의 여백·이미지·글자는 다른 객체와 유틸리티에 맡긴다.

<div class="flex-grid">
  <a class="flex-grid-item u-3/12">
    <img src="…" alt="…">
    <header class="title title-one-line">
      <p class="u-h3 u-color-sub">…</p>
      <h1 class="u-h3">…</h1>
    </header>
  </a>
</div>

헤더, 푸터, 로고처럼 한 번만 나오는 영역까지 억지로 객체로 만들 필요는 없다. 목적은 생산성이다.

예외로 둘 곳

상황이유처리
에디터가 만든 본문(CMS, 위지윅)생성된 HTML에 클래스를 붙일 수 없다.content h2처럼 컨테이너 범위의 자손 선택자를 허용한다
마크업을 바꿀 수 없는 서드파티 라이브러리선택자에 개입할 수 없다라이브러리 클래스를 감싸는 스타일을 따로 둔다

대표 패턴: 미디어 객체

미디어 객체(media object)는 왼쪽에 이미지(미디어), 오른쪽에 텍스트가 오는 블록이다. 댓글, 프로필, 알림, 상품 목록 등 웹 곳곳에 같은 뼈대가 반복된다. 이미지 크기와 텍스트 스타일은 바뀌어도 "미디어 옆에 본문이 흐른다"는 구조는 같으므로, 그 구조만 객체로 만들면 많은 코드를 줄인다.

.media { display: flex; align-items: flex-start; gap: 1rem; }
.media__body { flex: 1; min-width: 0; }

실천 규칙

규칙이유
자손 선택자를 피한다 (.sidebar h3 ✗)스타일이 위치에 묶인다
ID로 스타일을 주지 않는다특정도가 너무 높아 덮어쓰기 어렵다. ID는 JS 훅과 앵커에만 쓴다
요소에 클래스를 붙인 선택자를 피한다 (div.header ✗)다른 요소에서 재사용할 수 없고 특정도만 오른다
!important는 정말 드물게만특정도 전쟁의 마지막 무기라 이후 모든 덮어쓰기가 힘들어진다
린터로 검사한다과거에는 CSS Lint, 지금은 stylelint로 규칙을 자동 검사한다

특정도가 어떻게 계산되는지는 CSS 선택자와 우선순위에 있다. 지금은 @layer(캐스케이드 레이어)와 :where()(특정도 0)로 특정도를 의도적으로 낮게 관리할 수도 있다.

흔한 오용

오용모습문제
저격형을 뱀형으로#article-footer .issue ul a.more → .article-footer_issue_ul_a_more문서 구조를 이름에 옮겨 적었을 뿐이다. 재사용성은 그대로 없다
유틸리티 남발.mt10 .mt20 … .mr60 .f10 … .f20값마다 클래스를 만들면 레고를 쏟아 놓은 상자가 된다. 단계를 정하지 않은 매직 넘버의 나열이다

유틸리티는 정해 둔 단계(scale)에서만 만든다. Tailwind가 mt-4처럼 4px 단위 간격 체계를 강제하는 것도 같은 이유다.

시맨틱 클래스와의 긴장

OOCSS는 HTML에 globalwidth, skin 같은 겉모양을 설명하는 클래스를 늘린다. "마크업과 표현을 분리하라"는 원칙과 충돌한다는 비판을 받는다.

관점근거
겉모양 클래스를 HTML에 드러낸다 (OOCSS, 이후 유틸리티 CSS)CSS가 작아지고, 공통 스타일을 한곳에서 추적·수정한다. 재사용이 쉽다
HTML에는 의미만, 스타일 조합은 CSS 쪽에서 (시맨틱 클래스 + Sass @extend)HTML이 콘텐츠를 설명한다. 프레임워크를 바꿔도 HTML을 고치지 않는다

후자의 구체적인 방법은 Sass: 파일 구조, 믹스인과 @extend의 "@extend로 프레임워크 클래스를 숨기기"에 있다.

이어진 흐름

OOCSS의 "재사용 가능한 클래스 객체" 발상은 SMACSS·BEM 같은 방법론, 겉모양 분리를 속성 하나 단위까지 밀어붙인 유틸리티 CSS, 이름 충돌을 도구가 막는 CSS Modules·CSS-in-JS로 이어졌다. 그 흐름은 CSS의 역사에, 컴포넌트 단위로 스타일을 묶는 방식은 React 스타일링에 있다.

한계

  • 작은 사이트나 앱에는 과하다. 반복되는 패턴이 적으면 객체를 뽑아도 얻는 것이 적다.
  • HTML의 클래스 목록이 길어지고, 어떤 클래스 조합이 맞는지 알아야 한다. 문서화된 패턴 라이브러리가 없으면 조합이 제각각이 된다.
  • HTML 용량은 늘어난다. 대개 CSS 감소분이 더 크지만 항상 그렇지는 않다.

정리

  • 반복되는 시각 패턴을 찾아 클래스 객체로 만든다.
  • 구조와 겉모양을 나누고, 스타일이 컨테이너 위치에 묶이지 않게 한다.
  • ID 선택자, 자손 선택자, 요소+클래스 선택자, !important를 피해 특정도를 낮고 고르게 유지한다.
  • 기본값은 태그 선택자로 흘려보내고, 타이포그래피는 태그·제목·유틸리티 세 층으로 정한다.
  • 클래스명은 개발자를 위한 이름이다. 위치·색이 아니라 역할과 패턴으로 짓는다.
  • 규모가 클수록 이득이 크다. 작은 프로젝트에도 관점만큼은 유용하다. 원칙을 정하고, 예외는 팀이 토론해 정한다.

관련 노트: CSS 선택자와 우선순위, Sass: 파일 구조, 믹스인과 @extend, Atomic Design과 컴포넌트 폴더 구조, React 스타일링: CSS Modules와 styled-components, CSS의 역사로 본 CSS가 어려워진 이유, CSS 설계 방법론: BEM과 ITCSS

원본 기록2

댓글

GitHub 계정으로 댓글을 남길 수 있어요.