테마 전환

Tailwind 성능 최적화: JIT, content 설정 및 프로덕션 용량 관리

Easton editorial illustration: signal tracing instrument

Chrome DevTools를 열자 3.5MB짜리 CSS 파일이 눈에 들어왔습니다.

새로 출시한 기능 페이지의 로딩 시간이 800ms에서 3.2초로 늘어났습니다. 여러 부분을 살펴본 끝에 원인이 바로 이 CSS 파일이라는 사실을 알아냈습니다. 실제로는 전혀 사용하지 않는 Tailwind 클래스가 파일 안을 가득 채우고 있었습니다.

Tailwind는 ‘성능 친화적’이라고 알려졌는데, 어째서 오히려 발목을 잡은 걸까요?

나중에야 문제는 Tailwind 자체가 아니라 설정에 있다는 사실을 알았습니다. JIT 모드의 작동 원리를 이해하고 content 설정을 정확하게 조정한 다음 프로덕션 빌드에 몇 단계의 최적화를 더하면, CSS 파일을 MB 단위에서 KB 단위까지 줄일 수 있습니다. Netflix 웹사이트는 심지어 6.5KB만 사용했습니다.

오늘은 제가 직접 겪었던 시행착오를 이야기해 보겠습니다.


1. JIT 모드: Tailwind의 성능 혁신

1.1 기존 모드의 문제

그날 밤 3.5MB짜리 CSS 파일을 발견하기 전까지, 저는 Tailwind를 그저 ‘유틸리티 우선 CSS 프레임워크’ 정도로만 이해하고 있었습니다.

나중에 알게 된 사실은 Tailwind v2 이전의 ‘기존 모드’가 가능한 모든 클래스 이름 조합을 미리 생성한다는 것이었습니다. 모든 색상, 모든 간격, 모든 변형(hover, focus, disabled 등)이 여기에 포함됩니다. 중간 정도 복잡도의 프로젝트만 되어도 개발 환경의 CSS 파일은 10MB 이상으로 커질 수 있습니다.

10MB+
기존 모드의 개발 CSS 용량

로컬 개발에서는 10MB짜리 CSS 파일도 별문제 없어 보입니다. 대역폭이 문제가 되지 않기 때문입니다. 하지만 브라우저는 이렇게 큰 스타일시트를 파싱해야 하고, 그 결과 메모리 사용량과 DevTools 성능 모두 영향을 받습니다.

예전에 Firefox에서 디버깅할 때 바로 이런 일을 겪었습니다. 클래스 이름 하나를 바꾸면 DevTools가 몇 초 동안 멈췄고, 페이지 새로고침도 한없이 느리게 느껴졌습니다.

더 골치 아픈 점은 기존 모드에서는 여러 설정을 선뜻 바꾸기 어렵다는 것입니다. 예를 들어 새 브레이크포인트를 추가하거나 focus-visible 변형을 활성화하려면 먼저 ‘클래스 이름 조합이 얼마나 더 늘어날까?‘부터 계산해야 합니다. 결국 팀은 성능과 유연성 사이에서 선택을 강요받았습니다.

1.2 JIT는 실제로 어떻게 작동할까

JIT(Just-in-Time) 모드는 Tailwind v2.1에서 도입되었으며 v3+부터 기본으로 활성화됩니다. 간단히 말하면 필요할 때만 생성하는 방식입니다.

기존 모드는 빌드할 때 실제 사용 여부와 관계없이 가능한 모든 클래스 이름 조합을 미리 생성합니다. JIT 모드는 반대로 템플릿 파일(HTML, JSX, Vue 등)을 먼저 스캔하고 실제로 사용한 클래스 이름을 찾아 해당 스타일만 생성합니다.

예를 들어 보겠습니다. 기존 모드에서 Tailwind는 다음과 비슷한 CSS를 생성합니다.

