CSS Architecture를 훑어보고 내용이 마음에 들어서 정독할겸 가볍게 번역을 해보았다. 초반부는 요약해서 정리하다가 내용 이해가 점점 어려워져(집중력이 흐려져) 뒷쪽에는 그냥 다 번역 모드로 마무리하였다.

개인적으로는 대부분 마음에 드는 내용들인데 다른 의견들도 많이 궁금한 내용들이다. 글 자체가 엄청 기니 조금씩 쪼개서 다음주 중에 여기저기 화두를 던져볼까 싶다. 트위터 혹은 페이스북 CDK 그룹 등지에 올려봐야겠다.


이 글은 다음과 같은 목표를 위해 좋은 CSS 구조 설계에 대해 이야기한다.

  • 예측
  • 재사용
  • 유지보수
  • 확장가능

일반적인 나쁜 관행

부모 요소를 기반으로 컴포넌트 수정하기

.widget {
  background: yellow;
  border: 1px solid black;
  color: black;
  width: 50%;
}

#sidebar .widget {
  width: 200px;
}

body.homepage .widget {
  background: white;
}

위와 같은 코드를 가리키며 다음과 같은 문제가 있다고 지적한다.

  1. 예측 가능하지 않다: 이 코드를 사용하게 될 개발자는 #sidebar 어딘가에 .widget을 넣고 .widget 선언의 모양을 기대하지만 다른 곳에서 선언된 #sidebar .widget 때문에 예측과 다른 결과를 보게 될 것이다. 이것은 재사용성, 확장성이 떨어진다.
  2. 유지보수가 어렵다: .widget을 수정하려면 CSS의 여러 곳을 업데이트 해야하기 때문. 이는 소프트웨어의 단일 접근 원칙에 위배된다.

지나치게 복잡한 선택자

/* dropdown 메뉴에서 볼 수 있는 예제 */
#main-nav ul li ul li div { }
/* article의 메인 헤딩을 가리키는 예제 */
#content article h1:first-child { }
/* sidebar 섹션의 첫번째 문단에 여백을 추가하기 위한 목적으로 쓰일법한 예제 */
#sidebar > div > h3 + p { } 이는 다음과 같은 문제를 발생시킨다.
  1. CSS 선택자가 복잡해질수록 HTML과의 결합도가 증가한다: HTML 태그와 그 조합에 의지하는 CSS 코드는 HTML은 깨끗하게 유지시킬 수 있지만 CSS를 후잡하게 만든다. HTML은 현실적으로 수정할 일이 생길 수 밖에 없는데 그럴 때 이런 코드들은 수정 비용을 증가시킨다.
  2. 재사용 불가능하다: 선택자가 마크업의 매우 특별한 부분을 가리키고 있으면 다른 마크업을 가진 다른 컴포넌트가 어떻게 이 스타일을 가져다 쓰겠는가?
  3. HTML 수정이 필요할 때 예측이 어렵다: 3번째 예제에서 div를 section 태그로 수정했다고 생각해보라. 깨지게 된다. 큰 어플리케이션 제작에 절충/타협하려면 복잡한 선택자의 취약성은 HTML을 깨끗하게 유지하는 것 이외에는 가치가 없다.

과도하게 일반화된 클래스명

<div class="widget">
  <h3 class="title">...</h3>
  <div class="contents">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    In condimentum justo et est dapibus sit amet euismod ligula ornare.
    Vivamus elementum accumsan dignissim.
    <button class="action">Click Me!</button>
  </div>
</div>

.widget {}
.widget .title {}
.widget .contents {}
.widget .action {}

이상적으로는 .title.contents.action 자식 요소 클래스는 같은 클래스명을 가진 다른 요소들을 오염시킬 걱정이 없다. 하지만 이런 선택자는 다른 컴포넌트에서 정의된 같은 클래스명의 스타일에 의해 오염될 수 있다.

규모가 큰 프로젝트라면 .title이 어딘가에서 정의되었을 수 있고 그렇다면 .widget .title을 오염시킬 것이다.

