본문 바로가기
카테고리 없음

Styled-components 란

by SuldenLion 2026. 5. 14.
반응형

Styled-components — CSS-in-JS의 선구자

프론트엔드 개발 · 스타일링 & UI 시리즈 #3
컴포넌트와 스타일을 한 파일에서, JavaScript의 모든 힘을 빌려 CSS를 작성하는 패러다임. Styled-components의 철학, 내부 동작, 장단점을 깊이 탐구합니다.

 


1. CSS-in-JS란 무엇인가

CSS-in-JS는 CSS를 별도 파일이 아닌 JavaScript 코드 안에서 작성하는 패러다임입니다. 2014년 Facebook 엔지니어 Christopher Chedeau(@vjeux)의 발표에서 본격적으로 논의되기 시작했습니다.

그가 제시한 CSS의 7가지 근본적 문제는 지금도 유효합니다.

  1. 전역 네임스페이스: 모든 CSS 클래스는 전역 스코프를 공유한다
  2. 의존성 관리: 어떤 CSS가 어떤 컴포넌트에 필요한지 추적하기 어렵다
  3. 데드 코드 제거: 사용하지 않는 CSS를 찾아서 지우기 어렵다
  4. 최소화: 클래스 이름 압축이 어렵다
  5. 상수 공유: JS 변수를 CSS에서 쓰기 어렵다
  6. 비결정적 해결: 로드 순서에 따라 스타일이 달라질 수 있다
  7. 격리: 컴포넌트 간 스타일 충돌을 막기 어렵다

CSS-in-JS는 스타일을 컴포넌트에 완전히 캡슐화함으로써 이 문제들을 해결합니다.


2. Styled-components 소개

Styled-components는 2016년 Glen Maddern과 Max Stoiber가 만든 React 전용 CSS-in-JS 라이브러리입니다. ES6의 Tagged Template Literal 문법을 활용해 컴포넌트 정의와 스타일링을 하나로 통합합니다.

npm install styled-components
import styled from 'styled-components';

// HTML 요소를 확장한 스타일드 컴포넌트
const Button = styled.button`
  padding: .5rem 1.25rem;
  background: #3498db;
  color: white;
  border: none;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
  transition: background .2s;

  &:hover {
    background: #2980b9;
  }
`;

// React 컴포넌트처럼 사용
function App() {
  return <Button>클릭하세요</Button>;
}

3. 핵심 문법 완전 가이드

Props 기반 동적 스타일

JavaScript props를 받아 스타일을 동적으로 변경하는 것이 styled-components 최대의 강점입니다.

const Button = styled.button`
  padding: .5rem 1.5rem;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
  border: 2px solid transparent;
  transition: all .2s;

  /* props에 따른 동적 스타일 */
  background: ${({ variant }) =>
    variant === 'outline' ? 'transparent' :
    variant === 'danger'  ? '#e74c3c' : '#3498db'
  };

  color: ${({ variant }) =>
    variant === 'outline' ? '#3498db' : 'white'
  };

  border-color: ${({ variant }) =>
    variant === 'outline' ? '#3498db' : 'transparent'
  };

  /* size prop */
  font-size: ${({ size }) =>
    size === 'lg' ? '1.1rem' :
    size === 'sm' ? '.8rem'  : '1rem'
  };
`;

// 사용
<Button>기본 버튼</Button>
<Button variant="outline">아웃라인</Button>
<Button variant="danger" size="lg">삭제</Button>

컴포넌트 확장 (Extending)

기존 styled-component를 확장해 새 스타일을 추가합니다.

const BaseButton = styled.button`
  display: inline-flex;
  align-items: center;
  padding: .5rem 1.25rem;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
  transition: all .2s;
`;

// 확장: 기본 스타일 + 추가 스타일
const PrimaryButton = styled(BaseButton)`
  background: #3498db;
  color: white;
  border: none;

  &:hover { background: #2980b9; }
`;

const GhostButton = styled(BaseButton)`
  background: transparent;
  color: #3498db;
  border: 2px solid #3498db;

  &:hover {
    background: #3498db;
    color: white;
  }
`;

as prop — 렌더링 요소 변경

styled-component를 다른 HTML 요소나 React 컴포넌트로 렌더링할 수 있습니다.

const Button = styled.button`
  padding: .5rem 1.25rem;
  background: #3498db;
  color: white;
  border-radius: 6px;
  text-decoration: none;
  display: inline-block;
`;

// <button> 대신 <a> 태그로 렌더링
<Button as="a" href="/dashboard">대시보드 이동</Button>

// React Router Link로 렌더링
<Button as={Link} to="/profile">프로필</Button>

