CSS Modules vs CSS-in-JS vs Tailwind CSS 비교: 실무와 SSR에서 보는 스타일링 전략

Frontend

이 글은 스타일링 선택 기준 시리즈 2편입니다.

프론트엔드에서 스타일링 전략을 고를 때 후보는 생각보다 다양합니다. 전역 CSS를 어떻게 나눌지, CSS Modules를 쓸지, Emotion이나 styled-components 같은 CSS-in-JS를 쓸지, 아니면 Tailwind CSS처럼 유틸리티 클래스 중심으로 갈지 팀마다 선택이 달라집니다.

이때 흔히 "무엇이 더 생산적인가"만 묻기 쉽지만, 실제로는 그보다 더 오래 남는 문제가 있습니다.

  • SSR에서 스타일이 안정적으로 내려오는가
  • 런타임 비용이 늘어나지 않는가
  • 디자인 시스템과 토큰을 어디에서 관리할 것인가
  • 신규 인력이 들어왔을 때 이해하기 쉬운가
  • 컴포넌트가 많아질수록 수정 범위를 추적하기 쉬운가

즉, 스타일링 방식 선택은 취향 문제가 아니라 렌더링 방식과 팀 운영 방식까지 같이 결정하는 문제에 가깝습니다.

이 글에서는 비교 대상을 아래처럼 두겠습니다.

  • CSS: 실무에서 가장 비교하기 좋은 기준점인 CSS Modules
  • CSS-in-JS: Emotion, styled-components 같은 런타임 기반 방식
  • Tailwind CSS: 유틸리티 클래스 기반 방식

한눈에 보면

먼저 짧게 정리하면 이렇습니다.

  • CSS Modules: 표준 CSS 기반으로 가되 컴포넌트 단위 격리를 가져가고 싶을 때 무난합니다.
  • CSS-in-JS: 컴포넌트와 스타일 로직을 강하게 붙이고, 동적인 variant와 theme 조합을 코드 안에서 다루고 싶을 때 잘 맞습니다.
  • Tailwind CSS: 빠른 화면 조립, 일관된 spacing/scale 운영, 디자인 시스템 규칙 공유에 강한 편입니다.

조금 더 자세히 보면 아래처럼 볼 수 있습니다.

항목 CSS Modules CSS-in-JS (Emotion, styled-components) Tailwind CSS
기본 성격 정적 CSS + 클래스 스코프 JS 안에서 스타일 선언 유틸리티 클래스 조합
런타임 비용 거의 없음 상대적으로 있음 거의 없음
SSR 궁합 단순한 편 설정과 라이브러리 이해가 더 필요 단순한 편
RSC/App Router 궁합 좋음 라이브러리별 제약 고려 필요 좋음
동적 스타일 표현 보통 강함 상태 조합은 가능하나 길어질 수 있음
디자인 토큰 운영 CSS 변수와 조합하기 좋음 ThemeProvider 패턴과 잘 맞음 theme config와 조합하기 좋음
협업 가독성 CSS와 마크업이 분리됨 컴포넌트 안에 응집됨 JSX 안에서 바로 읽힘
장기 유지보수 비교적 안정적 팀 규칙이 없으면 편차가 커질 수 있음 클래스 규칙 합의가 중요

빠르게 정리하면, 표준 기반과 안정성을 우선하면 CSS Modules, 동적 스타일링과 컴포넌트 응집을 우선하면 CSS-in-JS, 속도와 일관된 스케일 운영을 우선하면 Tailwind CSS로 보면 됩니다.

왜 이 비교가 실무에서 중요할까?

스타일링 방식은 화면 하나를 만들 때보다, 화면 수가 늘고 팀원이 늘어날수록 차이가 커집니다.