이것은 CSS 코드를 예측하기 어렵게 만든다.

너무 많은 규칙 만들기

.widget {
  position: absolute;
  top: 20px;
  left: 20px;
  background-color: red;
  font-size: 1.5em;
  text-transform: uppercase;
}

위와 같이 너무 많은 규칙들을 가진 셀렉터는 거의 같은 모양을 가진 다른 컴포넌트에서 재활용 될 수 없고 CSS에 익숙하지 않은 개발자가 이 코드를 기반으로 거의 같은 모양을 가진 다른 컴포넌트를 개발하게 된다면 필요없는 코드 중복을 발생시키고 말 것이라고…

원인

HTML, CSS를 구조와 표현으로 완벽히 분리해내려고 하는 노력이 사실 위와 같은 문제들을 만들어낸다. 난 현실적으로 HTML이 CSS의 복잡도를 과도하게 증가시키지 않도록 class명을 통해 표현 계층을 함께 담당하는 것이 필요하고 가끔은 현명하다고 본다.

해결방법

내가 발견한 가장 좋은 방법은 CSS가 HTML 구조를 가능한한 최소한으로 포함하도록 하는 것이다. CSS는 시각 요소의 집합을 정의해야 하고 이 요소들은 HTML에 정의된 곳에 표시되도록 해야 한다. 같은 요소이지만 다른 표현이 필요한 경우 그 호출의 책임을 HTML에게 주어야 한다.

예로 CSS로 버튼 컴포넌트를 .button으로 정의한 후 HTML에서 이 버튼처럼 보일 곳에 이 클래스명을 넣고, 조금 더 큰 버튼이 필요해졌다면 CSS로 새로운 클래스를 정의한 후 HTML이 그 새로운 클래스명을 넣도록 하는 것을 들 수 있다.

CSS로 컴포넌트가 어떻게 보여야 할지를 정의한 후 HTML로 페이지에 있는 요소들에 그 표현을 할당하는 것이다. 이러면 CSS는 HTML 구조에 대해 알아야 할 필요가 거의 없어진다.

당신이 HTML에 원하는 것을 정확히 선언하는 것의 가장 큰 이득은 다른 개발자들에게 마크업만으로 요소들이 어떻게 보여질지 정확히 알 수 있게 해준다는 것이다. 의도가 명확해지는 것이다. 이런 방식이 아니면 요소가 어떻게 보여지는지 예측하기 어려워져 팀을 혼란에 빠트릴 수 있게 된다.

이 해결방법을 수많은 요소들에 공통 속성을 적용하기 위해 요소들마다 클래스명을 넣는 노가다를 해야한다고 오해하지 말라. 대안이 있으니. 표현 수준을 잘 추상화한 레일즈나 다른 프레임워크들은 HTML에 명시적으로 선언된 표현을 유지하면서 먼 길을 갈 수 있게 된다.

모범 사례

위와 실수들을 반복하면서 다음과 같은 원칙들을 만들어냈다.

의도적이 되어라

불필요한 요소에 스타일이 적용되는 것을 방지하라면 그럴 기회를 제거하라. #main-nav ul li ul li div는 그렇지 못하지만 .subnav는 확실히 그런 기회들을 제거할 수 있다.

/* 폭탄에 비유할 수 있는 선택자 */
#main-nav ul li ul { }

/* 저격수에 비유할 수 있는 선택자 */
.subnav { }

걱정을 분리하라

잘 구조화된 컴포넌트는 HTML의 구조와 CSS의 결합도를 낮춘다고 했다. 거기에 더해 CSS 컴포넌트 자체를 모듈화하라. 컴포넌트는 그 요소 자체를 잘 스타일링해야 하지만, 주변 레이아웃이나 배치, 부모 요소 등에 대해서는 책임을 지지 않도록 해야한다.