중첩 선택자와 &

SCSS와 유사하게 중첩 선택자를 사용합니다.

const Card = styled.div`
  padding: 1.5rem;
  border-radius: 12px;
  border: 1px solid #e0e0e0;
  transition: all .2s;

  /* 자식 요소 스타일 */
  h2 {
    font-size: 1.2rem;
    margin-bottom: .5rem;
    color: #1a1a2e;
  }

  p {
    color: #666;
    line-height: 1.6;
  }

  /* &는 컴포넌트 자신을 참조 */
  &:hover {
    box-shadow: 0 8px 24px rgba(0,0,0,.1);
    transform: translateY(-2px);
  }

  /* 특정 클래스가 있을 때 */
  &.featured {
    border-color: #3498db;
    border-width: 2px;
  }

  /* 다른 스타일드 컴포넌트를 타겟 */
  ${Button} {
    margin-top: 1rem;
    width: 100%;
  }
`;

createGlobalStyle — 전역 스타일

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  body {
    font-family: 'Pretendard', -apple-system, sans-serif;
    background: ${({ theme }) => theme.colors.background};
    color: ${({ theme }) => theme.colors.text};
    line-height: 1.6;
  }

  a {
    color: inherit;
    text-decoration: none;
  }
`;

// App.jsx
function App() {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <Router>...</Router>
    </ThemeProvider>
  );
}

keyframes — 애니메이션

import styled, { keyframes } from 'styled-components';

const fadeInUp = keyframes`
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;

const Modal = styled.div`
  animation: ${fadeInUp} .3s ease-out;
  background: white;
  padding: 2rem;
  border-radius: 12px;
`;

4. 내부 동작 원리

Styled-components가 어떻게 동작하는지 이해하면 디버깅과 성능 최적화에 도움이 됩니다.

Tagged Template Literal 파싱

styled.button`
  color: ${props => props.primary ? 'white' : '#333'};
  background: ${props => props.primary ? '#3498db' : 'transparent'};
`;

JavaScript 엔진이 이 코드를 파싱하면, styled-components는 정적 문자열 배열과 동적 interpolation 배열을 받아 처리합니다.

런타임 클래스 생성

  1. 컴포넌트가 렌더링될 때, props를 기반으로 CSS 문자열 생성
  2. CSS 문자열을 해시해 고유한 클래스명 생성 (예: sc-bdXxxt)
  3. <style> 태그에 해당 CSS를 삽입
  4. 컴포넌트에 생성된 클래스명을 적용
<!-- 런타임에 주입되는 스타일 -->
<style data-styled="active">
  .sc-bdXxxt { padding: .5rem 1.25rem; border-radius: 6px; }
  .bPNpEe { background: #3498db; color: white; }
  .fJMzMO { background: transparent; color: #3498db; }
</style>

<!-- 렌더링된 HTML -->
<button class="sc-bdXxxt bPNpEe">Primary</button>
<button class="sc-bdXxxt fJMzMO">Ghost</button>

Babel 플러그인 (권장)

개발 환경에서 디버깅을 쉽게 하기 위해 babel-plugin-styled-components를 사용합니다. 컴포넌트 이름이 클래스명에 포함되어 DevTools에서 확인하기 쉬워집니다.

npm install --save-dev babel-plugin-styled-components
// .babelrc
{
  "plugins": [
    ["babel-plugin-styled-components", {
      "displayName": true,
      "fileName": true
    }]
  ]
}

5. 테마 시스템 구축

ThemeProvider를 활용한 전역 테마 시스템은 styled-components의 핵심 기능 중 하나입니다.

// theme.js
export const lightTheme = {
  colors: {
    primary:    '#3498db',
    secondary:  '#2ecc71',
    background: '#ffffff',
    surface:    '#f8f9fa',
    text:       '#1a1a2e',
    textMuted:  '#6c757d',
    border:     '#dee2e6',
    danger:     '#e74c3c',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '48px',
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px',
    full: '9999px',
  },
  typography: {
    fontFamily: "'Pretendard', -apple-system, sans-serif",
    fontSizes: {
      sm: '.875rem',
      md: '1rem',
      lg: '1.125rem',
      xl: '1.5rem',
    },
  },
};

export const darkTheme = {
  ...lightTheme,
  colors: {
    ...lightTheme.colors,
    background: '#0f1923',
    surface:    '#1a2535',
    text:       '#e8eaf0',
    textMuted:  '#9aa3b0',
    border:     '#2a3545',
  },
};
// App.jsx — ThemeProvider 적용
import { ThemeProvider } from 'styled-components';
import { useState } from 'react';

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

  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <GlobalStyle />
      <Layout>
        <button onClick={() => setIsDark(d => !d)}>
          {isDark ? '라이트 모드' : '다크 모드'}
        </button>
        {/* ... */}
      </Layout>
    </ThemeProvider>
  );
}
// 컴포넌트에서 테마 사용
const Card = styled.div`
  background: ${({ theme }) => theme.colors.surface};
  border: 1px solid ${({ theme }) => theme.colors.border};
  border-radius: ${({ theme }) => theme.borderRadius.lg};
  padding: ${({ theme }) => theme.spacing.lg};
  color: ${({ theme }) => theme.colors.text};
`;

