Skip to content

CSS Modules의 변환 과정 살펴보기

CSS Modules를 사용하면 CSS 파일에 있는 클래스 이름을 직접 사용할 수 있습니다.

import styles from './button.module.css';

button.className = styles.button;

이후 브라우저에서 클래스 이름을 적용한 요소를 살펴보면 CSS 파일에 작성했던 .button과는 다른 이름이 붙어 있는 것을 확인할 수 있습니다. CSS Modules가 빌드 과정에서 클래스 이름을 변경한 것입니다. CSS Modules는 CSS를 지역적으로 사용할 수 있게 하고 전역 범위 문제를 해결하기 위해 이렇게 빌드 과정에서 클래스 이름을 임의로 변경합니다.

그렇다면 이러한 변경은 어떻게 이루어지는 걸까요? 어떤 도구가 CSS의 클래스 이름을 바꾸고styles.button의 이름과 변경된 이름을 연결하는 걸까요? 예제를 통해 실제 변환 과정을 함께 살펴보며 CSS Modules의 변환 과정을 이해해보겠습니다.

이 글에서는 PostCSS 플러그인을 사용하여 CSS Modules의 변환 과정을 살펴봅니다. 사용하는 빌드 도구에 따라 내부 구현과 출력 형태는 달라질 수 있습니다.

CSS Modules의 변환

먼저 버튼에 적용할 공통 스타일을 작성하겠습니다.

/* base.module.css */

.base {
  padding: 8px 16px;
  border-radius: 4px;
}

그리고 작성한 공통 스타일 활용한 버튼 스타일을 작성하겠습니다.

/* button.module.css */

@value accent: #0066ff;

.button {
  composes: base from './base.module.css';
  color: accent;
}

@value는 값에 이름을 붙이는 문법입니다. 여기서는 #0066ff를 accent로 참조합니다.

composes는 함께 사용할 클래스를 지정하는 문법으로 여기서는 버튼에 base.module.css의 base 클래스를 가져와 적용합니다.

이제 빌드 도구를 통해 변환 과정을 거치면 다음의 CSS를 얻을 수 있습니다.

이름이 바뀌는 과정을 쉽게 이해할 수 있도록 바뀐 이름은 파일명__클래스명으로 설정하겠습니다. 이는 예제를 위한 설정이기 때문에 실제로 붙는 이름은 구현에 따라 달라질 수 있습니다.

.base__base {
  padding: 8px 16px;
  border-radius: 4px;
}
.button__button {
  color: #0066ff;
}

결과적으로 styles.button을 요소에 지정하면 두 가지 클래스가 붙습니다.

<button class="button__button base__base">확인</button>

기존에 작성한 CSS 코드와 비교해보면 accent는 실제 색상값으로 바뀌었고 composes 선언은 사라지고 공통 스타일은 별도의 클래스로 포함됐습니다. 그리고 styles.button은 두 클래스 이름을 담은 문자열로 변경됐습니다.

전체적인 흐름을 살펴보았으니 이제 변환 과정을 하나씩 자세히 살펴보겠습니다.

4개의 플러그인을 거치며 달라지는 CSS

PostCSS는 CSS를 파싱하고 원하는 작업을 수행하여 CSS를 변환할 수 있게 해줍니다. 그리고 이러한 변환 작업은 특정 플러그인을 만들어 수행할 수 있습니다.

이번 예제에서는 다음 4개의 플러그인을 순서대로 적용합니다.

플러그인주요 역할
postcss-modules-values@value로 정의하거나 가져오는 값을 처리합니다. 값의 참조를 치환하고 가져오기와 내보내기 정보를 생성합니다.
postcss-modules-local-by-default기본적으로 클래스, ID와 애니메이션 이름을 지역 범위로 처리하고 :local과 :global에 따라 범위를 구분합니다.
postcss-modules-extract-importscomposes의 외부 파일 참조를 추출해 가져오기 정보를 만들고 가져올 클래스를 지역 별칭으로 참조하도록 바꿉니다.
postcss-modules-scope지역 범위의 이름을 변환합니다. 원래 이름과 변환된 이름의 대응 관계를 기록하고 클래스 조합 결과를 내보낼 값에 반영합니다.

이제 앞에서 작성한 CSS에 플러그인을 하나씩 적용해보며 실제 코드가 어떻게 변경되는지 살펴보겠습니다.

1. 이름을 붙인 값을 실제 값으로 바꾸기

먼저 postcss-modules-values를 이용하여 @value를 처리합니다. button.module.css에 이 플러그인을 적용하면 다음과 같이 변경됩니다.

/* button.module.css */

:export {
  accent: #0066ff;
}
.button {
  composes: base from './base.module.css';
  color: #0066ff;
}

@value accent: #0066ff 선언이 사라지고 color에 적었던 accent가 #0066ff로 바뀌었습니다.

