Translation: CSS Modules

2015-08-21 17:55 +09:00

이 글은 Glen Maddern이 작성한 CSS Modules라는 글을 한글로 번역한 것입니다. 관심이 생기신다면 원문도 읽으시는 것을 추천드립니다.


미래에 오신 것을 환영합니다

최근 CSS개발의 전환점을 꼽는다면, 아마 2014년 11월에 있었던 Christopher Chedeau의 “CSS in JS”발표를 선택할 것입니다. 당시는 마치 고에너지 충돌 후에 입자들이 제각각의 방향으로 흩어지듯이, 서로 다른 생각들이 흩어져 있던 과도기였습니다. 예컨대 React Style, jsxstyle, Radium같은 라이브러리들이 React에서의 스타일링에 있어 가장 최신의, 잘 만들어진, 쓸만한 것들이었습니다(그들이 프로젝트 Readme파일에 명시해두었듯이 말이죠). 만약 발명이라는 것이 가장 인접한 가능성을 찾아가는 과정이라면, Christopher는 수많은 그러한 가능성들을 더욱 인접하게 만들었다고 할 수 있습니다.

수많은 사람들에게 이 슬라이드는 정말 확 와닿았을 겁니다.
수많은 사람들에게 이 슬라이드는 정말 확 와닿았을 겁니다.

번역: CSS가 확장해감에 따라 생기는 문제들

  1. 전역 네임스페이스
  2. 의존성
  3. Dead Code Elimination
  4. Minification
  5. 변수 공유
  6. 비결정적 해석
  7. 고립

이러한 정당화된 문제들은 대부분의 경우 CSS 코드베이스가 거대화해감에 따라 이런 저런 방향에서 영향을 끼칩니다. Christopher는 모든 문제들이 자바스크립트를 사용해서 스타일링 하는 것만으로도 해결된다는 점을 지적했고, 실제로 그 지적은 옳습니다. 하지만 자바스크립트 스타일링 또한 스스로의 복잡성과 여러 특성들을 갖고있습니다. 제가 위에서 언급한 프로젝트들에서 :hover 상태를 어떻게 처리했는지 한번 보시면, 그들의 접근들이 얼마나 광범위한지 아실 수 있을 겁니다. CSS에서는 정말로 오래전에 이미 해결된 것인데 말이죠.

CSS Modules팀은 그런 문제들을 진취적으로 해결할 수 있다고 생각했습니다. 즉, CSS에서 우리가 좋아하는 것들은 남기면서도, 이전의 자바스크립트스타일링 커뮤니티에서 만들어낸 훌륭한 작업물들 위에서 무언가 만들어내는 것입니다. 따라서 우리는 우리의 접근 방법에 대해서 자부심이 있고, CSS의 좋은 점들도 견고하게 지켜냈지만, 사실 여러 선행자들이 다른 방면에서 열심히 노력했던 덕을 보고있는 셈이죠. 고마워요, 친구들! 👬👫👭

자, 이제 CSS Modules가 대체 무엇이고, 왜 그것이 미래인 것인지 알려드리겠습니다.

이렇게 우리는 정말로 최선을 다해 CSS에 대해서 생각했습니다.(Jony Ive 톤으로)
이렇게 우리는 정말로 최선을 다해 CSS에 대해서 생각했습니다.(Jony Ive 톤으로)

1단계. 지역변수를 기본으로.

CSS Modules에서는, 각 파일이 별도로 컴파일 되기 때문에 일반적인 이름들로 된 간단한 클래스 선택자(selector)들을 사용할 수 있습니다. 즉, 전역 공간을 오염시킬 걱정을 할 필요가 없다는 말이죠. 예를 들어 다음의 4가지 상태를 가지고 있는 간단한 제출 버튼을 만든다고 합시다.

Normal
Disabled
Error
In Progress

CSS Modules 이전의 구현

만약 Suit/BEM-style 클래스명과 일반적인 CSS와 HTML을 사용한다면 이렇게 될 것입니다:

