mason
mason-log.

스타일링 방식 비교.

mason

masonfe-hyunsu

🧵 1. 개요.

스타일링 도구들이 한자리에 모인 상징
  • 프론트에서 스타일을 쓰는 길이 너무 많아졌다. 팀마다 기본값이 다르고, 같은 React라도 파일 구조가 완전히 달라진다.

  • 이 포스팅에서는 CSS, SCSS, CSS Modules, Tailwind, Emotion을 같은 기준으로 비교하고, 상황마다 무엇을 고를지 정리해 본다.

  • CSS-in-JS 축은 Emotion으로만 본다. 결이 비슷한 라이브러리를 나열하기보다, 지금 기준으로 고를 때 보는 쪽을 기준으로 잡았다.

한 줄로 요약하면, 스타일링 선택은 문법 취향이 아니라 범위 제어, 빌드 비용, 컴포넌트와의 거리를 어디에 둘지 정하는 일이다.


🧭 2. 비교할 축.

도구 이름보다 먼저 보는 건 아래 세 축이다.

  • 범위 제어: 클래스가 전역으로 새는지, 컴포넌트 단위로 막히는지

  • 작성 위치: 스타일이 HTML/JSX와 얼마나 가까운지, 디자인 토큰을 어디서 공유하는지

  • 비용이 붙는 시점: 빌드 타임인지, 런타임인지, 둘 다인지

같은 “예쁘게 만든다”라도, 이 세 축에서 답이 갈린다. 그래서 문법 비교표만 보면 선택이 안 잡힌다.

범위 빌드 런타임 세 축

📄 3. CSS.

  • 브라우저가 그대로 이해하는 원본 규약이고, 전처리기나 런타임 라이브러리 없이 동작한다.

  • 요즘은 native nesting(네이티브 중첩), custom properties(커스텀 속성, 흔히 CSS 변수) 덕분에 “SCSS 없으면 못 쓴다”는 말은 예전보다 약해졌다.

  • 문제는 문법이 아니라 전역 네임스페이스다. 클래스 이름 충돌, 선택자 특이성(specificity) 싸움, 삭제해도 어디선가 남는 유령 규칙이 쌓이기 쉽다.

:root {
  --space-md: 1rem;
}

.card {
  padding: var(--space-md);

  & .title {
    font-weight: 600;
  }
}
  • 작은 사이트, 디자인 시스템이 얇은 페이지, 레거시 유지보수에는 여전히 정직한 선택이다.

  • 대신 규모가 커지면 이름 규칙과 레이어 규약을 팀이 직접 지켜야 하고, 도구가 막아 주지 않는다.


🎨 4. SCSS.

  • SCSS는 CSS 위에 변수, 믹스인, 중첩, partial import를 얹은 전처리기다. 빌드 때 일반 CSS로 컴파일된다.

  • 토큰과 공통 패턴을 _tokens.scss, _mixins.scss로 나누기 좋고, 디자인 시스템이 파일 트리로 보이는 팀에 잘 맞는다.

  • 다만 결과물은 결국 전역 CSS에 가깝다. 중첩이 깊어지면 선택자가 무거워지고, “이 클래스가 어디 쓰이지?”를 추적하기 어려워진다.

$space-md: 1rem;

.card {
  padding: $space-md;

  &__title {
    font-weight: 600;
  }
}
  • BEM(Block Element Modifier, 블록, 요소, 수식어로 클래스 이름을 나누는 규칙) 같은 네이밍과 같이 쓰면 버틸 만하다.

  • 지금은 native nesting과 CSS 변수가 많이 따라와서, “SCSS가 아니면 안 된다” 지점은 줄었다. 그래도 기존 SCSS 디자인 토큰 자산이 크면 갈아타기 비용이 먼저다.


🧩 5. CSS Modules.

  • CSS Modules는 파일 단위로 클래스 이름을 해시해서, 전역 충돌을 빌드 타임에 막는 방식이다. 문법은 CSS/SCSS에 가깝고 동작만 로컬이다.

  • React 컴포넌트와 styles.card처럼 연결하면, 쓰이지 않는 클래스를 찾기도 상대적으로 쉽다.

import styles from './Card.module.css';

