- 발행일
Tailwind v3에서 v4로 170개 파일을 옮겼다 — 설정 파일을 지우는 게 핵심이었다
Tailwind v3에서 v4로 170개 파일을 옮겼다
이 블로그에는 같은 전환을 다룬 글이 먼저 있습니다. 그 글은 시점과 경로가 실제 이력과 달라 정정을 붙여 뒀고, 이 글이 실제로 무슨 일이 있었는지의 기록입니다.
tailwind.config.js는 2024년 11월에 만들어져 그대로 있었습니다. 버전은 v3.4.16이었고요. 전환은 한 커밋에 끝냈습니다 — 170개 파일, 2809줄 추가 2662줄 삭제.
숫자만 보면 대공사 같은데, 실제로 손이 많이 간 건 클래스 이름을 옮기는 기계적인 작업이었고 판단이 필요했던 건 몇 군데 안 됩니다. 그 몇 군데가 전부 "v3에서는 조용히 잘 되던 것이 v4에서는 조용히 안 되는" 자리였어요.
설정을 어디에 둘 것인가
v4의 핵심 변화는 토큰이 JS 설정 파일이 아니라 CSS 안에 산다는 겁니다. 옮기는 방법은 여러 갈래가 있어요.
| 방법 | 장점 | 포기하는 것 | 판단 |
|---|---|---|---|
① @config로 v3 설정을 계속 읽게 하고 엔진만 올림 | 1차 변경이 작고 시각적 회귀가 거의 없음 | 토큰 정의가 CSS와 JS 두 곳에 공존하는 기간이 생김. "나중에 걷어낸다"의 나중이 안 옴 | 기각 |
② @theme로 옮기되 tailwind.config.js는 남겨 둠 | 되돌리기 쉬움 | 같은 문제 — 어느 쪽이 진짜인지 읽는 사람이 모름 | 기각 |
③ @theme 단일 원천 + tailwind.config.js 삭제 | 원천이 하나. 색을 찾을 때 볼 파일이 하나 | 한 커밋이 커지고, 회귀가 나면 범위가 넓음 | 채택 |
| ④ Tailwind 토큰을 안 쓰고 CSS 변수만 사용 | 프레임워크 의존 최소 | 유틸리티 클래스를 못 씀. 이 프로젝트 마크업 전체가 그 위에 서 있음 | 불가 |
①이 교과서적인 답이고 실제로 v4가 그러라고 브리지를 제공합니다. 그런데 **호환 모드로 단계를 쪼개는 전략의 마지막 단계는 "호환 모드 제거"**인데, 그 단계는 급한 일이 아니라서 계속 밀립니다. 이 프로젝트는 이미 네임스페이스 설정을 두 곳에 두고 데인 적이 있어서, 공존 기간을 아예 안 만드는 쪽으로 갔습니다.
결과적으로 static/css/theme.css 하나가 원천이 됐습니다.
@theme {
/* 기본 팔레트·브레이크포인트를 비운다 — 팔레트에 없는 색(blue-500 등)과
쓰지 않는 브레이크포인트(sm·md·lg)가 되살아나지 않게 하는 장치다. */
--color-*: initial;
--breakpoint-*: initial;
--color-primary: #006FF1;
--color-sub1: #2DC5F4;
...
}
--color-*: initial은 v3 시절 theme.colors를 통째로 재정의해 기본 팔레트를 지우던 것과 같은 효과를 냅니다. 이게 없으면 bg-blue-500이 되살아나요.
safelist가 조용히 사라진다
content는 @source로 그대로 옮기면 되는데, safelist는 v4에 대응하는 설정이 없습니다. v4는 config의 safelist를 더 이상 읽지 않습니다.
/* 스캔 경로 — 예전 tailwind.config.js 의 content 를 옮겼다 */
@source "../../templates/**/*.html.j2";
@source "../../static/js/**/*.js";
/* v3 의 safelist 자리. 두 클래스는 서버가 값으로 골라 붙여 마크업에 문자열이 없어
스캐너에 안 잡힌다. v4 는 config 의 safelist 를 더 이상 읽지 않는다. */
@source inline("bg-section-header bg-api");
bg-section-header와 bg-api는 뷰에서 값에 따라 골라 붙이는 클래스라 템플릿 어디에도 문자열로 안 나옵니다. v3에서는 safelist가 지켜 주고 있었어요. 그 줄을 안 옮겼으면 이 두 색은 CSS가 생성되지 않아 그냥 안 칠해졌을 겁니다. 빌드는 통과하고, 화면은 뜨고, 색만 없어요.
옮기다가 발견한 게 아니라 삭제할 설정 파일을 한 줄씩 읽다가 발견했습니다. 자동 변환 도구를 돌렸다면 못 봤을 자리예요.
클래스 이름은 같은데 @apply 대상이 아니게 된다
body가 @apply scrollbar로 가져다 쓰는 공용 클래스가 있었습니다. v3에서는 @layer components에 정의돼 있었고 잘 동작했어요. v4에서는 스타일이 안 붙습니다.
/* scrollbar: body 가 @apply 로 가져다 쓴다. v4 는 @layer components 에 둔 클래스를
@apply 대상으로 인정하지 않아 @utility 로 올려야 한다(시험 시스템도 같은 형태). */
@utility scrollbar {
@apply overflow-y-auto overflow-x-hidden rounded-md max-xl:overflow-x-auto;
&[web-admin] { ... }
&::-webkit-scrollbar { ... }
}
@utility로 승격하면서 .scrollbar::-webkit-scrollbar 같은 선택자들도 &로 중첩하게 정리했습니다. 덤으로 Firefox 대응 한 줄이 걸렸는데,
/* Firefox 는 scrollbar-color 를 @apply 로 못 받아 값을 직접 쓴다 — gray-400 과 같은 값 */
scrollbar-color: oklch(0.871 0 0) transparent;
토큰을 참조하지 못하고 값을 박아야 하는 자리라, 나중에 gray-400이 바뀌면 여기가 안 따라옵니다. 주석으로만 묶어 뒀어요.
마크업에 둔 기본 상태가 컴포넌트 규칙을 이긴다
제일 헷갈렸던 회귀입니다. 상단 메뉴에서 활성 메뉴 강조가 안 먹었습니다.
구조는 이랬어요. 링크의 평상시 글자색은 템플릿에 text-white로 붙어 있고, 활성 상태는 CSS의 .active 규칙이 다른 색을 줍니다. v3에서는 잘 됐습니다.
v4는 캐스케이드 레이어 우선순위가 더 엄격해서, 마크업의 유틸리티 클래스가 @layer components의 규칙을 이깁니다. 평상시 색이 활성 색을 덮어써요. 명시도가 아니라 레이어의 문제라 .active를 아무리 구체적으로 써도 안 됩니다.
/* 평상시 글자색. 마크업에 두면 utilities 레이어라 아래 .active 규칙(components)을
이겨버려 활성 메뉴가 강조되지 않는다. */
.nav-main-link {
@apply text-white;
}
고치는 방향은 기본 상태를 마크업에서 빼서 같은 레이어로 내리는 것입니다. 기본과 활성이 같은 층에 있어야 순서대로 겨루니까요. 이런 자리가 5건 있었고, 전부 "상태에 따라 값이 갈리는데 한쪽만 마크업에 있는" 모양이었습니다.
없던 브레이크포인트가 살아나자 8개 화면이 다열이 됐다
--breakpoint-*: initial로 기본 브레이크포인트도 지웠는데, 지우고 나서 템플릿을 훑다가 이상한 걸 봤습니다. md:grid-cols-2 같은 클래스가 이미 여기저기 쓰여 있어요.
/* sm·md·lg 는 Tailwind 기본값 그대로다. 템플릿 8곳이 이미 이 이름으로 다열 그리드를
짜두었는데 브레이크포인트가 없어 1열로만 나오던 것을 살렸다.
2xl 은 쓰는 곳이 없어 두지 않는다 — 없는 이름은 조용히 무시되니 쓰기 전에 여기 추가할 것. */
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
v3 설정에도 sm·md·lg가 없었습니다. 그 클래스들은 처음부터 아무 일도 안 하고 있었어요. 화면 8개가 넓은 창에서도 1열로 서 있었고, 아무도 버그로 신고하지 않았습니다. 1열도 그럭저럭 읽히니까요.
이 글에서 두 번째로 나오는 같은 모양입니다. 없는 이름을 쓰면 Tailwind는 항의하지 않고 무시합니다. 색에서 한 번, 브레이크포인트에서 한 번 같은 방식으로 당했어요.
나흘 뒤에 터진 지뢰 둘
전환 커밋에서는 안 보이다가 나흘 뒤에 하나씩 나온 게 있습니다. 둘 다 v4가 기본값을 바꾼 자리였어요.
첫째, 연한 회색 구분선 자리에 검은 선이 그어졌습니다. border-b처럼 색 없이 두께만 준 클래스들인데, **v4의 기본 테두리색은 gray-200이 아니라 currentColor**입니다. 글자색을 따라가니까 검은 글자 옆의 테두리는 검게 나와요. 모달 헤더 구분선 3곳, 설문 섹션 카드, 이중 다중선택 상자에서 border-gray-300을 명시해 고쳤습니다. 폼과 버튼은 input.css가 원소 단위로 색을 주고 있어 무사했고요.
둘째, showModal()로 띄운 대화상자가 화면 가운데가 아니라 좌상단에 붙었습니다.
/* preflight 의 `* { margin: 0 }` 가 UA 의 `margin: auto` 를 덮어써서
showModal() 한 dialog 가 화면 좌상단에 붙는다. 중앙 정렬 복구. */
dialog:modal {
@apply m-auto;
}
<dialog>의 중앙 정렬은 CSS가 아니라 브라우저 기본 스타일시트의 dialog:modal { margin: auto }가 해 주고 있었습니다. preflight의 * { margin: 0 }이 그걸 덮으면 자동 정렬이 사라져요. 개별 대화상자에 m-auto를 덧붙이는 대신 base 레이어에서 한 번 복구했습니다 — 이 프로젝트는 CSS 번들이 둘이라 양쪽에 넣어야 하고, 한쪽만 고치면 다른 쪽 대화상자는 계속 좌상단에 붙습니다.
찾아보니 public_legend.css에 .pa-public-legend-dialog { margin: auto }가 이미 있었습니다. 누군가 이 버그를 먼저 만나 그 대화상자만 국소 패치해 둔 흔적이에요. 한 곳에서 고칠 문제를 만난 자리에서 고치면 이렇게 남습니다. 값이 같아 무해해서 지우지 않고 뒀습니다.
검증
- 공개 12개·관리 5개 화면에서 가로 넘침 0 확인. 브레이크포인트를 새로 살렸기 때문에 좁은 폭에서 깨지는지가 제일 위험했습니다.
- e2e 실패 목록이 전환 전과 동일한 것 확인. 새로 깨진 게 없다는 뜻이고, 원래 빨간 것들은 그대로 빨갛습니다.
- 트리 아이콘 색 단언 1건은 갱신했습니다 — v4가
sky-600을 P3 기준 oklch로 재조정해 값이 바뀌었어요. 대비 4.02:1을 확인하고 색은 유지, 단언만 고쳤습니다. bg-section-header·bg-api가 빌드 산출물에 들어 있는지 직접 확인.@source inline()이 실제로 동작하는지 봐야 했습니다.- 상단 메뉴 활성 강조를 화면에서 확인. 회귀 5건은 전부 눈으로 봐야 나오는 종류라 스펙으로는 못 잡았습니다.
scripts/run.sh·deploy.sh포함 CLI 호출부 7곳 전부@tailwindcss/cli로 교체 후 dev·배포 양쪽 빌드 성공 확인.
남은 것 · 한계
- 배포 스크립트가 node 버전에 걸릴 수 있습니다. v4 CLI는 node 20+를 요구하는데 배포는 conda env의 node로 돕니다. 이번엔 통과했지만 서버 node가 낮은 환경에서는 이 단계에서 죽어요.
deploy.sh에 경고와 우회 경로(로컬 빌드 후 아카이브 동봉)를 주석으로만 적어 뒀고, 스크립트가 버전을 검사하지는 않습니다. - 타입 클래스 1152곳을 한 칸 옮긴 건 자동 치환입니다. 136개 파일에서 크기는 그대로 두고 이름만 시험 시스템 자리로 맞췄는데, 행간이 1.50에서 1.43으로 바뀌었습니다. 화면 전체를 눈으로 대조하지는 않았어요.
@utility로 승격한scrollbar의 Firefox 값이 토큰과 끊겨 있습니다.gray-400이 바뀌면 스크롤바만 옛 색으로 남습니다. 주석 말고는 막을 장치가 없습니다.- 캐스케이드 레이어 회귀 5건은 눈으로 찾았습니다. "기본 상태가 마크업에 있고 변화 상태가 CSS에 있는" 조합을 기계적으로 훑는 방법을 못 찾아서, 화면을 돌아다니며 이상한 걸 봤어요. 못 본 게 더 있을 가능성이 높습니다.
2xl은 일부러 안 넣었습니다. 지금 쓰는 곳이 없어서인데, 이 결정이 이 글의 "없는 이름은 조용히 무시된다" 함정을 그대로 재생산합니다. 누군가2xl:을 쓰면 아무 일도 안 일어나고 아무도 모릅니다. 팔레트 가드는 색만 보고 브레이크포인트는 안 봐요.- 전환을 한 커밋에 담아서, 회귀가 났을 때 어느 변경 때문인지 이분 탐색이 안 됩니다. 원천을 둘로 두지 않으려고 고른 대가입니다.
관련 글: 팔레트에 없는 색은 조용히 안 칠해진다 · Tailwind v3에서 v4 CSS-first @theme로 전환하면서 깨지고 다시 메운 기록