/* components/submit-button.css */
.Button { /* all styles for Normal */ }
.Button--disabled { /* overrides for Disabled */ }
.Button--error { /* overrides for Error */ }
.Button--in-progress { /* overrides for In Progress */
<button class="Button Button--in-progress">Processing...</button>

위의 구현도 충분히 좋습니다, 정말로요. 네종류의 변형을 정의했지만, BEM-style을 사용한다는 것은 상속 선택자(nested selector)는 없다는 의미입니다. 우선 다른 기존의 스타일이나 우리가 가져다 쓴 외부 라이브러리들과의 중복을 (아마도)막기 위해서 대문자 한글자를 가진 기본적인 Button을 정의했습니다. 그리고는 --modifier 문법을 이용해서 다른 변형들이 위의 기본 스타일을 필요로 한다는 것을 명확히 했습니다.

전반적으로, 위의 코드는 상당히 명시적이고 유지보수가 가능한 코드입니다. 허나 이를 위해서 네이밍 규칙에 있어 더럽게 많은 의도적인 노력을 기울여야 했습니다. 하지만 이것이 기본 CSS로 할 수 있는 최선입니다.

CSS Modules을 사용한 구현

CSS Modules를 사용한다면 우리의 이름들이 너무 일반적인 것에 대해 걱정할 필요가 없습니다. 그냥 제일 적절해 보이는 것을 쓰세요:

/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */

혹시 “button”이라는 단어를 전혀 쓰지 않았다는 점을 발견하셨나요? 왜 그렇게 했을까요? 파일명이 이미 “submit-button.css”이기 때문이죠. 다른 언어에서 모든 지역 변수명의 앞에 현재 작업중인 파일명을 붙일 필요는 없습니다. 그리고 CSS도 마찬가지여야 하죠.

CSS Modules는 자바스크립트에서 파일을 로드하듯이 requireimport를 써서 컴파일 됩니다. 이 방법으로 위의 내용이 가능해지는 것입니다.

/* components/submit-button.js */
import styles from './submit-button.css';

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

실제 사용되는 클래스명은 자동으로 생성되고, 유니크함이 보장됩니다. CSS Modules는 파일들을 컴파일해서 ICSS라는 포맷의 파일로 만듭니다(이에 대한 내용은 제 블로그 포스트를 참조하세요). 이 포맷에 의해 CSS와 자바스크립트간에 의사소통이 가능해집니다. 따라서, 어떤 앱을 실행했을 때 다음과 같은 결과를 보시게 될겁니다.

<button class="components_submit_button__normal__abc5436">
  Processing...
</button>

만약 위와 같은 결과를 DOM에서 보시게 된다면, 잘 동작하고 있다는 증거입니다!

당신이 저 고릴라고, CSS Modules가 상어입니다.
당신이 저 고릴라고, CSS Modules가 상어입니다.

(credit: Christopher Hastings)

네이밍 규칙

우리의 버튼 예시를 한번 더 봅시다:

/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */

어떤 한 클래스가 “기초”가 되고 다른 것들이 그것을 “덮어쓰는” 모양새가 아니라, 모든 클래스들이 독립적인 것을 보실 수 있습니다. CSS Modules에서는 모든 클래스가 그 변형에 필요한 모든 스타일을 가지고 있어야 합니다(어떻게 동작하는지는 조금 이따 보여드리겠습니다). 이는 자바스크립트에서 클래스들을 사용할 때 훨씬 편리하게 해줍니다:

/* 이렇게 하지 마세요 */
`class=${[styles.normal, styles['in-progress']].join(" ")}`

/* 단일 클래스명을 사용하는 것이 큰 차이를 만듭니다 */
`class=${styles['in-progress']}`

/* 캐멀케이스는 심지어 훨씬 낫죠 */
`class=${styles.inProgress}`

키 입력을 한번 할 때 마다 돈을 받는다면 이야기는 다르지만요!

React 예시

CSS Modules은 React만을 위한 특별한 무언가가 있는 것은 아닙니다. 하지만 React와 함께할 때 CSS Modules는 특히 사용하기 좋습니다. 그런 의미에서 약간 더 복잡한 예제를 한번 보는 것도 가치가 있겠지요:

/* components/submit-button.jsx */
import { Component } from 'react';
import styles from './submit-button.css';

export default class SubmitButton extends Component {
  render() {
    let className, text = "Submit"
    if (this.props.store.submissionInProgress) {
      className = styles.inProgress
      text = "Processing..."
    } else if (this.props.store.errorOccurred) {
      className = styles.error
    } else if (!this.props.form.valid) {
      className = styles.disabled
    } else {
      className = styles.normal
    }
    return <button className={className}>{text}</button>
  }
}

정의된 스타일들을 사용할 때 생성된 CSS의 클래스 명들이 전역공간 내에서 안전한 이름인지 신경 쓸 필요가 없습니다. 이는 곧 사용자들이 스타일링 자체가 아니라 컴포넌트에 집중할 수 있게 해줍니다. 또한 CSS를 사용함에 있어서 있었던 지속적인 컨텍스트 스위칭을 없애줍니다. 사용자들은 지금까지 그런 불편함들을 참고 견뎌왔다는 것에 놀랄 것입니다.

하지만 이는 시작에 불과합니다. 작성된 스타일들을 어떻게 배치할 것인지에 대해서 생각해봅시다. 그에 있어서도 CSS Modules는 당신을 고통으로부터 지켜줄 것입니다.

2단계. 구성이 모든 것이다.

이전에 각 클래스가 각 상태의 버튼에 해당하는 모든 스타일들을 가지고 있어야 한다고 설명했습니다. 이는 BEM모델에서 각 상태가 보통 한개 이상의 클래스를 가지는 것과 대조됩니다.

/* BEM Style */
innerHTML = `<button class="Button Button--in-progress">`

/* CSS Modules */
innerHTML = `<button class="${styles.inProgress}">`

하지만 잠시만요, 그럼 어떻게 모든 상태들이 공유하는 스타일들을 정의할까요? 그 해답이 아마도 CSS Modules가 가지는 가장 강력한 무기인 구성(composition)일 것입니다.

.common {
  /* all the common styles you want */
}
.normal {
  composes: common;
  /* anything that only applies to Normal */
}
.disabled {
  composes: common;
  /* anything that only applies to Disabled */
}
.error {
  composes: common;
  /* anything that only applies to Error */
}
.inProgress {
  composes: common;
  /* anything that only applies to In Progress */
}

composes 키워드는 .normal.common의 모든 상태를 가진다는 것을 나타냅니다. Sass로 치면 @extends같은 기능이죠. 하지만 Sass는 CSS 선택자들을 덮어씌워서 그것을 가능하게 하는 반면, CSS Modules는 어떤 클래스를 자바스크립트로 추출할 지를 변경합니다.

Sass의 처리방식

위에서 언급했던 BEM 예시를 가져와서 한번 Sass의 @extends를 적용하여 수정해보겠습니다:

.Button--common { /* font-sizes, padding, border-radius */ }
.Button--normal {
  @extends .Button--common;
  /* blue color, light blue background */
}
.Button--error {
  @extends .Button--common;
  /* red color, light red background */
}

위의 Sass는 CSS로 다음과 같이 컴파일됩니다:

.Button--common, .Button--normal, .Button--error {
  /* font-sizes, padding, border-radius */
}
.Button--normal {
  /* blue color, light blue background */
}
.Button--error {
  /* red color, light red background */
}

그 이후에는 그냥 정의된 클래스 중 하나를 마크업에 <button class="Button--error">와 같이 사용해서 원하는 공용 스타일과 특정 스타일을 동시에 얻어낼 수 있습니다. 이는 실로 강력한 개념이지만, 구현 그 자체에 주의하지 않으면 안되는 에지 케이스나 함정이 숨어있습니다. 이에 대한 문제점과 읽을 거리들을 Hugo Giraudel이 여기에 모아주었습니다.

CSS Modules의 처리방식

composes 키워드는 개념상으로는 @extends와 비슷하지만, 전혀 다르게 동작합니다. 시연을 위해서 다음 예시를 보겠습니다:

.common { /* font-sizes, padding, border-radius */ }
.normal { composes: common; /* blue color, light blue background */ }
.error { composes: common; /* red color, light red background */ }

위의 코드는 컴파일 되어 브라우저에서 볼 때는 다음과 같이 보이게 됩니다:

.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 { /* blue color, light blue background */ }
.components_submit_button__error__1638bcd { /* red color, light red background */ }

자바스크립트에서 import styles from "./submit-button.css"를 실행하면 다음과 같은 오브젝트를 반환합니다:

styles: {
  common: "components_submit_button__common__abc5436",
  normal: "components_submit_button__common__abc5436 components_submit_button__normal__def6547",
  error: "components_submit_button__common__abc5436 components_submit_button__error__1638bcd"
}

즉 우리는 여전히 styles.normal이나 styles.error와 같이 사용할 수 있지만 실제로 DOM에는 여러 클래스가 렌더링되게 되는 것이죠.

<button class="components_submit_button__common__abc5436 
               components_submit_button__normal__def6547">
  Submit
</button>

이것의 구성(composes)의 힘입니다. 이미 작성된 마크업을 수정하거나 CSS 선택자들을 다시 쓰지 않고도, 여러 독립적인 스타일 그룹들을 합칠 수 있는 것이죠 👌

단계 3. 파일 간 공유

Sass나 LESS를 사용해서 작업하는 경우, @import를 써서 가져온 파일들은 동일한 전역 공간에 존재하게 됩니다. 그렇게 변수나 믹스인을 한 파일에 정의해서, 다른 모든 파일에서 가져다 쓰는 것이죠. 그런 방식은 편리할 수는 있지만, 파일 간에 전역 변수명이 충돌할 지 모르는 상태가 되면(그것은 다른 전역 이름공간이기 때문에) 어쩔 수 없이 variables.scsssettings.scss를 수정해야만 하는 상태가 됩니다. 그렇게 되면 어떤 컴포넌트가 어떤 변수에 의존성이 있는지를 보기가 힘들게 됩니다. 그리고 당신의 설정 파일들은 다루기가 굉장히 힘들어지죠.

물론 조금 더 나은 방법론들도 있습니다(사실 Ben Smithett가 작성한 Sass와 Webpack을 같이 사용하는 방법은 직접적으로 CSS Modules 프로젝트에 영향을 주었습니다. 꼭 읽어보세요). 하지만 Sass를 사용하는 한 여전히 Sass의 전역적 특성에 묶일 수 밖에 없습니다.

CSS Modules는 동시에 한 파일씩만 처리되기 때문에, 오염될 전역 환경 자체가 없습니다. 또한 자바스크립트에서 하는 것 처럼 importrequire를 써서 의존성을 관리하기 때문에, CSS Modules는 파일간에 구성(composes)를 가능하게 합니다:

/* colors.css */
.primary {
  color: #720;
}
.secondary {
  color: #777;
}
/* other helper classes... */
/* submit-button.css */
.common { /* font-sizes, padding, border-radius */ }
.normal {
  composes: common;
  composes: primary from "../shared/colors.css";
}

구성(composition)을 이용해서, 완전히 일반적인 파일인 colors.css에 접근해 원하는 클래스를 지역 변수명으로 참조할 수 있습니다. 또한 구성은 어떤 클래스들을 추출할지 만을 변경하고 CSS자체를 변경하지 않기때문에, composes문들은 브라우저에 보여지기 전에 CSS에서 제거됩니다:

/* colors.css */
.shared_colors__primary__fca929 {
  color: #720;
}
.shared_colors__secondary__acf292 {
  color: #777;
}
/* submit-button.css */
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 {}
<button class="shared_colors__primary__fca929
               components_submit_button__common__abc5436 
               components_submit_button__normal__def6547">
  Submit
</button>

사실, 브라우저에 보여지기 전까지는, 우리의 지역 클래스명인 “normal”은 자기 자신만의 스타일은 전혀 갖지 않습니다. 그리고 이것은 좋은 것이죠! 이는 결국 지역적으로 의미있는 객체(“normal”이라는 이름을 가진 항목)를 CSS 코드 한줄도 추가하지 않고 만들어낼 수 있다는 것입니다. 이렇게 함으로써 우리 사이트가 시각적으로 불연속하게 되는 것을 막을 수 있고, 또한 유저들의 브라우저에서 읽게되는 불필요한 코드의 팽창도 막을 수 있습니다.

참고: 그러한 빈 클래스들은 csso같은 툴을 사용해서 쉽게 찾아 제거할 수 있습니다.

단계 4. 단일 책임 모듈들

구성(composition)은 유저들로 하여금 한 요소에 있어 어떤 스타일들이 그것을 구성하는지가 아니라, 그것이 무엇인지를 서술하게 돕습니다. 그렇기에 그것이 강력한 것이죠. 그것은 개념적 개체들(elements)들을 양식적 개체들(rules)로 대응시키는데 있어 기존과는 다른 방법인 것입니다. 기본적이고 평범한 CSS로 작성된 다음 예시를 한번 보시죠:

.some_element {
  font-size: 1.5rem;
  color: rgba(0,0,0,0);
  padding: 0.5rem;
  box-shadow: 0 0 4px -2px;
}

한개의 요소와 여러 스타일들. 간단하죠. 하지만, 사실 여기에는 문제가 있습니다. 즉 color, font-size, padding, box-shadow와 같은 모든 것이 이곳에 명시되어 있지만, 사실 그 스타일들은 다른 곳에서 사용될 지도 모른다는 것입니다. Sass를 써서 리팩토링해보도록 하겠습니다:

$large-font-size: 1.5rem;
$dark-text: rgba(0,0,0,0);
$padding-normal: 0.5rem;
@mixin subtle-shadow {
  box-shadow: 0 0 4px -2px;
}

.some_element {
  @include subtle-shadow;
  font-size: $large-font-size;
  color: $dark-text;
  padding: $padding-normal;
}

조금 나아졌군요. 하지만 전체 내용에 있어 반정도를 추출해냈을 뿐입니다. $large-font-size가 타이포그래피를 위해서 존재한다거나 $padding-normal이 레이아웃을 위해서 존재한다는 것은 그 이름에 의해서 표현될 뿐이지 어느 곳에서도 강제되지 않습니다. 만약 box-shadow와 같은 선언 그 자체를 변수화할 수 없는 경우에는 어쩔 수 없이 @mixin이나 @extends를 사용해야합니다.

CSS Modules과 함께라면

구성(composition)을 사용해서 재사용 가능한 부분들로 컴포넌트를 정의할 수 있습니다:

.element {
  composes: large from "./typography.css";
  composes: dark-text from "./colors.css";
  composes: padding-all-medium from "./layout.css";
  composes: subtle-shadow from "./effect.css";
}

위의 형식에 의해 자연스레 많은 단일 목적의 파일들이 생겨납니다. 이는 다른 목적의 스타일들을 이름공간이 아니라 파일시스템으로 구분짓는 것입니다. 만일 여러 클래스들을 한 파일로부터 구성하고 싶다면 다음과 같이 한줄로 선언하는 것도 가능합니다:

/* this short hand: */
.element {
  composes: padding-large margin-small from "./layout.css";
}

/* is equivalent to: */
.element {
  composes: padding-large from "./layout.css";
  composes: margin-small from "./layout.css";
}

이런 방법들은, 사이트에서 사용하는 모든 시각적 요소에 대해 별명(aliases)을 붙임으로써 극히 세분화된 클래스들을 사용할 수 있게 합니다.

.article {
  composes: flex vertical centered from "./layout.css";
}

.masthead {
  composes: serif bold 48pt centered from "./typography.css";
  composes: paragraph-margin-below from "./layout.css";
}

.body {
  composes: max720 paragraph-margin-below from "layout.css";
  composes: sans light paragraph-line-height from "./typography.css";
}

위의 방법론은 제가 조금 더 연구해보고 싶은 부분이기도 합니다. 제 생각에는, 이런 방법론이 Tachyons와 같은 아토믹한(atomic) CSS 방법론의 가장 좋은 면과 Semantic UI같은 것들의 가독성을 합쳐놓은 것 같습니다. 진짜배기의, 신뢰할만한 격리(isolation)와 함께요.

하지만 우리는 CSS Modules라는 이야기의 시작점에 서있을 뿐입니다. 한번 다음 프로젝트에서 CSS Modules을 써보시고, 우리가 그 미래를 잘 만들어갈 수 있도록 도와주세요.

자, 시작해봅시다!

CSS Modules과 함께함으로써 당신과 팀의 CSS에 관련된 지식과 제품들을 관리하는 것이 보다 쉬워졌으면 좋겠습니다. 뿐만 아니라 그것들이 더 편안하고 생산적으로 느껴졌으면 좋겠습니다. 우리는 계속해서 최소한의 새로운 문법들을 추가중이고, 이미 유저들이 작업중인 방법과 흡사한 예시들을 발견하기 위해 애쓰고 있습니다. Webpack, JSPM, Browserify를 위한 예시 프로젝트가 준비되어 있으니 만일 그중 하나를 사용중이시라면 한번 봐주세요. 우린 CSS Modules를 사용할 수 있는 새로운 환경들에 대해서도 항상 예의주시하고 있습니다. 이미 서버사이드 NodeJS에 관련된 작업이 진행중이고, 레일즈를 위한 작업도 준비중이죠.

하지만 심지어는 더 쉽게 사용해볼 수 있도록, 아무것도 설치하지 않고도 예제들을 둘러볼 수 있는 작은 Plunkr를 만들어두었습니다! 한번 살펴보세요:

준비가 되신 것 같다면 CSS Modules의 주저장소를 살펴보세요. 질문이 있으시면, 이슈를 올려서 대화의 막을 열어주세요. CSS Modules팀은 작기때문에 아직 모든 사용예시를 둘러보지는 못했습니다. 그렇기 때문에 여러분의 이야기를 듣고싶습니다.

그럼 행복한 스타일링 하세요, 친구들!

read other posts