금요일 밤 10시, npx @tailwindcss/upgrade를 돌렸다. "대부분의 프로젝트는 1~2시간이면 끝난다"는 공식 문서의 말을 믿었다. 3시간 반 뒤에도 나는 opacity 유틸리티를 수작업으로 고치고 있었다.
설정 파일이 통째로 사라졌다
이번 메이저 업데이트의 가장 큰 변화는 철학 자체의 전환이다. JavaScript 설정 파일이 사라지고, CSS 안에서 모든 커스터마이징을 처리하는 "CSS-first configuration"이 기본이 됐다.
v3까지는 이런 식이었다:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: '#4f46e5',
},
fontFamily: {
display: ['Pretendard', 'sans-serif'],
},
},
},
}
이제는 CSS 파일 하나로 끝난다:
@import "tailwindcss";
@theme {
--color-brand: #4f46e5;
--font-display: 'Pretendard', sans-serif;
}
솔직히 이쪽이 깔끔하긴 하다. JS 파일을 하나 없앤 것뿐 아니라, 런타임에서 테마 전환이 가능해졌다는 점도 매력적이다. 다크모드 토글 같은 기능을 CSS 변수만으로 처리할 수 있게 된 거다. 문제는 이 전환 과정에서 터지는 것들이다.
코드모드가 못 잡는 20%
@tailwindcss/upgrade는 꽤 괜찮은 자동화 도구다. 의존성 교체, 설정 파일의 CSS 변환, 템플릿 클래스명 업데이트까지 처리해준다. 공식적으로 80% 자동화를 표방하는데, 나머지 20%가 프로젝트마다 다른 지점에서 폭발한다.
opacity 유틸리티 전멸. bg-indigo-600 bg-opacity-75가 bg-indigo-600/75로 바뀌어야 한다. 그런데 코드모드가 이걸 건드리지 않았다. text-opacity-*, border-opacity-*, placeholder-opacity-* 전부 마찬가지. 프로젝트 전체를 정규식으로 긁어서 하나씩 바꿨다. 이 작업만 45분.
JS 파일 안에 숨어 있는 클래스. 업그레이드 도구는 템플릿 파일(.html, .jsx, .vue)만 스캔한다. clsx나 cva로 조건부 클래스를 조합하는 .ts 파일, Storybook의 .stories.tsx는 레이더에 안 잡힌다. 동적 스타일링 코드가 많은 프로젝트일수록 수동 작업이 기하급수적으로 늘어난다.
그래디언트 네이밍 전면 교체. bg-gradient-to-r이 bg-linear-to-r로 바뀌었다. CSS 표준의 linear-gradient 네이밍을 따르게 된 건데, 이건 코드모드가 잡아주긴 한다. 다만 서드파티 라이브러리 내부에 하드코딩된 클래스는 손 못 대니까 직접 확인해야 한다.
shadcn 쓰는 팀은 각오가 필요하다
아마 가장 고통스러운 케이스가 shadcn/ui 사용자일 거다. 이 라이브러리는 컴포넌트를 설치할 때 설정 파일을 참조한다. 새 버전에서 해당 파일이 없으면? 설치 자체가 뻗는다.
shadcn 팀에서 호환 가이드를 내놓긴 했다. 하지만 기존 프로젝트를 업그레이드하면서 동시에 UI 컴포넌트도 전환해야 하는 상황은 생각보다 복잡하다. CSS 변수 네이밍 규칙 자체가 달라졌고, @layer base에 정의하던 테마 토큰을 @theme 블록으로 옮기는 작업이 필요하다. 컴포넌트가 20개 넘는다면 두 마이그레이션을 별도 PR로 나누는 걸 강력히 권한다. 한꺼번에 하다가 뭐가 깨진 건지 추적 불가능해진다.
지금 올려야 하나
새 프로젝트라면 무조건 v4다. CSS-first 설정이 직관적이고, @tailwindcss/postcss가 기존 PostCSS 플러그인을 대체하면서 빌드 체인이 한결 단순해졌다. Oxide 엔진 기반이라 빌드 속도 차이도 체감된다.
기존 프로젝트는 상황에 따라 갈린다. 순정 유틸리티만 쓰는 곳은 반나절이면 끝낼 수 있다. 반면 addUtilities나 addComponents로 자체 플러그인을 만들어 쓰던 팀은 @plugin 디렉티브로 전부 재작성해야 한다. corePlugins 옵션으로 특정 유틸리티를 비활성화하고 있었다면, 그 옵션 자체가 사라졌으니 대안을 찾아야 한다.
조용히 치명적인 변경 하나 더 — border 유틸리티의 기본 색상이 gray-200에서 currentColor로 바뀌었다. 이걸 모르고 배포하면 보더가 갑자기 까맣게 변한 화면을 만나게 된다. QA에서 잡히면 다행이고, 프로덕션에서 고객이 먼저 발견하면 그게 바로 금요일 밤 배포의 비극이다.
급한 사람을 위한 체크리스트
npx @tailwindcss/upgrade먼저 실행*-opacity-*패턴을 프로젝트 전체 검색 → 슬래시 표기법으로 교체.ts,.tsx,.stories.*파일의 동적 클래스 수동 점검border기본색 변경 확인 — 필요하면border-gray-200명시 추가서드파티 UI 라이브러리 호환성 별도 확인
커스텀 플러그인은
@plugin으로 재작성
완벽한 자동 마이그레이션 같은 건 없다. 결국 코드모드를 돌리고 git diff를 한 줄씩 읽는 게 가장 확실한 방법이다. 3시간 반 걸린 게 억울하냐고? 수동으로 했으면 이틀은 걸렸을 거다.