
React 생태계의 대표적인 CSS-in-JS 라이브러리로 styled-components와 Emotion이 자주 비교된다.
Emotion은 상대적으로 가벼운 번들 사이즈와 유연한 css prop 지원 덕분에 실무에서 많이 채택된다.

번들 사이즈 비교


Next.js 환경에서 ${ChildDiv} { background-color: red; }처럼 컴포넌트 셀렉터를 사용할 때 @emotion/styled가 컴포넌트 클래스명을 제대로 인식하지 못하는 문제가 발생할 수 있다.
이 현상은 Emotion 전용 바벨 플러그인이 컴포넌트를 고유 클래스 셀렉터로 변환해 주지 못해 일어난다. 아래 절차로 설정을 추가하면 해결된다.
yarn add --dev @emotion/babel-plugin
.babelrc 설정프로젝트 루트 경로에 .babelrc 파일을 생성하고 Next.js 프리셋과 Emotion 플러그인을 연결한다.
{
"presets": [
[
"next/babel",
{
"preset-react": {
"runtime": "automatic",
"importSource": "@emotion/react"
}
}
]
],
"plugins": ["@emotion/babel-plugin"]
}
스타일을 적용할 파일 상단에 pragma 주석과 필요한 모듈을 불러온다.
/** @jsxImportSource @emotion/react */
import { jsx } from "@emotion/react";
import styled from "@emotion/styled";
npm trends 지표를 보면 다운로드 수치 면에서 Emotion의 선호도가 높은 편이다.
번들 사이즈 관점에서는 Emotion이 상대적으로 더 가볍다. 다만 두 라이브러리 간 런타임 렌더링 성능 차이는 일반적인 서비스 환경에서 유의미하게 크지 않다.
두 라이브러리 모두 태그드 템플릿 리터럴과 객체 문법을 지원하지만, Emotion은 인라인 스타일에 가까운 css prop 문법을 더욱 매끄럽게 지원한다.
// styled-components
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
// Emotion (styled 방식 및 css prop 방식 모두 지원)
const titleStyles = css`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
<h1 css={titleStyles}>Hiya!</h1>;
&)& 기호는 자기 자신(현재 컴포넌트의 셀렉터)을 가리킨다.
import styled from "@emotion/styled";
const MyDiv = styled.div`
width: 200px;
height: 200px;
background-color: red;
// MyDiv:hover 와 동일
&:hover {
background-color: blue;
}
`;
부모 컴포넌트의 스타일 블록 안에서 ${자식컴포넌트} 형태로 특정 하위 컴포넌트의 스타일을 오버라이딩할 수 있다.
/** @jsxImportSource @emotion/react */
import { jsx } from "@emotion/react";
import styled from "@emotion/styled";
const ChildDiv = styled.div`
width: 100px;
height: 100px;
background-color: blue;
`;
const ParentDiv = styled.div`
width: 500px;
height: 400px;
// ParentDiv 내부의 ChildDiv만 배경색을 red로 변경
${ChildDiv} {
background-color: red;
}
`;
export default function App() {
return (
<ParentDiv>
<ChildDiv />
</ParentDiv>
);
}
@emotion/react의 Global 컴포넌트를 활용해 Reset CSS나 전체 공통 스타일을 주입한다.
// styles/reset.js
import { css, Global } from "@emotion/react";
export const globalStyles = (
<Global
styles={css`
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html, body {
width: 100vw;
height: 100vh;
font-size: 16px;
}
`}
/>
);
// pages/_app.js
import { globalStyles } from "../styles/reset";
export default function MyApp({ Component, pageProps }) {
return (
<>
{globalStyles}
<Component {...pageProps} />
</>
);
}
styled(BaseComponent))이미 정의된 컴포넌트의 기본 속성을 유지한 채 새로운 스타일을 덮어씌울 때 유용하다.
const MyButton = styled.button`
font-size: 23px;
border: none;
`;
const BlackButton = styled(MyButton)`
background-color: black;
color: white;
`;
const RedButton = styled(MyButton)`
background-color: red;
color: blue;
`;
withComponent vs as prop스타일은 그대로 유지하되 렌더링되는 HTML 태그만 변경해야 할 때 두 가지 방식이 있다.
const MyFont = styled.p`
font-size: 24px;
font-weight: bold;
`;
// 방법 1: withComponent 사용 (새로운 컴포넌트 정의)
const MyItalicFont = styled(MyFont.withComponent("span"))`
font-style: italic;
font-weight: normal;
`;
// 방법 2: as prop 전달 (컴포넌트 선언 없이 JSX 레벨에서 태그 치환)
<MyFont as="span">span으로 렌더링된 텍스트</MyFont>
불필요한 변수 선언을 줄이고 코드 가독성을 높이기 위해서는 as prop을 활용하는 편이 훨씬 직관적이다.
자주 사용하는 스타일 조각을 css 함수로 선언해 두고 필요한 컴포넌트 내부에 삽입한다.
import { css } from "@emotion/react";
import styled from "@emotion/styled";
const hoverEffect = css`
&:hover {
background-color: red;
}
`;
const BaseButton = styled.button`
border: none;
padding: 10px;
font-weight: bold;
`;
const BlueButton = styled(BaseButton)`
background-color: blue;
${hoverEffect} // 믹스인 스타일 반영
`;
ThemeProvider)공통 테마 객체를 정의하고 최상단에서 ThemeProvider로 감싸면 하위 컴포넌트 어디서든 props.theme로 변수를 읽어올 수 있다.
// color.js
const MyColor = {
default: "black",
myAppColor: "red",
};
export default MyColor;
import { ThemeProvider } from "@emotion/react";
import styled from "@emotion/styled";
import MyColor from "./color";
const MyButton = styled.button`
border: none;
padding: 10px;
// props 조건에 따라 테마 색상 분기
background-color: ${(props) => (props.myAppColor ? MyColor.myAppColor : MyColor.default)};
`;
export default function App() {
return (
<ThemeProvider theme={MyColor}>
<MyButton>기본버튼</MyButton>
<MyButton myAppColor>테마버튼</MyButton>
</ThemeProvider>
);
}

keyframes 헬퍼 함수로 정의한 애니메이션 키프레임을 컴포넌트 내부에서 변수처럼 사용한다.
import { css, keyframes } from "@emotion/react";
import styled from "@emotion/styled";
const rotation = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const WarningButton = styled.button`
border: none;
padding: 10px;
${(props) =>
props.warning &&
css`
animation: ${rotation} 1s linear infinite;
background-color: red;
`}
`;
styled(MUIComponent) 형태로 래핑하여 스타일을 재정의할 때, HTML 고유 속성이나 라이브러리 전용 props는 styled 내부에서 주입하려 하지 말고 렌더링되는 JSX 태그에 직접 prop으로 전달해야 스타일 누락이나 타입 에러를 피할 수 있다.