Skip to content

Interoperable CSS (ICSS)

Frontend / CSS
  • css
  • css-modules
  • icss

해당 글은 Interoperable CSS (ICSS) 문서 전체를 한글로 번역한 글입니다.

이 문서는 CSS Modules를 가능하게 만든 저수준 파일 포맷의 명세를 설명합니다. 최종 사용자가 아니라 로더를 구현하는 사람을 위한 문서입니다. 상위 레벨 명세가 궁금하다면 CSS Modules 명세를 보세요.

이 명세가 필요한 이유

JavaScript 워크플로가 컴포넌트를 조립하는 방향으로 흘러가면서, CSS 워크플로도 그 뒤를 따랐습니다. 다만 CSS 쪽의 진전은 전부 **관례(convention)**에 기댄 것이었고, 언어 차원의 지원을 받은 적은 없습니다. 가장 눈에 띄는 예가 BEM 방법론인데, 사실 대부분의 접근법이 같은 논리를 공유합니다.

  • 스타일은 하나의 컴포넌트 안으로 범위가 좁혀져야 한다
  • 그런데 모든 CSS 선택자는 전역이다
  • ∴ 전역에서 유일한 선택자를 보장할 관례를 만들자

BEM에서는 그 관례가 .block-name__element-name--modifier-name 또는 .BlockName__elementName--modifierName 형태로 나타납니다.

언어를 넘나드는 명시적 의존성

Webpack 로더의 근본적인 기능 중 하나는 소스 파일의 종류와 무관하게 각 파일의 의존성을 명시적으로 기술할 수 있다는 점입니다. (JSPM의 핵심이기도 하고, Browserify에서도 어렵지 않게 가능합니다.) 컴포넌트 워크플로에서 CSS에 이를 적용하면 다음과 같은 모습이 됩니다.

// 이 CSS가 이 JS의 의존성임을 표시합니다.
// 로더에 따라 CSS는 DOM에 주입되거나
// 별도의 CSS 번들로 묶입니다.
require( './my-component.css' );
var MyComponent = // 컴포넌트 정의
module.exports = MyComponent;

이제 my-component.js가 로드되거나 번들링될 때마다, 다른 의존성과 똑같이 대응하는 CSS도 반드시 함께 존재하게 됩니다. 이 관례는 지금까지 없던 새로운 가능성을 열어주고, 동시에 새로운 명세를 요구합니다.

CSS와 JS의 상호 운용

CSS를 JS의 의존성으로 다루는 순간, 여태까지 불가능했던 일을 할 수 있게 됩니다. 바로 CSS에서 JS로 값을 넘기는 것입니다. 예를 들어 이런 코드 대신

// 부수 효과로 CSS를 로드합니다
require( './my-component.css' );

이제는 로더에 임의의 정보를 실어 보낼 수 있습니다.

// 부수 효과로 CSS를 로드하면서,
// 컴포넌트를 렌더링할 때 쓸 수 있는 값을 돌려받습니다.
var styles = require( './my-component.css' );
// 예를 들어 동적으로 생성된 클래스명일 수 있습니다:
elem.addClass( styles.elemClass );

Webpack, JSPM, Browserify 같은 현대적인 멀티 포맷 로더가 새로 가져다준 핵심 능력이 바로 이것이고, CSS Modules 명세는 이 능력으로 가능해진 CSS 기법들에 대한 하나의 견해 있는 제안입니다. 다만 더 근본적인 층위에서는, 이 심볼들이 오가는 메커니즘 자체를 기술하는 명세가 필요합니다.

명세

Interoperable CSS(ICSS)는 표준 CSS의 상위 집합으로, 두 개의 의사 선택자를 추가로 사용합니다.

:import("path/to/dep.css") {
  localAlias: keyFromDep;
  /* ... */
}
:export {
  exportedKey: exportedValue;
  /* ... */
}

:export

:export 블록은 소비자(consumer)에게 내보낼 심볼을 정의합니다. 기능적으로는 다음 JS 코드와 같다고 생각하면 됩니다.

module.exports = {
  "exportedKey": "exportedValue"
}

:export 문법에는 다음 제약이 있습니다.

  • 반드시 최상위 레벨에 있어야 하지만, 파일 안의 어느 위치든 상관없습니다.
  • 파일에 두 개 이상 있으면 키와 값이 합쳐져 함께 내보내집니다.
  • 특정 exportedKey가 중복되면 소스 순서상 마지막 것이 우선합니다.
  • exportedValue에는 CSS 선언 값으로 유효한 모든 문자가 들어갈 수 있습니다(공백 포함).
  • exportedValue는 따옴표로 감쌀 필요가 없습니다. 이미 리터럴 문자열로 취급됩니다.

다음은 결과물의 가독성을 위해 권장되지만, 강제되지는 않습니다.

  • :export 블록은 하나만 두는 편이 좋습니다.
  • 파일 상단에, 단 :import 블록들보다는 아래에 두는 편이 좋습니다.

:import

:import 구문은 다른 CSS 파일에서 변수를 가져옵니다. 다음 순서로 동작합니다.

  • 의존성을 가져와 처리합니다.
  • 의존성이 내보낸 값을 가져올 토큰과 대응시켜 localAlias에 연결합니다.
  • 현재 파일 안의 특정 위치(아래 참고)에서 localAlias가 쓰인 곳을 찾아, 의존성의 exportedValue로 치환합니다.

CSS 파일 안에서 localAlias를 찾는 위치는 다음과 같습니다.

  • 모든 선언 값 예: border: 1px solid localAlias;
  • 모든 선택자 예: .localAlias .MyComponent {}
  • 미디어 쿼리 인자 예: @media screen and localAlias

덕분에 한 파일에서 무엇을 가져와 어떻게 쓸지에 상당한 유연성이 생깁니다. 대신 치환 과정에서 엉뚱한 곳이 바뀌지 않도록, 로컬 별칭은 충분히 구별되는 이름이어야 합니다. 적용되는 제약은 다음과 같습니다.

  • 반드시 최상위 레벨에 있어야 합니다.
  • 한 파일에서 여러 의존성으로부터, 여러 심볼을 가져올 수 있습니다.
  • 로컬 별칭은 영숫자, 밑줄, 하이픈만으로 이루어진 하나의 항이어야 합니다. 또한 충돌을 피하기 위해 파일 안에서 유일해야 합니다.

다음은 가독성을 위해 권장되지만, 강제되지는 않습니다.

  • 의존성 하나당 import 하나를 두는 편이 좋습니다.
  • 모든 import는 파일 상단에 두는 편이 좋습니다.
  • 로컬 별칭은 이중 밑줄(__)로 시작하게 하는 편이 좋습니다.