.bg-black { background-color: #000 }
.hover\:bg-black:hover { background-color: #000 }
.focus\:bg-black:focus { background-color: #000 }
.disabled\:bg-black:disabled { background-color: #000 }
/* ... 그 밖에도 수십 가지 변형 조합 */

프로젝트에서 bg-black 하나만 사용하더라도 수십 가지 다른 변형 스타일까지 생성됩니다.

JIT 모드는 다릅니다. 템플릿을 스캔해 bg-blackhover:bg-black만 사용했다는 사실을 확인하면 다음 두 규칙만 생성합니다.

.bg-black { background-color: #000 }
.hover\:bg-black:hover { background-color: #000 }

덕분에 개발 환경의 CSS 파일은 10MB대에서 곧바로 KB대로 줄어들며, 프로덕션 환경과 마찬가지로 작은 용량을 유지합니다.

1.3 JIT의 실질적인 이점

프로젝트에서 JIT 모드를 처음 활성화했을 때가 기억납니다. 페이지 새로고침이 너무 빨라서 현실감이 없을 정도였습니다. 전에는 몇 초를 기다려야 했지만 이제는 거의 즉시 완료되었습니다.

빌드 속도: 이전에는 전체 빌드에 2~3분이 걸렸지만 이제 몇 초면 끝납니다. JIT는 모든 스타일을 미리 생성할 필요 없이 템플릿 파일을 스캔해 클래스 이름만 추출하면 되기 때문입니다.

개발 경험: DevTools가 더 이상 버벅이지 않았습니다. 이전에는 클래스 이름 하나를 수정해도 브라우저가 10MB짜리 스타일시트를 다시 파싱할 때까지 기다려야 했습니다. 지금은 스타일시트가 몇 KB에 불과하므로 변경 사항이 거의 즉시 반영됩니다.

임의 값 지원: 예상하지 못했던 장점입니다. 기존 모드에서 text-[#facc15] 같은 임의 값을 사용하려면 설정에서 먼저 safelist를 활성화해야 했습니다. JIT 모드는 곧바로 지원합니다.

// 설정에 미리 정의하지 않고 바로 사용
<h1 class="text-[2.5rem] mt-[1.35rem] text-[#facc15]">
  JIT 덕분에 모든 것이 간단해집니다
</h1>

동적 클래스 이름: 기존 모드에서는 'text-' + color처럼 조합한 클래스 이름을 감지할 수 없습니다. JIT 모드에도 제한은 있지만, safelist를 함께 사용하면 동적인 상황을 더 유연하게 처리할 수 있습니다.

여기까지 읽었다면 JIT 모드를 어떻게 활성화하는지 궁금할 것입니다. 사실 현재 Tailwind v3+에서는 JIT가 기본값이라 별도 설정이 필요 없습니다. 여전히 v2를 사용 중이라면 다음과 같이 활성화할 수 있습니다.

// tailwind.config.js
module.exports = {
  mode: 'jit',  // v2에서는 직접 활성화해야 함
  content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
  // ...
}

2. content 설정: 정밀 스캔의 핵심

JIT 모드는 유용하지만 content 설정이 정확해야 합니다.

content는 Tailwind가 클래스 이름을 추출하기 위해 어떤 파일을 스캔할지 결정합니다. 설정이 잘못되면 스캔 범위가 너무 좁아 스타일이 누락되거나, 반대로 너무 넓어 CSS 용량이 커집니다.

2.1 content 설정 기초

기본 문법은 매우 간단합니다.

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{html,js,jsx,ts,tsx}',  // src 디렉터리 아래의 모든 템플릿 파일 스캔
  ],
  // ...
}

여기서 **는 모든 깊이의 디렉터리를 의미하고, *.{html,js,jsx,ts,tsx}는 해당 확장자를 가진 파일을 매칭한다는 뜻입니다.

특정 프레임워크를 사용한다면 경로를 조정해야 할 수 있습니다.

// Next.js 프로젝트
content: [
  './pages/**/*.{js,ts,jsx,tsx}',
  './components/**/*.{js,ts,jsx,tsx}',
  './app/**/*.{js,ts,jsx,tsx}',  // App Router
]

// Astro 프로젝트
content: [
  './src/**/*.{astro,html,js,jsx,ts,tsx}',
]

핵심은 Tailwind 클래스 이름을 사용하는 모든 파일을 포함하는 것입니다. 디렉터리 하나라도 빠뜨리면 그 안에서 사용하는 스타일은 생성되지 않습니다.

2.2 제가 겪었던 문제

문제 1: 범위가 지나치게 넓은 glob 패턴

당시 저는 설정을 ‘간단하게’ 만들려고 다음과 같이 작성했습니다.

content: [
  './**/*.js',  // node_modules까지 스캔합니다!
]

그 결과 Tailwind가 node_modules에 있는 모든 JS 파일을 스캔했고, 빌드 시간은 급격히 늘었으며 정체를 알 수 없는 스타일도 잔뜩 생성되었습니다.

올바른 방법은 범위를 제한하는 것입니다.

content: [
  './src/**/*.js',      // src 디렉터리만 스캔
  './components/**/*.js', // components 디렉터리만 스캔
]

문제 2: 컴포넌트 디렉터리 누락

한 번은 프로젝트를 리팩터링하면서 컴포넌트를 ./lib/components/ 디렉터리로 옮겼습니다. content 설정을 업데이트하는 것을 깜빡한 탓에 새 위치의 컴포넌트 스타일이 모두 사라졌습니다.

한참을 조사한 뒤에야 원인을 찾았습니다. 여기서 얻은 교훈은 프로젝트 구조를 바꿀 때 반드시 content 설정도 함께 업데이트해야 한다는 것입니다.

문제 3: 동적으로 구성한 클래스 이름

다음과 같은 코드가 그 예입니다.

const color = 'red';
const className = `text-$&#123;color&#125;-500`;  // JIT가 감지할 수 없음

JIT가 스캔할 때 보이는 것은 완전한 클래스 이름이 아니라 템플릿 문자열입니다. 그 결과 프로덕션 빌드에서 이 스타일이 삭제되었습니다.

해결 방법은 safelist(뒤에서 설명합니다)를 사용하거나 객체 문법으로 바꾸는 것입니다.

const colors = {
  red: 'text-red-500',
  blue: 'text-blue-500',
};
const className = colors[color];  // 완전한 클래스 이름이므로 감지 가능

2.3 safelist와 동적 클래스 이름

동적 클래스 이름이 꼭 필요한 상황에서는 safelist를 활용할 수 있습니다.

// tailwind.config.js
module.exports = {
  safelist: [
    'text-red-500',
    'text-blue-500',
    'bg-red-500',
    // 또는 정규식으로 클래스 이름 그룹 매칭
    {
      pattern: /text-(red|blue|green)-(500|600)/,
      variants: ['hover', 'focus'],  // 변형도 함께 유지
    },
  ],
}

safelist를 사용하면 템플릿 파일에 직접 나타나지 않더라도 Tailwind가 해당 스타일을 강제로 생성합니다.

다만 safelist가 많을수록 CSS 파일도 커진다는 점에 주의해야 합니다. 꼭 필요한 상황에서만 사용하고, 사용할지도 모르는 모든 클래스 이름을 ‘보험’처럼 넣어 두지 마세요.


3. 프로덕션 용량 관리: 4단계 최적화 전략

JIT 모드를 사용하면 개발 환경에서 작은 CSS를 만들 수 있지만, 프로덕션 빌드에서는 추가 최적화가 필요합니다.

저는 설정부터 압축까지 단계적으로 적용하는 4단계 최적화 전략을 정리했습니다.

1단계
정밀한 content 설정
실제 사용하는 파일만 스캔
2단계
PurgeCSS 제거
사용하지 않는 스타일 자동 삭제
3단계
cssnano Minify
CSS 압축 최적화
4단계
Brotli/Gzip
네트워크 전송 압축
Source: 최적화 전략 단계

3.1 1단계: 정밀한 content 설정

앞에서 이미 설명한 가장 기본적인 단계입니다.

핵심 원칙은 실제로 Tailwind 클래스 이름을 사용하는 파일만 스캔하는 것입니다. 범위가 정확할수록 빌드는 빨라지고 CSS는 작아집니다.

// 좋은 예: 정확한 범위
content: [
  './src/components/**/*.jsx',
  './src/pages/**/*.tsx',
]

// 나쁜 예: 지나치게 넓은 범위
content: [
  './**/*.js',  // node_modules까지 스캔
]

3.2 2단계: PurgeCSS 자동 제거

Tailwind v3+는 프로덕션 빌드에서 PurgeCSS를 자동으로 활성화해 사용하지 않는 스타일을 제거합니다.

핵심은 빌드 명령이 개발 환경과 프로덕션 환경을 정확히 구분하도록 하는 것입니다.

# 개발 빌드(사용하지 않는 스타일을 제거하지 않음)
npm run dev

# 프로덕션 빌드(PurgeCSS 자동 실행)
npm run build

PostCSS를 사용한다면 설정에서 명시적으로 제어할 수 있습니다.

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
  },
}

3.3 3단계: cssnano Minify

PurgeCSS가 사용하지 않는 스타일을 제거한 뒤에는 cssnano가 CSS를 한 번 더 압축합니다.

이 과정에는 주석 제거, 중복 규칙 병합, 선택자 단순화, 숫자 값 압축 등이 포함됩니다.

# Tailwind CLI에서 바로 minify
npx tailwindcss -i ./src/input.css -o ./dist/output.css --minify

# 또는 PostCSS 사용(위에서 이미 설명)

실제 사례를 들면 제 프로젝트에서는 PurgeCSS 처리 후 150KB였던 파일이 minify 후 45KB로 줄었습니다.

3.4 4단계: Brotli/Gzip 네트워크 압축

이 단계는 CSS 자체가 아니라 네트워크 전송 계층을 최적화합니다.

서버에서 Brotli 또는 Gzip으로 정적 리소스를 압축하면 CSS 파일을 다시 60~80% 줄일 수 있습니다.

# Nginx Brotli 설정(ngx_brotli 모듈 설치 필요)
brotli on;
brotli_comp_level 6;
brotli_types text/css application/javascript;

# 또는 Gzip 사용(Nginx 기본 지원)
gzip on;
gzip_comp_level 6;
gzip_types text/css application/javascript;
6.5KB
Netflix CSS 전송 용량

수치를 비교하면 45KB짜리 CSS 파일은 Brotli 압축 후 전송 용량이 약 8KB로 줄어듭니다.

따라서 Netflix가 CSS 용량이 6.5KB라고 말할 때는 원본 파일 크기가 아니라 Brotli 압축 후의 전송 용량을 의미합니다.


4. 실전 사례와 데이터 비교

4.1 최적화 전후 비교

3.5MB → 28KB
개발 빌드 CSS
JIT 모드 활성화 전후
320KB → 48KB
프로덕션 빌드 CSS
압축 전 용량
7.2KB
최종 전송 용량
Brotli 압축 후
3.2s → 820ms
Lighthouse LCP
페이지 로딩 성능
Source: 실측 데이터

솔직히 이 비교 결과를 보면서 저도 꽤 놀랐습니다.

4.2 자주 발생하는 문제 해결

스타일이 사라졌다면 어떻게 해야 할까요?

첫째, content 설정을 확인해 Tailwind 클래스 이름을 사용하는 모든 파일이 스캔 범위에 포함되었는지 확인합니다.

둘째, 동적 클래스 이름이 있다면 safelist가 필요한지 확인합니다.

셋째, 빌드 명령을 확인해 개발 빌드가 아닌 프로덕션 빌드(npm run build)를 사용했는지 확인합니다.

CSS가 여전히 크다면 어떻게 해야 할까요?

safelist가 너무 많지 않은지 확인합니다. 서드파티 라이브러리의 CSS가 Tailwind 빌드 결과에 섞였는지도 확인합니다. content 범위가 너무 넓지 않은지도 살펴봅니다.

DevTools가 버벅인다면 어떻게 해야 할까요?

Tailwind 버전이 3 이상인지 확인합니다(JIT가 기본으로 활성화됩니다). content 설정이 정확한지도 확인합니다. 여전히 v2를 사용 중이라면 업그레이드하거나 JIT 모드를 직접 활성화하세요.


5. Tailwind v4의 새로운 기능 전망

기존 최적화를 살펴봤으니 이제 Tailwind v4(2024년 말 출시)의 새로운 변화를 알아보겠습니다.

5.1 Oxide 엔진

가장 기대되는 업데이트는 Tailwind가 기반 엔진을 Rust로 다시 작성했다는 점입니다.

182x
증분 빌드 속도 향상

공식 데이터에 따르면 증분 빌드 속도가 182배 향상되었습니다. 쉽게 말해 이전에는 클래스 이름 하나를 바꾼 뒤 재컴파일에 몇 초를 기다려야 했지만, 이제는 거의 밀리초 단위로 응답합니다.

Oxide 엔진이 파일 스캔 결과를 캐시하고 변경된 부분만 다시 처리하는 것이 그 원리입니다. 대규모 프로젝트에서 특히 유용합니다. 저희 프로젝트 중 하나는 컴포넌트 파일이 200개 이상인데, 예전에는 빌드할 때마다 약 10초를 기다려야 했지만 지금은 몇 초면 끝납니다.

5.2 무설정이라는 목표

Tailwind v4는 새로운 방향을 추구합니다. 대부분의 프로젝트에서 더 이상 tailwind.config.js가 필요하지 않다는 것입니다.

기본 설정만으로도 최신 CSS 기능, 컨테이너 쿼리, 합리적인 브레이크포인트, 완전한 색상 시스템 등 일반적인 요구 사항을 충족합니다. 깊은 수준의 사용자 지정이 필요할 때만 설정 파일을 작성하면 됩니다.

따라서 새 프로젝트를 더 빠르게 시작할 수 있고 설정 오류가 발생할 가능성도 줄어듭니다.

다만 v4로 마이그레이션할 때는 설정 문법이 달라졌다는 점에 주의해야 합니다. 예를 들어 기존의 theme.extend.colors는 이제 CSS 사용자 지정 속성으로 작성해야 합니다. 마이그레이션 전에 공식 업그레이드 가이드를 읽어 보는 것이 좋습니다.


마무리

새벽 세 시에 겪었던 그 사건 이후, 저는 프로젝트의 Tailwind 설정을 처음부터 다시 정리했습니다.

JIT 모드는 개발 환경과 프로덕션 환경 모두에서 작은 CSS를 사용할 수 있게 하고, content 설정은 스캔 범위를 결정하며, 4단계 최적화는 CSS 용량을 MB에서 KB로 줄입니다. 여기에 Tailwind v4의 Oxide 엔진까지 더해지면 빌드 속도는 더 이상 병목이 아닙니다.

프로젝트가 여전히 기존 모드를 사용 중이거나 content 설정이 적절한지 확신이 없다면 먼저 다음 항목을 확인해 보세요.

  1. Tailwind 버전이 3 이상인가(JIT 기본 활성화)
  2. content 설정이 모든 템플릿 파일을 정확히 포함하는가
  3. 프로덕션 빌드에서 PurgeCSS와 cssnano가 활성화되어 있는가
  4. 서버에 Brotli/Gzip 압축이 설정되어 있는가

이 항목들을 수정하면 대부분 눈에 띄는 성능 향상을 확인할 수 있습니다.

궁금한 점이 있다면 언제든 댓글로 남겨 주세요. Tailwind 공식 문서의 JIT와 프로덕션 최적화 섹션도 설명이 잘 되어 있으니 함께 읽어 보세요.



참고 자료

Tailwind CSS 성능 최적화 설정

JIT 모드 활성화부터 프로덕션 용량 관리까지 이어지는 전체 설정 과정

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Tailwind 버전 확인

    프로젝트의 Tailwind CSS 버전을 확인합니다.

    • Tailwind v3+에서는 JIT 모드가 기본으로 활성화됩니다.
    • Tailwind v2에서는 설정에 mode: 'jit'를 직접 추가해야 합니다.
    • 최상의 성능을 위해 v3+로 업그레이드하는 것이 좋습니다.
  2. 2

    Step 2: content 스캔 경로 설정

    tailwind.config.js에 정확한 스캔 경로를 설정합니다.

    • Next.js: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}']
    • Astro: ['./src/**/*.{astro,html,js,jsx,ts,tsx}']
    • './**/*.js'처럼 범위가 지나치게 넓은 glob 패턴은 피합니다.
  3. 3

    Step 3: 동적 클래스 이름 처리

    동적으로 구성하는 클래스 이름은 safelist를 사용해 강제로 생성합니다.

    • safelist 배열에 완전한 클래스 이름을 추가합니다.
    • pattern 정규식으로 클래스 이름 그룹을 매칭합니다.
    • variants를 함께 사용해 hover, focus 등의 변형을 유지합니다.
  4. 4

    Step 4: 프로덕션 빌드 최적화 설정

    PostCSS 설정에 cssnano를 추가합니다.

    • 개발 환경에서는 cssnano를 활성화하지 않아 빌드 속도를 높입니다.
    • 프로덕션 환경에서는 cssnano minify를 자동으로 활성화합니다.
    • process.env.NODE_ENV로 활성화 조건을 제어합니다.
  5. 5

    Step 5: 서버 압축 설정

    Nginx에 Brotli 또는 Gzip 압축을 설정합니다.

    • Brotli: brotli on; brotli_comp_level 6;
    • Gzip: gzip on; gzip_comp_level 6;
    • 두 방식 모두 text/css application/javascript 유형을 설정합니다.

FAQ

Tailwind v3에서 JIT 모드를 어떻게 활성화하나요?
Tailwind v3+에서는 JIT 모드가 기본으로 활성화되므로 별도 설정이 필요 없습니다. Tailwind v2를 계속 사용 중이라면 tailwind.config.js에 mode: 'jit'를 추가해야 합니다.
content 설정에서 파일을 누락하면 어떤 문제가 생기나요?
누락한 파일에 사용된 Tailwind 클래스 스타일이 생성되지 않아 프로덕션 빌드 후 스타일이 사라집니다. 프로젝트 구조를 바꿀 때는 반드시 content 설정도 함께 업데이트해야 합니다.
JIT가 동적 클래스 이름을 놓치는 이유는 무엇인가요?
JIT는 소스 파일의 텍스트를 스캔합니다. `text-$&#123;color&#125;-500`처럼 동적으로 조합한 클래스 이름은 소스 코드에서 완전한 클래스 이름이 아닌 템플릿 문자열이므로 감지할 수 없습니다. safelist를 사용하거나 객체 매핑 문법으로 바꿔야 합니다.
Netflix의 6.5KB CSS는 원본 용량인가요, 압축 후 용량인가요?
6.5KB는 Brotli로 압축한 후의 네트워크 전송 용량입니다. 원본 CSS 파일은 PurgeCSS와 cssnano 처리 후 수십 KB 정도이며, Brotli/Gzip으로 압축하면 전송 용량이 크게 줄어듭니다.
Tailwind v4의 Oxide 엔진은 무엇이 개선되었나요?
Rust로 다시 작성된 Oxide 엔진은 증분 빌드 속도를 182배 향상합니다. 파일 스캔 결과를 캐시하고 변경된 부분만 다시 처리하는 방식으로, 대규모 프로젝트의 빌드 시간을 10초대에서 몇 초대로 줄입니다.
safelist를 너무 많이 설정하면 어떤 영향이 있나요?
safelist는 지정한 스타일을 강제로 생성하므로 너무 많이 설정하면 CSS 파일 용량이 커집니다. 꼭 필요한 동적 클래스 이름에만 사용하고, 사용할 가능성이 있는 모든 클래스 이름을 추가하지 마세요.

2분 읽기 · 게시일: 2026년 3월 30일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog