컴포넌트를 새 자리로 옮기기로 정했다. 그런데 그 CSS 는 따라오지 않는다.
안 A 클래스 이름에 접두사를 붙여 겹치지 않게 한다
안 B 도구가 이름을 유니크하게 만들게 한다
컴포넌트 자리 표준이 컴포넌트를 셋으로 나눠 각자 자리를 줬는데, 스타일은 전부 styles/pages/*.css 한 곳에 남아 있다. 컴포넌트만 옮기면 그 컴포넌트의 CSS 가 어디 있는지 아무도 모르게 된다.
그리고 재보니 옮기기 전에 이미 깨져 있었다.
먼저: 같은 클래스가 다른 값으로 다섯 파일에 있다
이건 원칙이 아니라 이 표준을 급하게 만든 사실이다.
styles/ 전체에서 클래스 이름을 파일 단위로 세어 봤다.
.page-container 5개 파일 faq · inquiries · inquiry-detail · inquiry-new · legal-doc
.page-head 4개 파일 faq · inquiries · inquiry-new · reviews
.search-box 2개 파일 faq · inquiries
.not-found 2개 파일 inquiry-detail · review-detail
값이 전부 다르다.
/* faq.css */ .page-container { max-width: 760px }
/* inquiries.css */ .page-container { max-width: 760px }
/* inquiry-new.css */ .page-container { max-width: 640px }
/* inquiry-detail.css */ .page-container { max-width: 100%; padding: 24px 20px 120px }
/* legal-doc.css */ .page-container { max-width: 100%; padding: 24px 20px 80px }
CSS 규칙은 선택자 { 속성: 값 } 모양이다. .page-container { max-width: 760px } 는 「page-container 클래스가 붙은 요소는 가로 폭을 최대 760px 로 한다」는 뜻이다. 다섯 줄 모두 선택자(.page-container)는 같은데 중괄호 안 값이 760px · 640px · 100% 로 서로 다르다 — 같은 이름인데 파일마다 다른 규칙을 적어 둔 것이다.
그리고 app/layout.js 가 페이지 CSS 11개를 전부 import 한다. import '파일경로' 는 다른 파일에 적힌 내용을 지금 이 파일로 가져와 합친다는 뜻이다. CSS 파일을 import 하면 그 안의 모든 규칙이 지금 화면에 함께 적용된다 — app/layout.js 가 11개를 전부 import 했으니 11개 파일의 CSS 규칙이 전부 한 화면에서 동시에 살아 있는 것이다. 그중 둘 이상이 같은 선택자(.page-container)를 쓰면 브라우저는 “이름이 같은 규칙 중 하나만 적용한다”는 규칙으로 고르는데, 그 기준이 바로 특이도다. 전부 단일 클래스라 특이도가 같으므로 마지막에 로드된 것이 이긴다.
13 faq.css 760px 을 원한다
16 inquiries.css 760px 을 원한다
17 inquiry-new.css 640px 을 원한다
19 legal-doc.css 100% ← import 순서상 마지막. 이것이 이긴다
/faq 의 여백이 layout.js 의 import 줄 순서에 달려 있다. 그리고 .page-container 를 고치려면 다섯 파일 중 어디를 고쳐야 하는지 알 방법이 없다.
다만 실제 화면은 확인하지 못했다. CSS 규칙상 위와 같이 되지만, 미디어쿼리 안에 재정의된 것도 있어 최종 결과는 브라우저로 봐야 확실하다.
원칙 1. 이름 충돌은 규칙이 아니라 도구로 막는다
같은 이름이 겹치는 것을 어떻게 막는가.
안 A — 클래스 이름에 접두사를 붙인다. .faq-page-container · .inquiries-page-container 처럼 페이지 이름을 앞에 둔다. 파일을 하나도 안 옮겨도 된다는 것이 큰 이점이다.
버린 이유는 사람이 기억해야 지켜진다는 것이다. 새 페이지를 만들 때 접두사를 까먹으면 다시 충돌하고, 까먹었다는 걸 아무도 안 알려준다. 지금 660줄에 접두사가 없는 것도 규칙이 없어서가 아니라 규칙이 있었어도 같은 일이 벌어졌을 것이기 때문이다.
안 B — 페이지 CSS 를 layout.js 에서 빼고 각 페이지가 import 한다. 다른 페이지의 CSS 가 안 들어온다. 버린 이유는 충돌을 없애지 않는다는 것이다. 전역 CSS 는 한 번 로드되면 남고, 라우팅으로 페이지를 오가면 결국 여러 개가 쌓인다. 어느 경로로 들어왔느냐에 따라 화면이 달라진다 — 지금보다 알아보기 어려운 실패다.
골랐다 — 안 C, CSS Modules. Next.js 문서가 이 문제를 그대로 지목한다.
CSS Modules locally scope CSS by generating unique class names. This allows you to use the same class in different files without worrying about naming collisions.
번역하면 「CSS Modules 는 고유한 클래스 이름을 만들어 CSS 의 스코프를 그 파일 안으로 좁힌다. 그래서 다른 파일에서 같은 클래스 이름을 써도 이름이 겹칠 걱정이 없다」는 뜻이다.
안 A 와 안 B 가 못 지켜주는 것은 이름 충돌이 구조적으로 불가능해지는 것이다. 도구가 클래스 이름을 유니크하게 바꾸므로 사람이 기억할 규칙이 없다. .page-container 를 다섯 파일이 각자 써도 된다.
대가는 JSX 를 함께 고쳐야 하는 것이다. className="review-card" 가 className={styles.reviewCard} 가 된다. styles 는 import styles from './ReviewCard.module.css' 처럼 그 컴포넌트의 CSS Modules 파일을 불러와 담은 객체다. CSS Modules 가 .reviewCard 같은 클래스 이름을 고유한 이름(예: ReviewCard_reviewCard__a1b2c)으로 바꾸면서, 원래 이름으로 그 값을 꺼내 쓸 수 있게 styles.reviewCard 라는 자리를 만들어 준다. JSX 에서 {} 는 「이 자리에 자바스크립트 값을 그대로 넣어라」는 뜻이라, className={styles.reviewCard} 는 styles 객체에서 꺼낸 그 고유한 클래스 이름을 className 에 넣으라는 뜻이 된다. 파일 수만큼 손이 간다.
원칙 2. CSS 는 그것을 쓰는 파일 옆에 둔다
파일을 어디에 두는가. 컴포넌트 자리 표준이 정한 세 자리를 그대로 따른다.
아래 파일 이름에 붙은 .module.css 는 원칙 1에서 고른 CSS Modules 를 쓰겠다는 표시다. Next.js 는 파일 이름이 .module.css 로 끝나는 것만 CSS Modules 로 다루고, 그래야 import styles from './Header.module.css' 처럼 불러와 styles.객체 로 쓸 수 있다. 그냥 Header.css 로 지으면 CSS Modules 가 아니라 전역 CSS 로 처리된다.
components/layout/
Header.js
Header.module.css ← 옆에
app/faq/
page.js
page.module.css ← 옆에
_components/FaqList.js
_components/FaqList.module.css ← 옆에
components/
ReviewCard.js
ReviewCard.module.css ← 옆에
옆에 두는 이유는 컴포넌트를 옮길 때 CSS 가 따라오게 하려는 것이다. 지금은 컴포넌트가 components/ 에 있고 스타일이 styles/pages/home.css 에 있어서, 컴포넌트를 옮기면 그 스타일이 어디 있는지 찾아야 한다. 옆에 있으면 폴더째 옮기면 끝난다.
그리고 「이 스타일 아직 쓰나」에 답할 수 있게 된다. 지금은 styles/pages/home.css 의 어떤 규칙이 아직 쓰이는지 알려면 전체를 뒤져야 한다. 파일이 컴포넌트 옆에 있으면 그 컴포넌트를 지울 때 CSS 도 같이 지운다.
대가는 CSS 가 여러 곳으로 흩어지는 것이다. 「전체 스타일을 한눈에」 볼 수 없게 된다. 그래도 그쪽을 고른 이유는 「이 클래스 어디서 정의했지」를 찾는 일이 「전체를 훑는」 일보다 훨씬 잦기 때문이다 — 컴포넌트 자리 표준에서 페이지 전용을 흩은 것과 같은 판단이다.
원칙 3. 전역에 남기는 것은 셋뿐이다
전부 CSS Modules 로 가지는 않는다. 스코프가 필요 없는 것이 있다.
| 전역에 남긴다 | 왜 |
|---|---|
토큰 — :root 의 CSS 변수 |
어디서든 보여야 한다. 스코프를 씌우면 그 파일 안에서만 보인다 |
리셋 — * { box-sizing } 같은 것 |
요소 전체에 거는 것이라 클래스가 없다 |
요소 선택자 — body · a · h1 |
문서에 하나뿐이라 겹칠 수가 없다 |
:root 는 문서 전체를 가리키는 선택자다. 거기에 정의한 CSS 변수는 다른 CSS 파일 어디서든 var(--색상-이름) 식으로 꺼내 쓸 수 있다. 반면 CSS Modules 로 스코프를 씌우면 그 변수는 그 파일을 쓰는 컴포넌트 안에서만 보이게 되어, 다른 컴포넌트에서는 꺼내 쓸 수 없다 — 그래서 토큰은 전역에 남긴다. * { box-sizing: border-box } 같은 리셋 규칙은 *(모든 요소)에 거는 것이라 애초에 클래스 이름이 없고, box-sizing 은 요소의 가로·세로 크기를 잴 때 테두리와 안쪽 여백(padding)을 포함할지 정하는 CSS 속성이다. 요소 선택자(body·a·h1)도 문서 전체에서 그 태그를 쓰는 요소에 전부 적용되는 것이라 클래스가 없기는 마찬가지다.
그 밖의 클래스는 전부 CSS Modules 로 간다.
// app/layout.js — 전역만 import 한다
import '@/styles/tokens/colors.css';
import '@/styles/tokens/spacing.css';
...
import '@/styles/global.css'; // 리셋 · 요소 선택자
@/ 는 프로젝트 최상위 폴더를 가리키도록 미리 정해 둔 경로 표시다. 폴더가 몇 겹 깊이 있든 ../../../styles/... 처럼 상위 폴더를 세어 올라가지 않고 @/styles/... 한 줄로 같은 파일을 가리킬 수 있다.
layout.js 가 페이지 CSS 를 import 하지 않는다. 그 한 줄이 이 문제의 시작이었다 — 11개를 전부 불러서 서로 덮어쓰게 만들었다.
판별은 한 줄이다 — 클래스 선택자인가. 클래스면 Modules, 아니면 전역이다. 판단이 필요 없다.
판단 기준 정리
| 질문 | 답 | 결론 |
|---|---|---|
| 이름 충돌을 어떻게 막나 | 도구가 막는다 | 접두사 규칙은 사람이 기억해야 하고 까먹어도 안 알려준다 |
| 어디에 두나 | 쓰는 파일 옆에 | 컴포넌트를 옮기면 CSS 가 따라온다 |
| 전역에 남기는 것 | 토큰 · 리셋 · 요소 선택자 | 스코프가 필요 없거나 씌우면 안 되는 것들 |
| 판별 기준 | 클래스 선택자인가 | 클래스면 Modules, 아니면 전역 |
layout.js 는 무엇을 import 하나 |
전역만 | 페이지 CSS 를 부르지 않는다 |
| 전체 스타일을 한눈에 못 보게 된다 | 감수한다 | 「이 클래스 어디 있지」가 「전체 훑기」보다 훨씬 잦다 |
이 표준을 정하기까지
컴포넌트 자리를 정하고 나서 딸려 나온 문제였다. 컴포넌트를 _components/ 로 옮기기로 했는데 스타일이 styles/pages/*.css 에 남아 있어 따라오지 않는다. 옮기는 순간 그 컴포넌트의 CSS 가 어디 있는지 모르게 된다.
그래서 CSS 를 재봤더니 옮기기 전에 이미 깨져 있었다. 클래스 이름을 파일 단위로 세니 .page-container 가 다섯 파일에 서로 다른 값으로 있었다. 처음엔 미디어쿼리 안팎을 잘못 센 줄 알고 다시 셌는데, 파일 단위로도 다섯이었다.
「같은 값이 중복된 것」과 「다른 값이 충돌하는 것」은 급한 정도가 다르다. 앞서 푸터 때와 같은 이유로 값을 하나씩 열어봤고, 760px · 640px · 100% 로 전부 달랐다. layout.js 의 import 순서상 마지막인 legal-doc.css 의 100% 가 이긴다. /faq 는 760px 를 원했는데 안 되고 있다.
접두사 규칙이 제일 싸 보였다. 파일을 하나도 안 옮겨도 되니까. 그런데 그건 이 저장소가 계속 버려온 모양이다 — 사람이 기억해야 지켜지는 규칙. 새 페이지에서 접두사를 까먹으면 다시 충돌하고 아무도 안 알려준다. 660줄에 접두사가 없는 것도 규칙이 없어서가 아니라, 규칙이 있었어도 같은 일이 벌어졌을 것이기 때문이다.
Next.js 문서를 열어보니 이 문제를 그대로 지목하고 있었다 — “the same class in different files without worrying about naming collisions”. 그리고 CSS Modules 파일을 컴포넌트 옆에 두는 예시(app/blog/blog.module.css)가 방금 정한 컴포넌트 자리와 정확히 맞물렸다. 자리를 따로 정할 필요가 없었다.
전부 Modules 로 보내려다 셋을 남겼다. :root 의 CSS 변수에 스코프를 씌우면 그 파일 안에서만 보여서 토큰이 죽는다. 리셋과 요소 선택자는 애초에 클래스가 아니라 스코프를 씌울 대상이 없다. 그래서 판별을 「클래스 선택자인가」 한 줄로 정했다 — 판단이 필요 없는 기준이라야 지켜진다.
정리
- 이름 충돌은 규칙이 아니라 도구로 막는다. 접두사는 사람이 기억해야 하고 까먹어도 아무도 안 알려준다.
- CSS 는 그것을 쓰는 파일 옆에 둔다. 컴포넌트를 옮기면 CSS 가 따라오고, 지울 때 같이 지운다.
- 전역에 남기는 것은 토큰 · 리셋 · 요소 선택자 셋뿐이다.
:root변수에 스코프를 씌우면 토큰이 죽는다. - 판별은 「클래스 선택자인가」 한 줄이다. 판단이 필요 없는 기준이라야 지켜진다.
layout.js는 전역만 import 한다. 페이지 CSS 11개를 부르던 그 줄이 문제의 시작이었다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기