일반적으로 컴포넌트는 그 자체가 어떻게 보일지를 정의하지 레이아웃이나 위치는 정의하지 않는다.backgroundcolorfont 속성이 positionwidthheightmargin 같은 속성과 함께 정의되는 경우에 주의하라.

레이아웃이나 위치는 별도 레이아웃 클래스 혹은 별도 컨테이너 요소에 의해 처리되어야 한다. 효과적인 구조와 표현의 분리는 대부분 콘텐츠와 컨테이너를 꼭 구분해야 이룰 수 있다.

클래스명에 네임스페이스를 두어라.

/* 스타일이 오염될 위험성이 높은 구조 */
.widget { }
.widget .title { }

/* 스타일이 오염될 위험성이 낮은 구조 */
.widget { }
.widget-title { }

모듈 하위 선택자에 모듈명 등으로 네임스페이스를 주어 다른 모듈에서 정의된 선택자에 의해 오염되거나 반대로 다른 모듈을 오염시키는 경우를 방지하라.

수정 클래스명으로 컴포넌트를 확장하라

/* Bad */
.widget { }
#sidebar .widget { }

/* Good */
.widget { }
.widget-sidebar { }

부모 요소를 기반으로 컴포넌트를 확장하는 것의 문제를 이미 보았다. 수정 클래스는 특정 부모 요소 밑에서만 쓸 수 있는 부모 요소 기반 확장과는 다르게 어디서든 쓸 수 있다. 그리고 여러번 재사용 될 수 있다. 그리고 HTML 코드 내에서 개발자의 의도를 잘 드러낸다.

논리적 구조에 맞추어 CSS를 구성하라.

Jonathan Snook은 그의 훌륭한 책 SMACSS에서 CSS 규칙을 4가지 범주로 나누도록 주장한다:

  • 기본(base): 리셋 코드 + 요소별 기본 속성
  • 레이아웃(layout): 그리드 시스템 같은 사이트 전체적 레이아웃 헬퍼
  • 모듈(modules): 재사용 가능한 시각 요소
  • 상태(state): 자바스크립트로 온/오프 가능한 스타일

SMACSS에서는 모듈이 CSS 규칙의 대부분을 구성.

컴포넌트는 독립(standalone) 비주얼 요소이다. 템플릿은 반대로 빌딩 블록이다. 템플릿은 독립적으로 쓰이지 않으며 룩앤필을 정의하는 경우가 드물다. 대신 단일 반복 패턴 양식 구성 요소를 함께 넣을 수 있다.

모달 다이얼로그 컴포넌트로 구체적인 예제를 들어보자. 모달 다이얼로그는 헤더에 배경 그라데이션이 제공되고 그 주변에 그림자가 제공된다. 우상단 구석에 닫기 버튼이 있고 position: fixed로 페이지 정중앙에 위치한다. 이 네가지 패턴은 사이트 내에서 계속하여 재사용되니, 그 때마다 그 패턴을 다시 코딩하고 싶지 않을 것이다. 이와 같은 것들이 템플릿이며 함께 모달 컴포넌트를 구성하게 된다.

난 보통 좋은 이유를 갖기 전까지는 HTML에 템플릿용 클래스를 쓰지 않는다. 대신 전처리기를 통해 템플릿 스타일을 넣어 컴포넌트 정의한다. 이걸 조금더 토론해보고 나중에 좀 더 자세히 다루어보겠다.

스타일링 용도로만 클래스를 사용하라.

큰 프로젝트를 진행하다 보면 목적을 알 수 없는 클래스명이 포함된 HTML 요소를 만나게 되곤 한다. 이런 클래스명은 지우고 싶어도 짐작하지 못한 목적이 있을까봐 주저하게 된다. 이런 일이 반복해서 일어나면 이렇게 삭제를 두려워하는 목적없는 클래스명들이 늘어나게 된다.

문제는 이런 클래스명이 프론트엔드 개발자에게 너무 많은 책임을 부여하는 것이다. HTML 요소를 스타일링하고, 자바스크립트 훅을 달고, 기능 탐지(feature detection)을 위해 추가하고, 자동화 테스트를 위해 추가하고, 등등.