위쪽에는 :export라는 블록이 생겼습니다. 이 블록은 파일 밖에서 사용할 값에 대한 정보를 담습니다. accent: #0066ff는 이 파일을 사용하는 쪽에 accent라는 이름으로 #0066ff를 내보낸다는 뜻입니다. 이후 빌드 단계에서 이 정보를 읽어 외부에 전달할 값을 얻습니다.

2. 이름을 바꿀 클래스의 범위 표시하기

다음으로 postcss-modules-local-by-default를 이용하여 클래스 선택자의 범위를 처리합니다.

/* button.module.css */

:export {
  accent: #0066ff;
}
:local(.button) {
  composes: base from './base.module.css';
  color: #0066ff;
}

.button에 :local(...)이 추가됐습니다. 이 클래스가 현재 파일의 지역 범위에 속한다는 표시입니다.

CSS Modules에서 클래스 이름을 파일마다 구분할 수 있는 것은 뒤에서 이러한 처리가 실행되기 때문입니다. 반대로 :global(...)로 작성한 선택자는 전역 범위로 처리됩니다.

3. 다른 파일에서 가져올 클래스 기록하기

세 번째로 postcss-modules-extract-imports를 이용하여 composes에 적힌 외부 파일 참조를 처리합니다.

/* button.module.css */

:import("./base.module.css") {
  i__imported_base_0: base;
}
:export {
  accent: #0066ff;
}
:local(.button) {
  composes: i__imported_base_0;
  color: #0066ff;
}

composes: base from './base.module.css'에 담겨 있던 정보가 두 곳으로 나뉘었습니다. 새로 생긴 :import 블록에는 어디에서 무엇을 가져올지 적혀 있고 composes에는 가져올 값을 가리키는 별칭인 i__imported_base_0이 들어갑니다.

i__imported_base_0: base는 “base.module.css가 내보내는 base를 현재 파일에서는 i__imported_base_0이라는 이름으로 참조한다”는 뜻입니다. 왼쪽은 현재 파일에서 사용할 별칭이고 오른쪽은 상대 파일이 내보내는 이름입니다.

4. 클래스 이름을 바꾸고 내보낼 값 연결하기

마지막으로 postcss-modules-scope가 지역 클래스 이름을 변환합니다. 이 예제에서는 앞서 정한 이름 생성 규칙에 따라 .button을 .button__button으로 바꿉니다.

/* button.module.css */

:import("./base.module.css") {
  i__imported_base_0: base;
}
:export {
  accent: #0066ff;
}
.button__button {
  color: #0066ff;
}
:export {
  button: button__button i__imported_base_0;
}

:local(.button)이 .button__button으로 바뀌었습니다. 아래쪽에는 :export 블록이 하나 더 생겼습니다. 이 블록의 button은 파일 밖에서 참조할 이름이고 그 값에는 새 클래스 이름인 button__button이 들어 있습니다. 이 관계를 통해 사용하는 쪽은 원래 이름인 button으로 바뀐 클래스 이름을 알아낼 수 있습니다.

여기에 composes가 가리키던 i__imported_base_0도 포함됩니다. 이를 통해 버튼에 두 클래스를 모두 적용할 수 있습니다. 이 정보가 옮겨지면서 CSS 규칙 안의 composes 선언은 사라졌습니다.

4가지 변환 단계를 거쳐 CSS 규칙과 :import, :export 블록이 남았습니다. 이렇게 CSS 안에 모듈이 가져올 값과 내보낼 값을 표현하는 형식을 ICSS(Interoperable CSS) 라고 부릅니다.

4가지 플러그인을 통해 변환 과정을 수행했지만 아직 모든 작업이 끝난 것은 아닙니다. button 값에는 여전히 i__imported_base_0이라는 별칭이 남아 있으며 ICSS 또한 파일에 남아있기 때문입니다.

이제 마지막 과정을 거쳐 최종 결과를 얻어보겠습니다.

최종 결과 만들기

플러그인을 통해 가져오기(:import)와 내보내기(:export) 정보를 기록했다면 이후에는 별도의 빌드 도구를 사용하여 이 정보를 읽고 의존성을 연결하여 외부에서 사용할 값을 만드는 과정이 필요합니다. 이 처리를 담당하는 코드는 사용하는 도구에 따라 달라집니다.

이번 예제에서는 앞의 플러그인들과 의존 파일 처리까지 포함하는 postcss-modules를 통해 변환 과정을 살펴보겠습니다. 내부의 파서(Parser)가 :import와 :export를 읽어 파일 사이의 값을 연결하고 내보낼 값을 수집한 뒤 처리가 끝난 블록을 CSS에서 제거합니다.

이 과정을 아래의 3가지로 나누어 살펴보겠습니다.

1. 가져올 값을 찾아 별칭과 연결하기

:import가 가리키는 base.module.css를 같은 방식으로 변환하면 다음 결과를 얻습니다.

/* base.module.css */

