- Published on
styled-components에서 Emotion으로 점진적으로 마이그레이션하기
- Authors

- Name
- GW
개요
프로젝트가 커질수록 스타일링 코드도 함께 복잡해집니다.
처음에는 styled.div, styled.button처럼 의미 있는 이름의 컴포넌트를 만들어 사용하는 방식이 읽기 쉽고 편리합니다.
하지만 화면과 상태가 많아지면서 스타일 전용 컴포넌트가 계속 늘어나기 시작하면 이야기가 달라집니다.
const Container = styled.div``;
const Header = styled.div``;
const Title = styled.h2``;
const ButtonWrapper = styled.div``;
const SubmitButton = styled.button``;
컴포넌트 이름만 보면 구조가 명확해 보이지만, 실제 비즈니스 로직과 관계없는 스타일 전용 컴포넌트가 많아질수록 JSX 구조를 한눈에 파악하기 어려워질 수 있습니다.
또한 같은 스타일을 여러 컴포넌트에서 조금씩 변형해 사용하거나, 상태에 따라 여러 조건부 스타일을 조합해야 하는 경우에는 스타일 정의 방식도 점점 복잡해집니다.
이러한 문제를 개선하기 위해 styled-components 기반 스타일링을 Emotion으로 점진적으로 전환하고, 단순한 라이브러리 교체를 넘어 스타일 정의 방식과 SSR 스타일링 환경을 함께 정비했습니다.
이번 글에서는
- 기존 스타일링 구조에서 어떤 문제가 있었는지
- 왜 Emotion을 선택했는지
- 왜
styledAPI보다css기반 조합을 적극적으로 사용했는지 - 점진적으로 마이그레이션한 이유와 방법
- SSR 환경에서는 무엇을 함께 고려해야 했는지
- 실제 구조가 어떻게 달라졌는지
- 마이그레이션 과정에서 얻은 장점과 트레이드오프
를 중심으로 살펴보겠습니다.
기존 스타일링 구조에서 어떤 문제가 있었을까?
styled-components 자체가 문제였던 것은 아닙니다.
styled-components는 CSS를 JavaScript 안에서 작성할 수 있고, props 기반 동적 스타일링, 테마, SSR 등 다양한 기능을 제공합니다.
즉, 이번 마이그레이션의 목적은
styled-components의 기능이 부족해서 다른 라이브러리로 교체하는 것
이 아니었습니다.
더 큰 문제는 프로젝트가 성장하면서 스타일을 표현하는 방식이 컴포넌트 중심으로 지나치게 세분화되고 있었다는 점이었습니다.
예를 들어 다음과 같은 코드를 생각해 보겠습니다.
const Container = styled.div`
display: flex;
flex-direction: column;
`;
const Header = styled.div`
display: flex;
align-items: center;
`;
const Title = styled.h2`
font-size: 20px;
font-weight: 700;
`;
const ButtonWrapper = styled.div`
display: flex;
gap: 8px;
`;
const SubmitButton = styled.button<{ disabled: boolean }>`
padding: 12px 16px;
background: ${({ disabled }) => (disabled ? '#ccc' : '#000')};
`;
JSX에서는 다음과 같이 사용합니다.
<Container>
<Header>
<Title>프로필 수정</Title>
</Header>
<ButtonWrapper>
<SubmitButton disabled={isDisabled}>저장</SubmitButton>
</ButtonWrapper>
</Container>
각각의 이름은 의미가 있지만, 실제 React 컴포넌트 관점에서 보면 대부분은 스타일을 적용하기 위해 만들어진 Wrapper입니다.
이런 컴포넌트가 화면마다 계속 늘어나면 JSX를 읽을 때
<UserProfilePage> ──> <CardWrapper> ──> <FlexContainer> ──> <div> (실제 DOM)
└────────── 스타일 껍데기들 ────────┘
처럼 단계를 거쳐 구조를 해석해야 합니다.
또 상태에 따른 스타일 변화가 많아질수록 props 역시 스타일링을 위해 추가되기 시작합니다.
<Button active={isActive} disabled={isDisabled} selected={isSelected} />
스타일링 목적의 props가 컴포넌트 인터페이스에 섞이면서 UI 상태와 스타일 구현 세부사항의 경계도 흐려질 수 있습니다.
따라서 이번 개선에서는 단순히 라이브러리를 바꾸는 것보다
스타일을 컴포넌트로 표현할 것인지, 재사용 가능한 스타일 조각으로 표현할 것인지
를 다시 정리하는 것이 더 중요한 목표였습니다.
왜 Emotion을 선택했을까?
css prop과 Composition 중심의 스타일링 구조
Emotion 역시 styled-components와 같은 Runtime CSS-in-JS 라이브러리입니다.
따라서 두 라이브러리는 상당히 비슷한 기능을 제공합니다.
예를 들어 Emotion에서도 styled API를 사용할 수 있습니다.
import styled from '@emotion/styled';
const Button = styled.button`
padding: 12px 16px;
`;
즉,
styled-components
↓
Emotion styled
로 단순히 API만 바꾼다면 구조적으로 얻을 수 있는 변화는 크지 않습니다.
Emotion을 선택하면서 더 중요하게 본 부분은 css prop과 스타일 조합(Composition) 이었습니다.
Emotion 공식 문서에서도 css prop을 주요 스타일링 방식으로 소개하고 있으며, 여러 스타일을 조합하는 Composition을 핵심 패턴 중 하나로 설명합니다.
예를 들어 다음과 같이 스타일을 작은 단위로 분리할 수 있습니다.
const buttonBaseStyle = css`
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
`;
const primaryStyle = css`
background-color: #000;
color: #fff;
`;
const disabledStyle = css`
background-color: #ccc;
cursor: not-allowed;
`;
그리고 컴포넌트에서는 필요한 스타일을 조합합니다.
<button css={[buttonBaseStyle, primaryStyle, isDisabled && disabledStyle]}>저장</button>
구조가 다음처럼 달라집니다.
기존
Button 컴포넌트에 props 전달
↓
컴포넌트 내부에서 조건부 스타일 계산
개선
기본 스타일(buttonBaseStyle)
+ variant 스타일(primaryStyle)
+ 상태 스타일(disabledStyle)
↓
필요한 스타일 조합
이렇게 하면 스타일을 적용하기 위해 새로운 React 컴포넌트를 만들지 않고도 공통 스타일과 조건부 스타일을 재사용할 수 있습니다.
TypeScript 기반의 Object Style
Emotion은 문자열 방식뿐 아니라 Object Style도 공식적으로 지원합니다.
특히 TypeScript 환경에서는 Object Style을 사용할 경우 csstype을 기반으로 CSS 속성과 값에 대한 타입 검사를 받을 수 있습니다.
Emotion 공식 문서에서도 TypeScript 환경에서 Object Style을 사용할 경우 이러한 타입 지원을 활용할 수 있다고 설명하고 있습니다.
// 1. String Style
const stringStyle = css`
color: red;
box-sizing: border-boxxxx;
`;
// 2. Object Style
const titleStyle = css({
color: 'red',
boxSizing: 'border-boxxxx', // ❌ 오류 발생
});
두 방식 모두 동일한 스타일을 표현할 수 있지만, Object Style은 IDE 자동 완성과 타입 검사의 도움을 받을 수 있다는 장점이 있습니다.
예를 들어 존재하지 않는 CSS 속성을 작성하면 TypeScript가 바로 오류를 알려줍니다.
이처럼 스타일 작성 단계에서 실수를 빠르게 발견할 수 있기 때문에, 프로젝트 규모가 커질수록 유지보수성과 개발 생산성을 높이는 데 도움이 됩니다.
왜 styled API보다 css 기반 조합을 사용했을까?
Emotion도 styled API를 제공하기 때문에 모든 코드를 css로 작성할 필요는 없습니다.
오히려 재사용 가능한 독립 UI 컴포넌트라면 styled가 더 자연스러운 경우도 있습니다.
예를 들어 디자인 시스템의 Button, Input, Card처럼 스타일 + 동작 + 컴포넌트 의미가
하나의 단위로 묶여 있다면 styled component로 표현하는 것도 충분히 좋은 선택입니다.
반면 단순한 레이아웃이나 특정 화면에서만 사용하는 스타일이라면 별도의 컴포넌트로 추상화할 필요가 없는 경우가 많습니다.
예를 들어 기존에
const HeaderWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const ContentWrapper = styled.div`
padding: 20px;
`;
처럼 사용했다면,
Emotion에서는 다음처럼 작성할 수 있습니다.
const headerStyle = css`
display: flex;
justify-content: space-between;
align-items: center;
`;
const contentStyle = css`
padding: 20px;
`;
<header css={headerStyle}>
...
</header>
<section css={contentStyle}>
...
</section>
이렇게 하면 JSX에서 실제 HTML 구조를 바로 확인할 수 있습니다.
# Before
HeaderWrapper
↓
실제 header 또는 div
# After
header
↓
css 적용
특히 조건부 스타일에서는 조합 방식의 장점이 더 분명해집니다.
const cardStyle = css`
padding: 16px;
border-radius: 8px;
`;
const selectedStyle = css`
border: 1px solid #000;
`;
const disabledStyle = css`
opacity: 0.5;
pointer-events: none;
`;
<div css={[cardStyle, isSelected && selectedStyle, isDisabled && disabledStyle]} />
각 스타일이 독립된 역할을 가지기 때문에
cardStyle + selectedStyle + disabledStyle
처럼 조합할 수 있습니다.
이 방식은 특정 컴포넌트의 props 내부에 모든 조건을 몰아넣는 것보다 스타일의 역할을 분리하기 쉽고, 여러 컴포넌트에서 같은 조건 스타일을 공유하기에도 유리합니다.
점진적으로 마이그레이션한 이유
스타일링 라이브러리는 애플리케이션 전체에 넓게 퍼져 있습니다.
따라서 한 번에 모든 코드를 변경하는 방식은 위험합니다.
특히 스타일링 코드는 기능 테스트만으로는 문제를 모두 발견하기 어렵습니다.
- 기능은 정상이지만 레이아웃이 깨짐
- 특정 breakpoint에서만 문제 발생
- hover / focus 상태 누락
같은 문제가 생길 수 있기 때문입니다.
따라서 전체 코드를 한 번에 바꾸는 Big Bang Migration보다 점진적 마이그레이션 방식을 선택하는 것이 안전했습니다.
방향을 단순하게 표현하면 다음과 같습니다.
기존 styled-components 유지
↓
신규 코드부터 Emotion 적용
↓
수정 빈도가 높은 영역부터 전환
↓
공통 스타일 패턴 정리
↓
기존 화면 점진적 마이그레이션
이 방식의 가장 큰 장점은 마이그레이션 자체가 기능 개발을 막지 않는다는 것입니다.
기존 코드는 그대로 동작하도록 유지하면서, 새롭게 작성하거나 수정하는 코드부터 새로운 규칙을 적용할 수 있습니다.
또한 마이그레이션 과정에서 스타일 규칙 자체를 검증하고 개선할 수 있습니다.
예를 들어 처음부터 모든 styled component 제거를 목표로 하지 않고,
재사용 가능한 UI 컴포넌트
→ styled 사용 가능
단순 레이아웃
→ css 사용
조건부 스타일
→ css 조합
공통 스타일
→ 독립적인 style fragment
처럼 기준을 만들면 단순한 라이브러리 교체가 아니라 스타일링 구조 개선으로 이어질 수 있습니다.
독립적인 style fragment는 특정 HTML 태그나 컴포넌트에 귀속되지 않고, 필요한 곳에 레고 블록처럼 끼워 넣을 수 있는 "순수 스타일 조각(변수)"을 의미합니다.
Before / After로 보는 스타일 구조 변화
마이그레이션 전에는 컴포넌트 자체가 스타일의 단위가 되는 코드가 많았습니다.
const Button = styled.button<{ active?: boolean }>`
padding: 12px 16px;
border-radius: 8px;
background-color: ${({ active }) => (active ? '#000' : '#fff')};
color: ${({ active }) => (active ? '#fff' : '#000')};
`;
export const Filter = () => {
return <Button active={true}>전체</Button>;
};
이 구조에서는 active라는 값이 스타일을 결정하기 위해 컴포넌트의 props가 됩니다.
Emotion의 css 기반 조합으로 변경하면 다음처럼 표현할 수 있습니다.
const buttonStyle = css`
padding: 12px 16px;
border-radius: 8px;
`;
const activeStyle = css`
background-color: #000;
color: #fff;
`;
const inactiveStyle = css`
background-color: #fff;
color: #000;
`;
export const Filter = () => {
const isActive = true;
return <button css={[buttonStyle, isActive ? activeStyle : inactiveStyle]}>전체</button>;
};
스타일의 책임이 다음처럼 명확해집니다.
buttonStyle
→ 공통 스타일
activeStyle
→ 활성 상태
inactiveStyle
→ 비활성 상태
컴포넌트는 어떤 상태인지 결정하고, 스타일 정의는 해당 상태에 어떤 스타일을 적용할지 담당합니다.
이렇게 스타일을 작은 단위로 나누면 같은 스타일을 다른 컴포넌트에서도 재사용하거나 새로운 variant를 추가하기 쉬워집니다.
무엇이 좋아졌을까?
이번 작업에서 가장 큰 변화는 라이브러리 자체보다 스타일을 구성하는 방식이었습니다.
첫 번째는 불필요한 스타일 전용 컴포넌트를 줄일 수 있었다는 점입니다.
# 이전
// JSX를 읽을 때 실제 HTML 구조가 한눈에 들어오지 않음
export const Card = () => {
return (
<Container>
<HeaderWrapper>
<h2>제목</h2>
</HeaderWrapper>
<ButtonWrapper>
<button>확인</button>
</ButtonWrapper>
</Container>
);
};
// 스타일만을 위한 Wrapper 컴포넌트들을 계속 선언
const Container = styled.div`
padding: 24px;
`;
const HeaderWrapper = styled.header`
display: flex;
justify-content: space-between;
`;
const ButtonWrapper = styled.div`
text-align: right;
`;
# 이후
export const Card = () => {
return (
<div css={containerStyle}>
<header css={headerStyle}>
<h2>제목</h2>
</header>
<div css={buttonStyle}>
<button>확인</button>
</div>
</div>
);
}
두 번째는 공통 스타일과 조건부 스타일의 재사용성이 높아졌습니다.
Emotion은 스타일 조합을 공식적으로 지원하며, css로 생성한 스타일을 다른 스타일에 포함하거나 여러 스타일을 조합할 수 있습니다. *Composition
export const Button = ({ isPrimary, isDisabled, children }) => {
return (
<button
css={[buttonBaseStyle, isPrimary && primaryStyle, isDisabled && disabledStyle]}
disabled={isDisabled}
>
{children}
</button>
);
};
// 공통 Base 스타일
const buttonBaseStyle = css`
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
`;
// Variant 스타일
const primaryStyle = css`
background-color: #3182ce;
color: white;
`;
// 조건부 스타일
const disabledStyle = css`
opacity: 0.5;
cursor: not-allowed;
`;
세 번째는 스타일의 책임을 더 작게 나눌 수 있었습니다.
컴포넌트 하나
↓
모든 스타일 조건 처리
에서
Base Style + Variant Style + State Style
↓
필요한 스타일 조합
형태로 바뀌면서 스타일을 읽고 수정하기 쉬워졌습니다.
Runtime CSS-in-JS의 한계와 React Server Components
React Server Components(RSC)가 등장하면서 Runtime CSS-in-JS 방식의 한계도 함께 이야기되고 있습니다.
Emotion과 styled-components는 모두 Runtime CSS-in-JS 라이브러리입니다.
즉, 컴포넌트가 렌더링되는 시점에 스타일을 계산하고, 클래스 이름을 생성한 뒤 브라우저에 스타일을 주입하는 방식으로 동작합니다.
Component Render
↓
Style 계산
↓
ClassName 생성
↓
<style> 태그에 CSS 삽입
↓
브라우저 렌더링
이 방식은 Client Side Rendering(CSR) 환경에서는 매우 유연합니다.
컴포넌트의 Props나 Theme 값에 따라 스타일을 동적으로 생성할 수 있고, JavaScript의 모든 기능을 활용해 스타일을 작성할 수 있기 때문입니다.
하지만 React Server Components 환경에서는 이야기가 조금 달라집니다.
Server Component는 브라우저가 아닌 서버 환경에서 실행되며, 렌더링 결과는 RSC Payload 형태로 전달되고 Next.js의 서버 렌더링 과정에서 HTML 생성에도 활용됩니다.
즉,
- 브라우저의 DOM이 존재하지 않고
- 클라이언트 런타임 기반 스타일 주입 방식을 그대로 사용할 수 없으며
- React Context를 사용하는 Theme Provider 등은 Client Component 경계가 필요할 수 있습니다.
이 때문에 Runtime CSS-in-JS가 사용하는 스타일 생성 및 주입 방식은 React Server Components와 구조적으로 잘 맞지 않습니다.
실제로 Emotion과 styled-components 모두 React Server Components에서는 별도의 설정이나 Client Component('use client') 경계를 통해 사용하는 방식을 안내하고 있습니다.
물론 이것이 Emotion이나 styled-components를 사용할 수 없다는 의미는 아닙니다.
다만 React Server Components가 점점 확대되는 현재 시점에서는 Runtime CSS-in-JS보다 빌드 시점(Build-time)에 CSS를 생성하는 Zero-runtime 방식이 새로운 대안으로 많이 선택되고 있습니다.
최근에는 어떤 선택지가 있을까?
React Server Components가 점점 보편화되면서 최근에는 Runtime CSS-in-JS 대신 Build-time 기반 스타일링 도구도 많이 사용되고 있습니다.
대표적으로 다음과 같은 선택지가 있습니다.
Tailwind CSS
Utility Class 기반의 스타일링 방식입니다.
빌드 시 필요한 클래스만 생성하며, 별도의 Runtime CSS 생성 과정이 없습니다.
또한 React Server Components 환경에서도 별도의 스타일 주입 과정 없이 사용할 수 있습니다.
Panda CSS
Emotion의 Object Style과 비슷한 개발 경험을 제공하면서도 CSS를 빌드 시점에 생성하는 Zero-runtime CSS-in-JS 라이브러리입니다.
Object Style 기반의 개발 경험을 유지하면서 React Server Components와의 호환성도 함께 고려할 수 있는 선택지입니다.
CSS Modules
Next.js를 비롯한 대부분의 프레임워크에서 기본적으로 지원하는 방식입니다.
빌드 시 CSS를 생성하므로 별도의 런타임 스타일 생성 과정이 없고, React Server Components와도 자연스럽게 사용할 수 있습니다.
어떤 방식이 항상 더 좋다고 말할 수는 없습니다.
프로젝트의 규모, 기술 스택, 팀의 개발 방식, 디자인 시스템 구조 등에 따라 가장 적합한 선택은 달라질 수 있습니다.
중요한 것은 "남들이 써서" 혹은 "유행하는 라이브러리라서"가 아니라, 현재 우리 팀이 겪고 있는 문제를 정확히 정의하고 이를 해결해 줄 수 있는 도구를 선택하는 것이 중요합니다.
마치며
styled-components에서도 충분히 좋은 구조를 만들 수 있고, Emotion에서도 규칙 없이 사용하면 동일한 문제가 반복될 수 있습니다.
중요한 것은
- 어떤 경우에 컴포넌트로 추상화할지
- 어떤 경우에 단순한 스타일로 유지할지
- 공통 스타일과 variant를 어떻게 나눌지
- 동적 스타일을 어느 수준까지 CSS-in-JS로 처리할지
에 대한 팀의 기준을 만드는 것입니다.
Emotion으로 전환하면서 css 기반 조합을 적극적으로 사용한 것도 결국 스타일을 작은 단위로 나누고 필요한 곳에서 조합하기 위한 선택이었습니다.
결국 이번 마이그레이션 작업은 단순 라이브러리 교체가 아니라, 스타일을 어떻게 정의하고 조합할 것인지에 대한 팀의 기준을 다시 세운 것에 있었습니다.
기술을 선택할 때 어떤 도구가 더 우수한가 보다 이 도구가 우리 팀의 문제를 어떻게 해결해 줄 수 있는가 를 깊게 고민해 볼 수 있었던 경험이었습니다.