이건 문제다. 어플리케이션에서 클래스명이 너무 많은 부분에 사용되면 그것들을 지우기 무서워진다.

하지만, 약속된 컨벤션이 있다면 이 문제는 완전히 해결될 수 있다. 당신이 HTML의 클래스명을 볼 때 그것의 목적이 뭔지 바로 이야기할 수 있어야한다. 나의 추천은 스타일링 이외의 목적을 가진 클래스명에 접두사를 주는 것이다. 난 자바스크립트용으로 .js-를, Modernizr 클래스명에 .supports-를 쓴다. prefix 없는 클래스명은 스타일링 전용이다.

이것은 안쓰는 클래스명을 스타일시트 디렉토리만 찾아보고 나서 지울 수 있게 만들어준다. 심지어 이 절차를 자바스크립트를 통해 document.styleSheets 객체와 HTML을 비교하여 자동화 할 수도 있다.document.styleSheets에 없는 클래스명은 안전하게 지울 수 있는 것이다.

일반적으로 콘텐츠에서 표현을 분리해내는 것이 가장 좋은 방법이듯, 표현에서 기능을 분리해내는 것 또한 중요하다. 스타일이 적용된 클래스를 자바스크립트 훅용으로 사용하는 것은 CSS와 자바스크립트의 의존도를 높인다. 이것은 기능의 깨짐 없이 그런 요소들의 표현을 업데이트 하기 어렵거나 불가능하게 만든다.

논리적 구조로 클래스명을 지어라.

요즘 많은 사람들이 CSS의 단어 구분자로 하이픈(-)을 쓴다. 하지만 하이픈만으로는 여러 타입의 클래스명들을 구분하기에는 충분하지 않다.

Nicolas Gallagher는 최근에 이 문제의 해결법을 제시했고 나도 약간의 수정을 거쳐 적용 후 큰 성과를 얻었다. 명명 규칙의 필요성을 설명하기 위해 다음을 보자:

/* 컴포넌트 */
.button-group { }

/* 컴포넌트 수정 (.button 수정) */
.button-primary { }

/* 컴포넌트 서브 객체 (.button과 함께 적용) */
.button-icon { }

/* 이건 컴포넌트용 클래스일까? 아니면 레이아웃용 클래스일까? */
.header { }

위의 클래스명들을 보면 어떤 타입의 규칙을 적용하려는지 설명할 수 없다. 이건 개발 내내 혼란을 증가시킬 뿐 아니라 CSS와 HTML의 자동화 테스트를 어렵게 만든다. 구조화된 명명 규칙은 HTML에 선언된 클래스명을 보고 다른 클래스명과의 연견성을 정확히 알 수 있게 해준다. 이는 이름 짓기를 쉽게, 그리고 테스트 가능하게 만들어준다.

/* 템플릿 규칙 (Sass placeholder 사용) */
%template-name
%template-name--modifier-name
%template-name__sub-object
%template-name__sub-object--modifier-name

/* 컴포넌트 규칙 */
.component-name
.component-name--modifier-name
.component-name__sub-object
.component-name__sub-object--modifier-name

/* 레이아웃 규칙 */
.l-layout-method
.grid

/* 상태 규칙 */
.is-state-type

/* 스타일되지 않는 자바스크립트 훅 */
.js-action-name처음 예제를 고쳐보면:

/* 컴포넌트 */
.button-group { }

/* 컴포넌트 수정 (.button 수정) */
.button--primary { }

/* 컴포넌트 서브 객체 (.button과 함께 적용) */
.button__icon { }

/* 레이아웃 클래스 */
.l-header { }

도구

