본문으로 건너뛰기
Previous
Next
Emotion CSS-in-JS Complete Guide

Emotion CSS-in-JS Complete Guide

Emotion CSS-in-JS Complete Guide

이 글의 핵심

Emotion lets you write CSS directly in JavaScript with full TypeScript support, theming, and dynamic styles. This guide covers both the css prop and styled API, plus theming, SSR, and when to use Emotion vs Tailwind CSS.

Why Emotion?

Emotion lets you colocate styles with components, use JavaScript variables in CSS, and get TypeScript autocomplete for theme values:

// Before: separate CSS file
// styles.module.css → .button { ... }
// component.tsx → className="button"

// After: Emotion
const Button = styled.button`
  background: ${theme.colors.primary};
  padding: ${theme.spacing(2)};
  &:hover { background: ${theme.colors.primaryDark}; }
`

Installation

# Core packages
npm install @emotion/react @emotion/styled

# Optional: babel plugin for css prop (without pragma comment)
npm install --save-dev @emotion/babel-plugin

1. Two APIs

Emotion has two main APIs:

@emotion/styled — Like styled-components

import styled from '@emotion/styled'

const Button = styled.button`
  background: cornflowerblue;
  color: white;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background: royalblue;
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`

// Usage
<Button onClick={handleClick}>Click Me</Button>
<Button disabled>Disabled</Button>

@emotion/react — css prop

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'

const buttonStyles = css`
  background: cornflowerblue;
  color: white;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
`

// css prop works on any JSX element
function App() {
  return (
    <button css={buttonStyles}>Click Me</button>
  )
}

2. Dynamic Styles

import styled from '@emotion/styled'

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  fullWidth?: boolean
}

const Button = styled.button<ButtonProps>`
  border: none;
  border-radius: 4px;
  cursor: pointer;
  width: ${({ fullWidth }) => fullWidth ? '100%' : 'auto'};

  /* Variant styles */
  ${({ variant = 'primary' }) => {
    const variants = {
      primary: `background: cornflowerblue; color: white;`,
      secondary: `background: transparent; color: cornflowerblue; border: 1px solid currentColor;`,
      danger: `background: #e53e3e; color: white;`,
    }
    return variants[variant]
  }}

  /* Size styles */
  ${({ size = 'md' }) => {
    const sizes = {
      sm: `padding: 4px 8px; font-size: 12px;`,
      md: `padding: 8px 16px; font-size: 14px;`,
      lg: `padding: 12px 24px; font-size: 16px;`,
    }
    return sizes[size]
  }}
`

// Usage
<Button variant="primary" size="lg">Submit</Button>
<Button variant="danger" fullWidth>Delete Account</Button>

3. Theming

import { ThemeProvider, useTheme } from '@emotion/react'
import styled from '@emotion/styled'

// Define theme type
declare module '@emotion/react' {
  export interface Theme {
    colors: {
      primary: string
      primaryDark: string
      background: string
      text: string
      border: string
    }
    spacing: (n: number) => string
    borderRadius: string
    shadows: {
      sm: string
      md: string
    }
  }
}

const lightTheme = {
  colors: {
    primary: '#4A90E2',
    primaryDark: '#357ABD',
    background: '#ffffff',
    text: '#1a1a1a',
    border: '#e2e8f0',
  },
  spacing: (n: number) => `${n * 8}px`,
  borderRadius: '6px',
  shadows: {
    sm: '0 1px 3px rgba(0,0,0,0.12)',
    md: '0 4px 6px rgba(0,0,0,0.1)',
  },
}

const darkTheme = {
  ...lightTheme,
  colors: {
    primary: '#63B3ED',
    primaryDark: '#4299E1',
    background: '#1a202c',
    text: '#e2e8f0',
    border: '#2d3748',
  },
}

// Theme-aware styled component
const Card = styled.div`
  background: ${({ theme }) => theme.colors.background};
  color: ${({ theme }) => theme.colors.text};
  border: 1px solid ${({ theme }) => theme.colors.border};
  border-radius: ${({ theme }) => theme.borderRadius};
  padding: ${({ theme }) => theme.spacing(3)};
  box-shadow: ${({ theme }) => theme.shadows.sm};
`

const PrimaryButton = styled.button`
  background: ${({ theme }) => theme.colors.primary};
  color: white;
  padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
  border: none;
  border-radius: ${({ theme }) => theme.borderRadius};

  &:hover {
    background: ${({ theme }) => theme.colors.primaryDark};
  }