예를 들어 같은 버튼 컴포넌트를 만들어도 관점이 다릅니다.

  • CSS Modules는 "스타일 파일을 어떻게 나누고 클래스 충돌을 어떻게 막을까"를 중심으로 봅니다.
  • CSS-in-JS는 "컴포넌트 상태와 스타일을 같은 문맥에서 다룰 수 있을까"를 중심으로 봅니다.
  • Tailwind CSS는 "디자인 토큰과 유틸리티 조합으로 얼마나 빠르게 일관성을 유지할까"를 중심으로 봅니다.

즉, 차이는 문법보다 무엇을 비용으로 보고 무엇을 단순화하려는가에서 드러납니다.

각 방식은 어떤 감각으로 쓰이게 될까?

CSS Modules

CSS Modules는 가장 표준 CSS에 가깝지만, 전역 클래스 충돌 문제를 줄여준다는 점에서 실무 기준점으로 잡기 좋습니다.

/* Button.module.css */
.button {
  padding: 12px 16px;
  border-radius: 8px;
  background: var(--color-primary);
  color: white;
}
 
.danger {
  background: var(--color-danger);
}
import styles from './Button.module.css';
 
export function Button({
  danger = false,
  children,
}: {
  danger?: boolean;
  children: React.ReactNode;
}) {
  return (
    <button className={danger ? `${styles.button} ${styles.danger}` : styles.button}>
      {children}
    </button>
  );
}

장점은 비교적 분명합니다.

  • 브라우저 표준 CSS 문법을 그대로 쓸 수 있고
  • 빌드 결과가 정적 자산으로 떨어지며
  • SSR과 App Router 환경에서 별도 런타임 스타일 처리가 거의 필요 없습니다

대신 상태와 variant가 많아질수록 클래스 조합이 늘어나고, 스타일 파일과 컴포넌트 파일을 같이 오가야 한다는 점은 있습니다.

CSS-in-JS

Emotion, styled-components 계열은 스타일을 컴포넌트 로직과 더 가깝게 붙여서 다룰 수 있다는 장점이 있습니다.

import styled from 'styled-components';
 
const Button = styled.button<{ $danger?: boolean }>`
  padding: 12px 16px;
  border-radius: 8px;
  background: ${({ $danger }) => ($danger ? 'var(--color-danger)' : 'var(--color-primary)')};
  color: white;
`;
 
export function ActionButton() {
  return <Button $danger>삭제</Button>;
}

이 방식은 특히 아래 상황에서 편합니다.

  • props에 따라 스타일 분기가 많은 경우
  • variant 조합이 복잡한 컴포넌트 라이브러리를 만드는 경우
  • ThemeProvider 같은 패턴으로 토큰을 코드 안에서 강하게 묶고 싶은 경우

반면 실무에서는 아래 비용도 같이 봐야 합니다.

  • 런타임 스타일 생성 비용
  • SSR 시 스타일 수집과 주입 과정
  • hydration 타이밍과 스타일 삽입 순서
  • 라이브러리별 설정 차이

즉, CSS-in-JS는 문법이 예쁜지만 볼 것이 아니라 런타임과 렌더링 파이프라인에 어떤 일을 추가하는지까지 같이 봐야 합니다.

Tailwind CSS

Tailwind CSS는 스타일을 별도 CSS 파일보다 유틸리티 클래스 조합으로 가져가는 방식입니다.

export function Button() {
  return (
    <button className="rounded-lg bg-blue-600 px-4 py-3 text-sm font-medium text-white hover:bg-blue-700">
      저장하기
    </button>
  );
}

이 방식의 장점은 명확합니다.

  • 화면 조립 속도가 빠르고
  • spacing, color, typography 스케일이 일관되며
  • 정적 추출 기반이라 SSR과 성능 관점에서도 단순합니다

대신 JSX 안의 클래스 문자열이 길어질 수 있고, 의미 있는 UI 추상화 없이 유틸리티만 계속 쌓으면 컴포넌트 설계 없이 마크업만 두꺼워지는 상태로 가기 쉽습니다.