export function Card() {
  return <div className={styles.card}>...</div>;
}
  • 장점: 런타임 CSS-in-JS 비용이 없고, 일반 CSS 지식을 거의 그대로 쓴다.

  • 단점: 동적 값(props에 따른 색, 간격)은 변수나 인라인으로 우회해야 하고, 유틸리티 조합 UX는 Tailwind만큼 빠르지 않다.

  • CSS/SCSS의 익숙함을 유지하면서 범위만 안전하게 잡고 싶을 때 자주 고른다. 이 포스팅에 따로 넣은 이유도 이 지점이다.


🌬️ 6. Tailwind.

  • Tailwind는 미리 정의된 유틸리티 클래스를 마크업에 붙여 스타일을 조합하는 방식이다. “클래스 이름을 짓는” 부담을 거의 없앤다.

  • 디자인 토큰이 tailwind.config에 모이고, 빌드가 쓰인 클래스만 CSS로 남긴다. 런타임에 스타일 객체를 계산하지 않는다.

<button className="rounded-lg bg-teal-700 px-4 py-2 text-sm text-white hover:bg-teal-800">
  저장
</button>
  • 빠른 UI 스케치, 반복적인 간격, 타이포 패턴, 디자인 시스템이 유틸 토큰으로 합의된 팀에 강하다.

  • 반대로 마크업이 길어지고, “이 버튼의 시각 규칙”이 JSX 여기저기에 흩어진다. 복잡한 애니메이션, 특수 선택자는 결국 @apply나 별도 CSS로 빠진다.

  • 일관된 토큰 + 빠른 구현이 우선이면 잘 맞고, 스타일 구조 자체를 컴포넌트 API로 숨기고 싶다면 Emotion 쪽이 덜 거슬린다.


🎭 7. Emotion.

  • Emotion은 JS 안에서 스타일을 쓰고, 클래스명을 런타임(또는 설정에 따라 빌드)에 붙이는 CSS-in-JS다. 이 블로그도 Emotion styled를 쓴다.

  • props, 테마, 조건 분기를 같은 언어로 표현할 수 있어서, 컴포넌트 API와 스타일 규칙이 한곳에 모인다.

const Button = styled.button<{ $primary?: boolean }>`
  border-radius: 0.5rem;
  padding: 0.5rem 1rem;
  background: ${({ $primary, theme }) =>
    $primary ? theme.colors.teal : theme.colors.muted};
`;
  • 장점: 동적 스타일, 테마 토큰, 스타일 격리, 컴포넌트 단위 사고.

  • 단점: 번들, 런타임 비용, SSR/스트리밍에서의 스타일 주입 설정, “순수 CSS”보다 학습면이 넓다.

  • 지금 기준으로 CSS-in-JS를 고른다면, 내가 실제로 쓰는 쪽은 Emotion이다. 같은 축의 다른 라이브러리를 나열해 비교표만 키우지는 않는다.


⚖️ 8. 무엇을 고를까.

정답 하나보다, 제약을 먼저 고른다.

  • 전역 문서, 마케팅 페이지, 스타일 양이 적음 → CSS (필요하면 SCSS)

  • CSS 문법 유지 + 충돌만 막고 싶음 → CSS Modules

  • 토큰 합의된 제품 UI를 빠르게 쌓음 → Tailwind

  • 테마, props 기반 컴포넌트 라이브러리, 스타일을 JS 경계에 두고 싶음 → Emotion

  • 이미 SCSS 토큰, 믹스인 자산이 큼 → 당장 갈아타기보다 SCSS(+ Modules)로 범위를 좁히는 편이 싸다

실무에서는 하나가 독점하지 않는다. 글로벌 reset은 CSS, 앱 UI는 Tailwind나 Emotion, 레거시 영역은 Modules처럼 경계를 나누는 경우가 많다.

중요한 건 유행이 아니라, 팀이 삭제, 추적, 테마 변경을 얼마나 자주 하는지다. 그 빈도가 높을수록 범위 제어와 컴포넌트 거리가 이긴다.

CSS SCSS, Modules, Tailwind, Emotion 선택 옵션

🔗 9. 참고 자료.

이전 글
이미지가 브라우저에서 렌더링 되는 과정.

댓글

불러오는 중...
목록으로