'런타임 비용을 줄여보자!' 며 패기있게 시작한 스타일링 이원화 전략은 생각보다 더 다뤄야할 요소들이 깊었습니다..

스타일링 이원화 전략 정리 글 ( ▶이동 )

먼저 스타일의 우선순위 선정이 필요했습니다.

이번 글은 스타일링 우선순위 선정을 주제로 아래 목차 순으로 진행해보겠습니다~

목차

1. 문제 상황 ( ▶이동 )

2. CSS Cascade Layer 개념 ( ▶이동 )

3. Tailwind layer 개념 ( ▶이동 )

4. 해결 전략 : Emotion Unlayered 설정 ( ▶이동 )

5. 환경별 세팅 ( ▶이동 )

6. 활용하기 위해 알아야 하는 개념들 ( ▶이동 )

0. Intro

Tailwind CSS(유틸리티 클래스)와 Emotion(CSS-in-JS)를 함께 사용하면 아래와 같은 장점들을 가져갈 수 있다.

  • Tailwind : 빠른 레이아웃 구현, 간격, 반응형 처리
  • Emotion : 컴포넌트 단위 스타일 캡슐화, 디자인 토큰 기반 스타일링, 상태 기반 동적 스타일링

하지만, 두 라이브러리를 함께 사용할 때 CSS 우선 순위 충돌 문제가 발생할 수 있다.
발생 원인과 CSS Cascade Layer를 활용한 해결 방법을 정리해본다.


1. 왜 스타일 충돌이 발생할까?

1-1. CSS 기본 우선 순위 규칙