.base__base {
  padding: 8px 16px;
  border-radius: 4px;
}
:export {
  base: base__base;
}

의존 파일을 처리하는 과정에서도 :export를 읽습니다. 이 파일에서 base라는 이름으로 내보내는 값은 base__base입니다. 이 값을 꺼낸 뒤 :export 블록은 제거하고, .base__base의 CSS 규칙은 최종 결과에 포함할 수 있도록 남겨둡니다.

버튼 파일의 :import에는 가져올 값에 대해 다음과 같이 적혀 있었습니다.

/* button.module.css */

:import("./base.module.css") {
  i__imported_base_0: base;
}

/* ... */

이 정보를 읽으면 현재 파일의 별칭인 i__imported_base_0과 의존 파일이 내보낸 값인 base__base를 연결할 수 있습니다. 파서는 이 대응 관계를 저장한 뒤 :import 블록을 제거합니다.

base.module.css가 내보내는 base 
 > base__base

button.module.css의 i__imported_base_0 
 > base__base

2. 별칭을 치환하고 내보낼 값 꺼내기

이제 저장한 대응 관계를 바탕으로 버튼 파일에 남은 별칭을 실제 값으로 바꿉니다. :export 안의 i__imported_base_0을 base__base로 치환하면 버튼 파일은 다음 모습이 됩니다.

/* button.module.css */

:export {
  accent: #0066ff;
}
.button__button {
  color: #0066ff;
}
:export {
  button: button__button base__base;
}

이 시점에는 :import가 사라졌고 내보낼 button 값에도 실제 클래스 이름 두 개가 들어 있습니다.

다음으로 파서는 두 :export 블록을 읽어 이름과 값을 하나의 내보내기 객체에 모읍니다. 첫 번째 블록에서 accent와 #0066ff를, 두 번째 블록에서 button과 button__button base__base를 꺼냅니다. 값을 기록한 뒤에는 각 :export 블록도 CSS에서 제거합니다.

따라서 버튼 파일 자체에는 다음 CSS 규칙이 남습니다.

/* button.module.css */

.button__button {
  color: #0066ff;
}

:import와 :export에 담겨 있던 정보는 이 과정에서 의존성을 연결하고 내보내기 객체를 만드는 데 사용됐습니다. 필요한 정보를 모두 꺼냈으므로 최종 CSS에는 두 블록이 모두 제거됩니다.

3. 최종 CSS와 내보내기 객체 확인하기

postcss-modules는 의존 파일에서 얻은 CSS도 결과에 포함합니다. base.module.css에서 남겨둔 .base__base 규칙과 버튼 파일의 .button__button 규칙이 모여 처음에 보았던 최종 CSS가 됩니다.

.base__base {
  padding: 8px 16px;
  border-radius: 4px;
}
.button__button {
  color: #0066ff;
}

버튼 파일의 :export에서 꺼낸 값은 별도로 다음 내보내기 객체에 담깁니다.

{
  "accent": "#0066ff",
  "button": "button__button base__base"
}

이렇게 스타일을 적용할 CSS와 외부에서 사용할 값이 각각 만들어졌습니다. 의존 파일이 내보낸 base는 버튼의 클래스 조합을 해결하는 데 쓰였고 버튼 파일이 내보내는 이름은 accent와 button입니다.

다시 styles.button으로

처음에 작성했던 코드를 다시 보겠습니다.

import styles from './button.module.css';

button.className = styles.button;

CSS를 styles 객체로 가져오는 사용 방식에서 styles.button에 해당하는 값은 "button__button base__base"입니다. 이 문자열을 요소에 지정하면 .button__button의 스타일과 .base__base의 스타일이 함께 적용됩니다.

여기서 composes가 어떻게 적용됐는지 알 수 있었습니다. 최종 CSS에서 공통 스타일은 .base__base에 남아 있고 내보낼 클래스 이름에 base__base가 추가됐습니다. 함께 적용할 클래스 이름을 하나의 문자열로 전달해 스타일을 조합한 것입니다.

빌드 도구는 이렇게 얻은 값을 JavaScript에서 가져올 수 있도록 모듈로 만들고 CSS는 브라우저에 전달할 수 있도록 처리합니다. 모듈의 내보내기 형식이나 CSS를 별도 파일로 추출할지는 도구와 설정에 따라 달라집니다. 위 JSON은 단순히 처리에 사용할 값을 보여줍니다.

CSS의 .button과 JavaScript의 styles.button이 연결되는 과정을 따라가보며 이름을 바꾸는 작업과 관계를 전달하는 작업을 이해할 수 있었습니다. CSS Modules에서 변환은 지역 범위를 표시한 뒤 클래스 이름을 바꾸고 바뀐 이름과 의존성을 기록한 다음 각 파일을 연결했습니다. 이 과정에서 ICSS가 무엇을 의미하는지 또 어떻게 사용되는지 또한 이해할 수 있었습니다.