스타일링 방식 비교.
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처럼 경계를 나누는 경우가 많다.
중요한 건 유행이 아니라, 팀이 삭제, 추적, 테마 변경을 얼마나 자주 하는지다. 그 빈도가 높을수록 범위 제어와 컴포넌트 거리가 이긴다.

댓글