// useTheme 훅으로도 접근 가능
import { useTheme } from 'styled-components';

function Component() {
  const theme = useTheme();
  return <div style={{ color: theme.colors.primary }}>...</div>;
}

6. 성능 이슈와 해결책

Styled-components는 런타임에 스타일을 생성하기 때문에 성능 이슈가 발생할 수 있습니다.

주요 성능 문제

CSS 직렬화 비용: 컴포넌트가 렌더링될 때마다 CSS 문자열을 생성하고 파싱합니다.

스타일 주입 비용: <style> 태그에 동적으로 CSS를 삽입하는 작업이 매 렌더링마다 발생할 수 있습니다.

SSR 이슈: 서버에서 렌더링 시 스타일 수집과 클라이언트 hydration 과정이 복잡합니다.

해결책

1. .attrs() 메서드로 prop 최소화

// Bad: 렌더링마다 새 class 생성 가능성
const Input = styled.input`
  width: ${({ width }) => width}px;
`;
<Input width={300} />

// Good: .attrs()로 static attrs 분리
const Input = styled.input.attrs(props => ({
  type: props.type || 'text',
}))`
  border: 1px solid #ddd;
  border-radius: 6px;
  padding: .5rem .75rem;
`;

2. 동적 스타일은 CSS 변수로

// Bad: props마다 새 클래스 생성
const Box = styled.div`
  width: ${({ width }) => width}px;
  height: ${({ height }) => height}px;
`;

// Good: CSS 변수 활용으로 단일 클래스 유지
const Box = styled.div`
  width: var(--box-width);
  height: var(--box-height);
`;
<Box style={{ '--box-width': '300px', '--box-height': '200px' }} />

3. SSR 설정

// Next.js _document.jsx
import Document from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: [initialProps.styles, sheet.getStyleElement()],
      };
    } finally {
      sheet.seal();
    }
  }
}

7. 장단점 정리

장점

  • 완전한 컴포넌트 캡슐화: 스타일이 컴포넌트와 함께 존재해 응집도가 높음
  • 동적 스타일링: props와 JS 로직으로 강력하게 스타일 제어 가능
  • 자동 CSS 스코핑: 전역 네임스페이스 충돌 없음
  • TypeScript 지원: props 타입 정의로 스타일 타입 안전성 확보
  • 데드 코드 제거: 컴포넌트와 스타일이 같은 파일, 삭제 시 함께 제거
  • 테마 시스템: ThemeProvider로 전역 디자인 토큰 관리

단점

  • 런타임 오버헤드: 스타일 생성과 주입이 런타임에 발생
  • 번들 크기: 라이브러리 자체가 ~12KB (gzipped)
  • SSR 복잡성: 서버 사이드 렌더링 설정이 번거로움
  • CSS 표준과의 거리: 일반 CSS 도구(lint, browser DevTools) 활용 제한적
  • React 전용: Vue, Svelte 등에서 사용 불가 (Emotion은 가능)
  • 새로운 패러다임 학습: 팀 전체의 학습 비용

8. 결론 — 지금도 좋은 선택인가?

Styled-components는 CSS-in-JS 패러다임을 대중화시킨 역사적인 라이브러리입니다. 그러나 2023년 이후 트렌드는 런타임 CSS-in-JS에서 빌드 타임 솔루션으로 이동하고 있습니다.

프로젝트 상황 권장 선택

기존 styled-components 프로젝트 유지 (마이그레이션 비용 큼)
React SPA, 성능 중요도 낮음 Styled-components 또는 Emotion
Next.js App Router Tailwind CSS 또는 CSS Modules
성능 민감한 SSR 앱 CSS Modules, Tailwind, Vanilla Extract
강력한 동적 스타일 필요 Styled-components 또는 Emotion

한 줄 요약: CSS-in-JS의 철학과 동적 스타일링의 힘을 배우기에 최고의 라이브러리이지만, 새 프로젝트에서는 런타임 오버헤드와 SSR 복잡성을 감수할 만한 이유가 있는지 먼저 따져보세요.


 

반응형

댓글