그래서 Tailwind CSS는 도구 자체보다도, 언제 유틸리티로 두고 언제 컴포넌트로 추상화할지 규칙을 같이 가져가야 합니다.

SSR과 Server Components 관점에서 보면

이 지점은 요즘 프론트엔드에서 특히 중요합니다. Next.js App Router, React Server Components, streaming SSR 환경에서는 스타일링 방식의 차이가 더 분명해지기 때문입니다.

CSS Modules와 Tailwind CSS는 대체로 단순하다

두 방식 모두 최종적으로는 정적인 CSS 자산으로 정리되는 비중이 큽니다.

그래서 보통은:

  • 서버에서 스타일을 따로 수집하는 코드가 덜 필요하고
  • streaming SSR에서도 상대적으로 단순하며
  • hydration 이후 스타일 불일치 가능성도 비교적 낮습니다

즉, SSR 환경에서 문제를 줄이는 쪽으로 보면 CSS ModulesTailwind CSS는 운영 난이도가 낮은 편입니다.

CSS-in-JS는 가능하지만 고려할 것이 더 많다

Emotionstyled-components도 SSR을 지원합니다. 다만 지원 여부와 별개로, 실무에서는 아래 질문을 더 보게 됩니다.

  • 서버에서 어떤 방식으로 스타일을 모을 것인가
  • streaming 중 스타일 삽입 순서를 어떻게 맞출 것인가
  • App Router와 현재 라이브러리 조합이 안정적인가
  • 런타임 스타일 생성 비용이 큰 컴포넌트는 없는가

즉, CSS-in-JS가 SSR에서 "안 된다"는 뜻은 아닙니다. 다만 잘 동작하게 만들기 위해 팀이 이해해야 할 레이어가 더 많다는 뜻에 가깝습니다.

요청 단위 테마는 무엇이 더 유연할까?

SSR에서 자주 나오는 요구사항 중 하나는 요청 단위 테마입니다.

  • 고객사별 브랜드 컬러
  • 사용자별 다크모드
  • 국가별 혹은 제품별 테마 차이

이런 경우에는 세 방식 모두 대응할 수 있지만 감각이 다릅니다.

  • CSS Modules: CSS variables와 조합해서 대응
  • CSS-in-JS: theme 객체와 props 조합으로 대응
  • Tailwind CSS: CSS variables 또는 제한된 theme scale 조합으로 대응

실무에서는 런타임 유연성과 SSR 단순성을 같이 보게 되는데, 복잡한 동적 테마가 많을수록 CSS-in-JS나 CSS variables 조합이 유리하고, 정해진 디자인 스케일 안에서 움직일수록 CSS Modules나 Tailwind CSS가 단순합니다.

운영 비용 관점에서는 무엇을 봐야 할까?

1. 번들·성능·런타임 비용

이 축은 CSS-in-JS에서 특히 중요합니다.

  • CSS Modules: 정적 CSS라서 런타임 비용이 낮음
  • Tailwind CSS: 정적 추출 기반이라 런타임 비용이 낮음
  • CSS-in-JS: 스타일 계산과 주입이 런타임에 일부 개입할 수 있음

컴포넌트 수가 적을 때는 체감이 크지 않을 수 있지만, 대형 서비스나 디자인 시스템에서는 이 차이를 무시하기 어렵습니다.

2. 협업 방식

스타일링 전략은 협업 방식에도 영향을 줍니다.

  • 디자이너와 토큰 중심으로 이야기하는 팀
  • 마크업과 스타일을 한 파일에서 보고 싶은 팀
  • CSS 자체를 전문적으로 다루는 팀

예를 들어:

  • CSS 친화적인 팀이면 CSS Modules가 자연스럽고
  • React 컴포넌트 응집을 강하게 선호하면 CSS-in-JS가 맞을 수 있으며
  • 빠른 제품 실험과 일관된 scale 운영이 중요하면 Tailwind CSS가 잘 맞습니다