효과적이고 잘 조직된 CSS 구조를 유지하는 것은 특히 큰 팀일수록 매우 어렵다. 몇가지 나쁜 규칙이 눈덩이처럼 불어나서 전체를 엉망으로 만들 수 있다. 일단 어플리케이션의 CSS가 특성 전쟁의 영역으로 들어가고!important를 추가해야 할 상황이 오면 코드를 다시 만들지 않고서는 돌이키키 어렵다. 해결책은 처음부터 이러한 문제가 발생되지 않도록 하는 것이다.

다행히도 CSS 구조를 훨씬 쉽게 제어할 수 있는 도구가 있다.

전처리기

요즘에는 전처리기에 대한 언급 없이는 CSS 도구를 이야기할 수 없고, 이 글도 크게 다르지 않다. 하지만 그 유용성을 이야기하기 전에 몇가지 경고를 할까 한다.

전처리기는 CSS를 빠르게 작성하는 것을 도와주지만 잘 작성하는 것을 도와주지는 않는다. 궁극적으로 일반 CSS로 컴파일되면 동일한 규칙이 적용된다. 전처리기가 CSS를 빠르게 작성하는 것을 도와주면 나쁜 CSS 코드를 빠르게 작성하는 것도 도와준다는 뜻이 된다. 그래서 전처리기로 문제를 해결을 고민하기 전에 좋은 CSS 구조에 대한 이해는 중요하다.

많은 전처리기의 소위 “기능”들이 실제로는 CSS 구조에 악영향을 끼칠 수 있다. 다음은 내가 많은 비용을 들여서라도 피하고자 하는 “기능”들이다. (모든 전처리기 언어에 해당하는 내용이지만, 이 가이드라인은 구체적으로 SASS에 맞춰져있다.)

  • 코드 그룹화를 위해 규칙을 중첩하지 말라. 결과 CSS 선택자가 당신이 원하는 것일때만 중첩하라.
  • 인자를 넘길 때를 제외하고는 mixin을 사용하지 말라. 인자 없는 mixin은 확장 가능한 형태의 템플릿으로 쓰는 것이 훨씬 낫다.
  • 단일 클래스가 아닌 선택자에서 @extend를 사용하지 말라. 이는 디자인 관점에서 이해되지 않으며 컴파일된 CSS 코드의 양을 증가시킨다.
  • 컴포넌트 수정 규칙에 @extend를 사용하지 말라. 이는 상속 체인을 잃게 만든다. (아래에서 추가로 다루겠다.)

전처리기의 가장 훌륭한 부분은 @extend와 %placeholder 같은 함수이다. 이 두가지는 CSS 코드의 양을 늘어나게 하거나 관리하기 어려운 많은 양의 기본 클래스명을 만들어내지 않고 CSS 추상화를 관리할 수 있게 해준다.

@extend는 언젠가 HTML에 그 클래스명들을 쓰게 될 것이므로 주의를 기울여 사용되어야 한다. 예로, 처음@extend를 배울 때 그것은 다음과 같이 클래스명을 수정하는 식으로 사용하도록 유혹한다:

.button {
  /* button styles */
}

/* Bad */
.button--primary {
  @extend .button;
  /* modification styles */
}

이렇게 할 때의 문제점은 HTML에서의 상속 구조를 잃게 된다는 점이다. 이러면 자바스크립트로 모든 버튼을 선택하기가 어려워진다.

일반적으로 난 결코 UI 컴포넌트나 나중에 그 종류를 알고 싶다고 생각하는 것들에 @extend를 적용하지 않는다. 이것은 템플릿을 위한 것이고 템플릿과 컴포넌트를 구분하는 것을 도와주기 위한 방법이다. 템플릿은 어플리케이션 로직을 대상으로 작성하지 말아야 하는 것이고 그 결과로 전처리기에서 안전하게 @extend를 통해 확장할 수 있게 되는 것이다.

다음은 윗쪽에서 참조된 모달 예제를 사용해본 코드이다:

.modal {
  @extend %dialog;
  @extend %drop-shadow;
  @extend %statically-centered;
  /* other modal styles */
}

.modal__close {
  @extend %dialog__close;
  /* other close button styles */
}