`

// useTheme hook in function components
function Header() {
  const theme = useTheme()
  return (
    <header css={{ background: theme.colors.primary, padding: theme.spacing(2) }}>
      Logo
    </header>
  )
}

// App root
function App() {
  const [isDark, setIsDark] = useState(false)

  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <Card>
        <PrimaryButton onClick={() => setIsDark(!isDark)}>
          Toggle Theme
        </PrimaryButton>
      </Card>
    </ThemeProvider>
  )
}

4. Composition

import { css } from '@emotion/react'
import styled from '@emotion/styled'

// Reusable style fragments
const flexCenter = css`
  display: flex;
  align-items: center;
  justify-content: center;
`

const truncate = css`
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
`

// Compose multiple styles
const Card = styled.div`
  ${flexCenter}
  padding: 16px;
  border-radius: 8px;
`

// Extend another styled component
const HighlightedCard = styled(Card)`
  border: 2px solid cornflowerblue;
  background: rgba(100, 149, 237, 0.1);
`

// Compose with css prop
function UserName({ children }: { children: string }) {
  return (
    <span css={[truncate, { maxWidth: '200px', display: 'block' }]}>
      {children}
    </span>
  )
}

5. Keyframe Animations

import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const fadeIn = keyframes`
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: translateY(0); }
`

const spin = keyframes`
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
`

const pulse = keyframes`
  0%, 100% { transform: scale(1); }
  50%       { transform: scale(1.05); }
`

const FadeInDiv = styled.div`
  animation: ${fadeIn} 0.3s ease-out;
`

const Spinner = styled.div`
  width: 24px;
  height: 24px;
  border: 2px solid #e2e8f0;
  border-top-color: cornflowerblue;
  border-radius: 50%;
  animation: ${spin} 0.8s linear infinite;
`

const PulseButton = styled.button`
  animation: ${pulse} 2s ease-in-out infinite;
`

6. Global Styles

import { Global, css } from '@emotion/react'

const globalStyles = css`
  *, *::before, *::after {
    box-sizing: border-box;
  }

  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    -webkit-font-smoothing: antialiased;
  }

  h1, h2, h3, h4, h5, h6 {
    margin: 0 0 1rem;
    line-height: 1.2;
  }

  a {
    color: cornflowerblue;
    text-decoration: none;
    &:hover { text-decoration: underline; }
  }
`

function App() {
  return (
    <>
      <Global styles={globalStyles} />
      {/* rest of app */}
    </>
  )
}

7. Server-Side Rendering (Next.js)

// pages/_document.tsx (Pages Router)
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'
import Document, { Html, Head, Main, NextScript } from 'next/document'

export default function MyDocument({ emotionStyleTags }) {
  return (
    <Html>
      <Head>{emotionStyleTags}</Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

MyDocument.getInitialProps = async (ctx) => {
  const cache = createCache({ key: 'css' })
  const { extractCriticalToChunks } = createEmotionServer(cache)

  const initialProps = await Document.getInitialProps(ctx)
  const emotionStyles = extractCriticalToChunks(initialProps.html)
  const emotionStyleTags = emotionStyles.styles.map(style => (
    <style
      data-emotion={`${style.key} ${style.ids.join(' ')}`}
      key={style.key}
      dangerouslySetInnerHTML={{ __html: style.css }}
    />
  ))

  return { ...initialProps, emotionStyleTags }
}

Emotion vs Tailwind CSS vs styled-components

EmotionTailwind CSSstyled-components
BundleSmall (runtime)Zero runtimeSimilar to Emotion
Dynamic stylesEasyLimited (JIT)Easy
ThemingBuilt-inConfig-basedBuilt-in
TypeScriptExcellentGoodGood
Learning curveLow (CSS syntax)Medium (utility classes)Low
PerformanceGoodBest (no runtime)Good
SSRSupportedNo issueSupported
Best forDesign systems, themingRapid UI, utility-firstComponent libraries

Key Takeaways

  • Two APIs: styled (component-based) and css prop (inline, needs pragma)
  • Dynamic styles: use props in template literals for variant-based styling
  • Theming: ThemeProvider + typed theme + useTheme hook — full autocomplete
  • Composition: combine css fragments with arrays or ${fragment} in template literals
  • SSR: use @emotion/server for Next.js; required for avoiding FOUC
  • When to use: complex theming, design systems, programmatic styles — use Tailwind for utility-first rapid development

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Style React apps with Emotion. Covers the css prop, styled components, theming, keyframes, global styles, server-side re… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [React Hooks Deep Dive | useEffect· useMemo](/en/blog/react-hooks-deep-dive/
  • [Next.js Performance Optimization Guide | Core Web Vitals](/en/blog/nextjs-performance-optimization-guide/
  • [TypeScript 5 Complete Guide | Decorators· satisfies](/en/blog/typescript-5-complete-guide-en/

이 글에서 다루는 키워드 (관련 검색어)

Emotion, CSS-in-JS, React, Styling, TypeScript, Frontend 등으로 검색하시면 이 글이 도움이 됩니다.