즉, 좋은 전략은 절대값이 아니라 팀이 읽고 수정하고 리뷰하기 쉬운 전략입니다.

3. 디자인 시스템 운영

디자인 시스템 관점에서도 선택 기준이 달라집니다.

CSS Modules

  • 토큰을 CSS variables로 두고
  • 컴포넌트 단위 스타일 파일로 분리하며
  • 정적 결과물을 예측 가능하게 유지하기 좋습니다

CSS-in-JS

  • variant와 theme 조합을 타입과 함께 다루기 좋고
  • props 기반 API를 만들기 편하며
  • 복잡한 컴포넌트 라이브러리에서는 응집도가 높습니다

Tailwind CSS

  • 디자인 scale을 강하게 통일하기 좋고
  • 프로토타이핑과 제품 화면 생산 속도가 빠르며
  • 사내 규칙이 정리되면 화면 간 편차를 줄이기 쉽습니다

다만 디자인 시스템을 오래 운영할수록 중요한 것은 문법보다 토큰이 어디에 있고, override 규칙이 무엇이며, variant를 누가 통제하느냐입니다.

어떤 팀에 어떤 전략이 잘 맞을까?

CSS Modules가 잘 맞는 경우

  • Next.js SSR/App Router 환경에서 단순함이 중요할 때
  • 표준 CSS 기반으로 가고 싶을 때
  • 디자인 토큰을 CSS variables 중심으로 운영할 때
  • 런타임 비용을 최소화하고 싶을 때

CSS-in-JS가 잘 맞는 경우

  • 컴포넌트 라이브러리의 variant가 많을 때
  • props 기반 스타일 분기가 많을 때
  • 테마 객체와 스타일 로직을 한 문맥에서 다루고 싶을 때
  • 팀이 SSR 설정과 런타임 비용을 충분히 이해하고 있을 때

Tailwind CSS가 잘 맞는 경우

  • 화면 생산 속도와 일관된 스케일 운영이 중요할 때
  • 많은 화면을 빠르게 조립해야 할 때
  • 디자인 시스템 규칙을 유틸리티로 강하게 통일하고 싶을 때
  • SSR 환경에서 스타일 계층을 단순하게 유지하고 싶을 때

실무에서는 어떻게 고르면 좋을까?

아래처럼 정리하면 비교적 판단이 쉽습니다.

상황 우선 고려할 방식
표준 CSS 기반, 낮은 운영 복잡도 CSS Modules
복잡한 variant, theme, props 기반 스타일링 CSS-in-JS
빠른 UI 생산, 일관된 scale, 낮은 런타임 비용 Tailwind CSS
SSR/App Router에서 단순함이 가장 중요 CSS Modules, Tailwind CSS
디자인 시스템 컴포넌트 API를 코드로 강하게 통제 CSS-in-JS

정리하면

스타일링 전략에는 정답이 없습니다. 대신 각 방식이 줄여주는 비용과 늘리는 비용은 비교적 분명합니다.

  • CSS Modules: 표준 CSS 기반, 안정적인 운영, 낮은 런타임 비용
  • CSS-in-JS: 높은 응집도와 동적 표현력, 대신 더 많은 런타임·SSR 고려사항
  • Tailwind CSS: 빠른 생산성과 일관된 스케일, 대신 추상화 기준이 없으면 마크업이 비대해질 수 있음

제 경험 기준으로 요약하면 이렇습니다.

  • 단순하고 오래 가는 선택이 필요하면 CSS Modules
  • 스타일 로직과 컴포넌트 API를 강하게 묶고 싶으면 CSS-in-JS
  • 속도와 일관성이 가장 중요하면 Tailwind CSS

중요한 것은 어떤 문법이 더 멋진지가 아니라, 우리 팀의 렌더링 방식, 디자인 시스템 운영 방식, 리뷰 문화에 어떤 전략이 가장 잘 맞는가입니다.

같이 보면 좋은 글