.modal__header {
  @extend %background-gradient;
  /* other modal header styles */
}

CSS Lint

Nicole Sullivan과 Nicholas Zakas는 개발자들이 그들의 CSS 코드에서 나쁜 습관을 발견하는 것을 도와주기 위한 코드 품질 도구로서 CSS Lint를 만들었다. CSS Lint 사이트에서는 CSS Lint를 아래와 같이 정의한다:

CSS Lint는 당신의 CSS 코드의 문제점을 지적한다. 기본 구문 검사 뿐만 아니라 잘못된 패턴 또는 비효율적인 신호를 갖는 코드 규칙들도 검사한다. 규칙들은 모두 플러그인할 수 있어서, 당신만의 규칙을 만들어서 적용할 수 있으며 원치 않는 규칙은 제거할 수도 있다.

일반 규칙 집합은 대부분의 프로젝트에 대해 완벽하지는 않을 수 있지만, CSS lint의 최고의 기능은 원하는대로 정확히 커스터마이징할 수 있다는 것이다. 즉, 기본 기능 목록에서 원하는 것을 고르고 또한 필요한 규칙을 직접 작성할 수 있다는 것이다.

CSS Lint와 같은 도구는 최소한 일관성과 규칙 준수의 기준으로서 모든 대규모 팀에 꼭 필요하다.

내가 위에 제시한 규칙들을 기반으로 특정 안티패턴을 탐지하는 규칙을 작성하기 매우 쉬워졌다. 다음은 내가 사용하는 몇가지 제안이다:

  • 선택자에 ID를 쓰지 말라.
  • 어떤 다중 선택자에도 의미없는 타입 선택자(예: divspan)를 쓰지 말라.
  • 3가지 이상의 규칙을 조합하지 말라.
  • js-로 시작하는 클래스명은 허용되지 않는다.
  • l-로 시작하지 않는 규칙에 레이아웃 및 포지셔닝 관련 속성을 쓴 경우 경고한다.
  • 클래스명 자체적으로 생성된 규칙이 다른 선택자의 자식 규칙으로서 사용된 경우 경고한다.

이것들은 그저 단순한 제안이다. 하지만 아마 당신의 프로젝트에 적용할만한 규칙들을 만들 아이디어를 얻을 수 있었을 것이다.

HTML Inspector

앞서 나는 HTML 클래스명과 연결된 모든 스타일시트를 검색하는 것이 매우 쉬울 것이고, HTML에서 사용했지만 스타일시트에 정의되지 않는 클래스명을 경고해주는 것을 제안했다. 나는 현재 이 과정을 쉽게 만들어줄 HTML Inspector라는 도구를 개발중이다.

HTML Inspector는 당신의 HTML을 탐색하여 (CSS Lint와 유사하게)어떤 컨벤션이 깨진 경우 에러와 경고를 발생시킬 수 있는 당신만의 규칙을 작성할 수 있도록 해준다. 난 현재 다음과 같은 규칙들을 사용하고 있다:

  • 같은 ID가 한번 이상 쓰인 경우 경고
  • 화이트리스트(js- 같은 접두사를 가진 클래스명 등)를 제외하고 어떤 스타일시트에도 정의되지 않은 클래스명을 쓸 수 없다.
  • 수정 클래스명은 기본 클래스명과 함께 사용되어야만 한다.
  • divspan 요소는 클래스명 없이 HTML에서 사용되지 말아야 한다.

요약

CSS는 단순한 비주얼 디자인이 아니다. CSS 코드를 작성한다는 핑계로 유용한 프로그래밍의 모범 사례들을 무시하지 말라. OOP, DRY, 개방/폐쇄 원칙, 관심사의 분리 등의 개념은 CSS에도 적용할 수 있다.

결론: 코드를 작성하는 동안 그 코드들이 개발을 쉽게 하고 장기적으로 쉽게 유지보수할 수 있는 코드인지를 확실히 판단할 수 있도록 하라.