CSS는 아래와 같은 세 가지 기준으로 우선 순위를 정한다.

  1. Specificity : 선택자 구체성(#id > .class > tag)
  2. Source Order : 나중에 선언된 스타일이 우선
  3. !important : 모든 규칙을 무시하고 적용

1-2. Tailwind와 Emotion 충돌 지점

먼저 각각 생성 스타일을 살펴보겠다.

Emotion 생성 스타일

.css-abc123 { padding: 16px }

Tailwind 생성 스타일

.p-4 { padding: 1rem }

두 스타일 모드 클래스 선택자(.로 시작) 1개이므로 Specificity가 동일하다.
이 경우 Source Order(선언 순서)로 우선순위가 결정된다.

문제는 이 경우에 발생한다.
빌드 환경 A : Tailwind가 나중에 삽입 → Tailwind 승
빌드 환경 B : Emotion이 나중에 삽입 → Emotion 승

동일한 출력물도 빌드 환경, 번들러 설정, 청크 분할 방식에 따라 CSS 삽입 순서가 달라지므로 결과를 예측할 수 없다.

참고

  • 빌드 환경 : React, Next.js
  • 번들러 설정 : Vite, Webpack, Turbopack
  • 청크 분할이란? : 하나의 큰 번들을 여러 개의 작은 파일(chunk)로 나누는 것

2. CSS Cascade Layer란?

2-1. @layer 개념

CSS Cascading and Inheritance Level 5 스펙에서 도입된 우선순위 그룹화 메커니즘이다.

@layer base, components, utilities;

@layer base {
	/* 가장 낮은 우선 순위 */
}

@layer utilities {
	/* 가장 높은 우선 순위 */
}

2-2. @layer 핵심 규칙

1. 선언 순서 === 우선 순위

@layer A, B, C에서 C가 우선 순위가 가장 높다. → Source Order

2. Layer > Specificity

상위 layer의 .class가 하위 layer의 #id를 이김 (#id > .class > tag )

3. Unlayered > Layered

@layer 밖의 스타일이 모든 layer보다 우선

2-3. 시각화

정리(핵심)

Layer를 사용하면 CSS 삽입 순서와 무관하게 우선순위가 고정된다!


3. Tailwind v4의 Layer 구조

3-1. Tailwind 기본 layer 구조

layer는 다음과 같이 4개의 layer로 구성된다.

  • theme : CSS 변수, 테마 토큰
  • base : Preflight(브라우저 초기화)
  • components : @apply 기반 컴포넌트 스타일
  • utilities : 유틸리티 클래스(p-4, mt-2 등)

3-2. Preflight란?

Tailwind에 내장된 브라우저 초기화 스타일이다.
modern-normalize를 기반으로 하며, base layer에 포함된다.

주요 초기화 내용

  • margin/padding 제거 : 모든 요소의 기본 여백 초기화
  • border 리셋 : border: 0 solid;로 통일
  • heading 스타일 제거 : h1~h6의 font-size, font-weight 상속
  • list 스타일 제거 : ul, ol의 bullet/number 제거
  • img block 처리 : 이미지를 display:block으로 설정

초기화 내용이 좀 익숙하였다.

그렇다. reset.css와 중복된다.

3-3. Tailwind v4 import 문법

Tailwind v4에서는 간결한 import 문법을 사용한다

@layer theme, base, components, utilities;

@import 'tailwindcss';

이 한 줄로 theme, preflight, utilities가 모두 포함된다.

만약 개별 import가 필요한 경우라면 아래와 같이 진행한다.

@layer theme, base, components, utilities

@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/preflight.css' layer(base);
@import 'tailwindcss/utilities.css' layer(utilities)

4. 해결 전략 : Emotion Unlayered 설정

4-1. 전략 선택 이유

Emotion에서 @layer를 직접 지원하는 방법을 검토했다.(with 클로드)

  1. stylis 플러그인 커스텀 : Emotion 출력을 @layer로 감싸기 → 실용성 낮음. 불안정
  2. CSS 후처리 : 런타임에서 style 태그 수정 → 실용성 낮음. 성능 이슈(런타임 비용 증가)
  3. Unlayered로 유지 : Emotion을 layer 밖에 두기 → 실용성 높음. 안정적

Emotion v11이 현재 @layer를 네이티브로 지원하지 않는다.
커스텀 플러그인이나 후처리 방식은 복잡도가 높고, 안정성이 떨어진다.

결론

Emotion을 Unlayered로 두는 전략이 가장 실용적이다.

4-2. 최종 우선 순위


CSS 스펙 규칙상
Unlayered 스타일은 모든 Layered 스타일보다 우선한다.

따라서 Emotion 스타일이 항상 Tailwind보다 높은 우선순위를 가진다.

Tailwind로 레이아웃 → Emotion으로 컴포넌트 스타일링 순서가 자연스럽게 보장된다.

4-3. 설정 방법

1. globals.css

2. reset.css 삭제

Tailwind Preflight가 브라우저 초기화를 담당한다.
그러므로 기존 reset.css는 삭제한다.
중복 선언을 방지하고 유지보수를 단순화 할 수 있다.

4-4. 유틸리티 오버라이드가 필요한 경우

현재 전략은 EmotionTailwind utilities보다 우선순위가 높다.
만약 특정 상황에서 Tailwind 유틸리티Emotion 스타일을 덮어써야 한다면 ! 접두사를 이용한다.

참고
!접두사는 해당 유틸리티에 !important를 추가한다.
남용은 하지말고..꼭 필요한 경우에만 사용하자..!


5. 환경별 설정 가이드

현재 나는 동시에 참여 중인 React, Next.js에서 모두 이원화 전략을 사용하고 있다.

맞다..그래서 2번의 세팅을 진행했다..( 주여.. )

5-1. React + Vite

1. globals.css

2. main.tsx


globals.css import만 하면 끝!!

5-2. Next.js(App Router)

Next.js에서 App Router에서 Emotion을 사용할 때는 SSR 대응을 위한 추가 설정이 필요하다.

1. app/globals.css

React와 동일하게 globals.css를 설정한다.

2. app/EmotionCacheProvider.tsx

참고
Emotion Hydration 해결 ( ▶이동 )
Emotion HTML 삽입 ( ▶이동 )

3. app/layout.tsx

5-3. 공통 확인 사항

  • @layer 선언 위치 : CSS 파일 최상단에 위치하는 지
  • 빌드 테스트 : dev/prod 환경에서 동일하게 동작하는지
  • 브라우저 확인 : 개발자 도구에서 Computed 탭에서 적용된 스타일 확인

6. 알아야 하는 개념들

6-1. CSS

Cascade Layer(@layer)

  • CSS 우선순위를 그룹 단위로 관리하는 메커니즘
  • 삽입 순서와 무관하게 선언된 순서대로 우선순위 설정
  • 나중에 선언된 layer가 높은 우선순위

Specificity vs Source Order

  • Specificity : 선택자의 구체성(#id > .class > tag)
  • Source Order : 동일 Specificity인 경우 나중에 선언된 스타일 우선
  • @layer 사용 시 Layer 우선순위가 Specificity보다 먼저 적용됨

Unlayered vs Layered

  • Unlayered : @layer 에 선언된 스타일
  • Layered : @layer 에 선언된 스타일

    우선 순위 : Unlayered > Layered

우선 순위 정리

Unlayered > Layered ( Layer > Source Order > Specificity(#id > .class > tag) )

6-2. Tailwind

Preflight

  • tailwind 내장 브라우저 초기화 스타일
  • modern-normalize 기반
  • base layer에 포함

layer 구조

  • theme : CSS 변수, 테마 토큰
  • base : Preflight, 기본 스타일
  • components : @apply 기반 컴포넌트
  • utilities : 유틸리티 클래스

    우선순위

    theme → base → components → utilities

v4 import 문법

  • @import "tailwindcss"; 한줄로 전체 import
  • 개별 importlayer( )함수로 맵핑

    여기서 주의해야할 점은,

    위에서 언급한 Preflightlayer 개념으로 "tailwindcss"에 내장되어 있다.

    그래서 @layer를 사용할 경우 무조건 @layer를 최상단에 선언한 뒤, @import "tailwindcss";를 위치시켜야 한다.

    @layer 선언 전에 @import "tailwindcss";를 진행한다면,이후 @layer 선언이 기존 구조를 덮어쓰거나 무시될 수 있다.

Tailwind 우선순위 선정 단계는 다음과 같다.

  1. @layer 선언 : (theme→ base → components → utilities 순으로 렌더) 스타일시트 처리 시점에서 layer 우선순위 구조 확정
  2. @import "tailwindcss"; : 이미 선언된 layer 안에 스타일 배치
  3. @config(optional) : Tailwind 설정(tailwind.config.ts) 설정 적용

참고
Tailwind v4부터는 tailwind.config.ts를 사용하지 않는다.
다만, 나는 스타일링 이원화에 따른 중앙 관리형 타이포시스템 사용을 위해 추가로 설정했다.

6-3. Emotion 관련

런타임 스타일 생성

  • 컴포넌트 렌더링 시점에 <style> 태그 동적 삽입
  • 빌드타임에 CSS 파일을 생성하지 않음

cacheProvider와 createCache

  • Emotion 스타일 캐싱 관리 : className 캐싱-중복 선언 방지를 위해
  • key : 생성되는 클래스명 접두사(prefix)
  • prepend : <style>태그 삽입 위치
    • true : head 최상단

prepend 옵션 주의

  • prepend : true → head 최상단에 삽입 → 우선순위 낮아짐.
  • @layer 사용 시 prepend 설정은 의미가 없음. layer 우선순위가 결정되기 때문에.

6-4. Next.js (App Router)

SSR과 CSS 삽입

  • 서버에서 HTML 렌더링 시 스타일도 함께 추출
  • 클라이언트 하이드레이션 후에도 스타일 유지 필요

Emotion Cache 패턴

  • useServerInsertedHTML로 SSR시 스타일 삽입
  • CacheProvider로 클라이언트 스타일 관리

7. 마무리

7-1. 핵심 요약

  • 문제 상황 : Tailwind + Emotion 동일 속성 충돌, 빌드마다 출력되는 결과 다름
  • 원인 : CSS 삽입 순서에 따른 Source Order 의존
  • 해결 : CSS Cascade Layer로 명시적 우선순위 설정
  • 전략 : Emotion을 Unlayered로 유지(최상위 우선순위)
  • 오버라이드 : 필요 시 !접두사 사용

7-2. 체크리스트

  1. globals.css@layer 선언
  2. reset.css 삭제(tailwind preflight로 대체)
  3. Next.js 경우 EmotionCache 설정
  4. dev/prod 빌드 테스트

( 이렇게 또 이슈 상황에서 살아남았다..! )
내가 판 무덤같은 이원화 전략이지만, 이왕 선택한 김에 제대로 활용해보고 싶다.
언젠간 만날 클린 코드를 기다리며..글을 마칩니당~

참고 자료
Tailwind Preflight 공식 문서
MDN @layer
Emotion Server Side Rendering 공식 문서
CSS Cascading and Inheritance Level 5

profile
바라는 색이 있다면 눈이 멀도록 바라볼 것. 가능한 온몸으로 부서질 것.

0개의 댓글