본문 바로가기

Styled-components 기초

styled-components로 CSS를 컴포넌트에 결합하고 props 기반 동적 스타일·태그 변경·스타일 확장을 구현합니다.

우리는 CSS 모듈을 통해 클래스 이름 충돌 문제를 해결하고 컴포넌트 단위 스타일링을 구현하는 방법을 배웠습니다.

이제 5장 스타일링과 CSS의 세 번째 방법으로, CSS-in-JS의 대표 라이브러리 중 하나인 Styled-components를 소개합니다.

Styled-components는 자바스크립트 코드 안에서 CSS를 작성하고, 이를 컴포넌트에 직접 연결해 사용하는 방식입니다.

이 방식은 스타일 정의를 컴포넌트 코드 가까이에 둘 수 있게 합니다. 다만 결과는 브라우저가 처리하는 CSS와 class이므로, CSS 캐스케이드와 DOM 속성 계약까지 사라지는 것은 아닙니다.


CSS-in-JS란?

CSS-in-JS는 말 그대로 JavaScript 코드 안에 CSS를 작성하는 방식을 통칭합니다.

기존에는 CSS 파일을 별도로 분리하여 관리했지만, CSS-in-JS는 컴포넌트 로직과 스타일 로직을 하나의 JavaScript 파일 안에서 함께 작성합니다.

Styled-components의 핵심 아이디어
  • 컴포넌트 기반 스타일링: 스타일이 적용된 리액트 컴포넌트를 생성합니다.
  • 클래스 이름 자동 생성: 라이브러리가 스타일 규칙과 연결되는 class를 생성합니다. 생성 문자열의 구체적인 모양을 애플리케이션 계약으로 사용해서는 안 됩니다.
  • 동적 스타일링 용이: 보간 함수에서 props와 theme을 읽어 스타일을 유연하게 변경할 수 있습니다. state 값도 prop으로 전달해 사용할 수 있습니다.
  • 컴포넌트 중심의 이름과 규칙: 스타일을 이름 있는 컴포넌트로 묶어 재사용합니다. 그러나 전역 CSS, 전달한 className, 선택자 우선순위, 소스 순서, 상속은 여전히 일반 CSS 캐스케이드에 참여합니다.

Styled-components 설치

Styled-components를 사용하기 위해서는 먼저 라이브러리를 설치해야 합니다.

터미널에서 다음 명령어를 실행하세요.

npm install styled-components
# 또는
yarn add styled-components

Styled-components 기본 사용법

Styled-components는 태그드 템플릿 리터럴(Tagged Template Literals) 문법을 사용하여 스타일이 적용된 컴포넌트를 생성합니다.

import styled from 'styled-components'; // styled 객체 임포트

// 1. styled.{HTML 태그} 백틱(``) 안에 CSS 작성
const StyledButton = styled.button`
  background-color: #61dafb;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1em;
  transition: all 0.3s ease-in-out;

  &:hover { /* 가상 클래스도 바로 사용 가능 */
    background-color: #21a1f1;
    transform: translateY(-2px);
  }

  &:active {
    transform: translateY(0);
  }
`;

function MyComponent() {
  return (
    // 2. 스타일링된 컴포넌트 사용
    <StyledButton>클릭하세요</StyledButton>
  );
}

예제: 기본적인 스타일링 컴포넌트 만들기

src/components/StyledComponentsBasic.js 파일을 생성하고 다음 코드를 작성하세요.

src/components/StyledComponentsBasic.js
import React from 'react';
import styled from 'styled-components'; // styled-components 임포트

// 1. 스타일이 적용된 <Container> 컴포넌트 생성
const Container = styled.div`
  background-color: #f0f8ff; /* 연한 하늘색 배경 */
  padding: 30px;
  border-radius: 10px;
  border: 1px solid #dbeafe;
  text-align: center;
  margin-bottom: 30px;
`;

// 2. 스타일이 적용된 <Title> 컴포넌트 생성
const Title = styled.h2`
  color: #1e90ff; /* 진한 파란색 */
  font-size: 2.5em;
  margin-bottom: 15px;
  text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
`;

// 3. 스타일이 적용된 <Paragraph> 컴포넌트 생성
const Paragraph = styled.p`
  color: #4682b4; /* 중간 파란색 */
  font-size: 1.2em;
  line-height: 1.7;
  margin-bottom: 20px;
`;

// 4. 스타일이 적용된 <StyledButton> 컴포넌트 생성
const StyledButton = styled.button`
  background-color: #007bff;
  color: white;
  padding: 12px 25px;
  border: none;
  border-radius: 7px;
  cursor: pointer;
  font-size: 1.1em;
  font-weight: bold;
  transition: background-color 0.3s ease, transform 0.2s ease;

  &:hover { /* 가상 클래스 */
    background-color: #0056b3;
    transform: translateY(-2px);
  }

  &:active {
    transform: translateY(0);
  }
`;

function StyledComponentsBasic() {
  return (
    <Container> {/* 생성한 Styled 컴포넌트 사용 */}
      <Title>Styled-components 기초 예제</Title>
      <Paragraph>
        이것은 styled-components로 스타일링된 단락입니다.
        스타일 정의가 컴포넌트 코드 가까이에 있습니다.
      </Paragraph>
      <StyledButton>Styled 버튼</StyledButton>
    </Container>
  );
}

export default StyledComponentsBasic;

App.js에서 렌더링

App.js 파일을 수정하여 StyledComponentsBasic 컴포넌트를 렌더링하세요.

src/App.js
import React from 'react';
import './index.css'; // 전역 스타일 유지
import StyledComponentsBasic from './components/StyledComponentsBasic';
// import CssModulesExample from './components/CssModulesExample'; // 필요 없으면 주석 처리 또는 삭제

function App() {
  return (
    <div className="App">
      <h1>Styled-components 학습</h1>
      <StyledComponentsBasic />
    </div>
  );
}

export default App;

브라우저에서 개발자 도구(F12)를 열어 엘리먼트 탭을 확인해 보세요.

각 HTML 요소에는 styled-components가 스타일 규칙과 연결하기 위해 만든 class 토큰이 붙습니다. 그 토큰의 정확한 문자열 형식은 버전과 빌드 설정에 따라 달라질 수 있으므로 코드에서 직접 참조하지 않습니다.


Props를 이용한 동적 스타일링

Styled-components의 가장 큰 장점 중 하나는 props를 이용하여 스타일을 동적으로 변경할 수 있다는 것입니다.

태그드 템플릿 리터럴 내에서 JavaScript 코드를 삽입할 수 있는 것처럼, props를 인자로 받는 함수를 작성하여 동적인 스타일을 적용할 수 있습니다.

src/components/DynamicStyledButton.js
import React from 'react';
import styled from 'styled-components';

// $로 시작하는 transient prop은 스타일 계산에는 쓰이지만 DOM에는 전달되지 않습니다.
const DynamicButton = styled.button`
  background-color: ${props => props.$primary ? '#007bff' : '#6c757d'}; /* (1) $primary에 따라 배경색 변경 */
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1em;
  margin: 5px;
  transition: background-color 0.3s ease;

  &:hover {
    background-color: ${props => props.$primary ? '#0056b3' : '#5a6268'};
  }
`;

// props에 따라 크기가 변하는 텍스트
const ResizableText = styled.p`
  font-size: ${props => props.$size || '1em'}; /* (2) $size에 따라 폰트 크기 변경 */
  color: ${props => props.$color || '#333'}; /* (3) $color에 따라 색상 변경 */
  margin-top: 15px;
`;

function DynamicStyledComponents() {
  return (
    <div style={{ textAlign: 'center', margin: '20px', padding: '20px', border: '1px dashed #ccc', borderRadius: '8px' }}>
      <h2>동적 스타일링 예제</h2>
      <DynamicButton $primary>기본 버튼</DynamicButton>
      <DynamicButton>보조 버튼</DynamicButton>

      <ResizableText $size="1.5em" $color="#d9534f">크고 빨간 텍스트</ResizableText>
      <ResizableText $size="0.8em" $color="#5cb85c">작고 초록 텍스트</ResizableText>
    </div>
  );
}

export default DynamicStyledComponents;

App.js에 DynamicStyledComponents 컴포넌트를 추가하고 실행해 보세요.

$primary, $size, $color와 같은 props를 전달하여 컴포넌트의 스타일이 동적으로 변경되는 것을 확인할 수 있습니다.

이 방식은 인라인 스타일링처럼 동적 값을 다루면서도 CSS 문법을 함께 사용할 수 있다는 장점을 가집니다.

$ 접두사가 붙은 transient prop은 보간 함수에서 읽되 최종 target으로 전달하지 않는 스타일 전용 값입니다. styled-components v6 계열은 v5의 기본 자동 prop 필터를 제공하지 않으므로, 일반 이름의 스타일 전용 prop을 그대로 두면 target까지 전달될 수 있습니다. 이름만으로 충분하지 않은 조합에서는 withConfig({ shouldForwardProp })로 전달 여부를 명시합니다. 반면 type, aria-label처럼 실제 HTML 요소가 이해하는 속성은 DOM까지 전달해야 합니다. styled(MyComponent)처럼 사용자 컴포넌트를 감쌀 때는 그 컴포넌트가 받은 className을 실제 DOM 요소에 연결해야 생성된 스타일이 적용됩니다.

스타일 보간과 DOM prop 전달은 서로 다른 경로입니다

STYLED-COMPONENTS · TWO CONTRACTS

스타일 보간과 DOM prop 전달은 서로 다른 경로입니다

보간 함수는 props와 theme으로 CSS 규칙을 계산합니다. 스타일 전용 transient prop은 target으로 보내지 않고, 필요한 DOM 속성은 별도로 전달합니다.

본문 DynamicButton · $primary의 두 경로

$primary=true → background: #007bff
$primary 없음 → background: #6c757d
CSS·class로 적용 / DOM $primary 속성 없음

v6에는 v5의 기본 자동 prop 필터가 없습니다. 일반 이름의 스타일 prop은 shouldForwardProp으로 명시적으로 걸러야 할 수 있습니다.

styled-components 보간과 prop 전달 경로보간이 생성 CSS와 class를 만드는 경로와 transient/config가 props 전달을 걸러 내는 경로는 병렬입니다. 실제 DOM에서 class와 허용 속성이 결합됩니다. 사용자 target은 className을 최종 요소에 연결해야 합니다. JSX props$primary·typearia-labelstyled 평가보간 입력·attrsCSS와 class$primary로 색 계산prop 전달transient 제외필요시 config 필터최종 DOMclass + 표준 속성사용자 target은className 전달
styled-components 보간과 prop 전달 경로보간이 생성 CSS와 class를 만드는 경로와 transient/config가 props 전달을 걸러 내는 경로는 병렬입니다. 실제 DOM에서 class와 허용 속성이 결합됩니다. 사용자 target은 className을 최종 요소에 연결해야 합니다. JSX props$primary·typearia-labelstyled 평가보간 입력·attrsCSS와 class$primary로 색 계산prop 전달transient 제외필요시 config 필터최종 DOMclass + 표준 속성사용자 target은className 전달

보간이 생성 CSS와 class를 만드는 경로와 transient/config가 props 전달을 걸러 내는 경로는 병렬입니다. 실제 DOM에서 class와 허용 속성이 결합됩니다. 사용자 target은 className을 최종 요소에 연결해야 합니다.

단순 HTML target
표준 HTML·ARIA·data 속성은 전달한다. 스타일 전용 값은 $ 접두사로 분리한다.
사용자 컴포넌트 target
컴포넌트가 props를 받고, 생성 스타일을 적용할 DOM 요소에 className을 전달해야 한다.
CSS 경계
전역 CSS, 추가 className, 상속, 우선순위와 소스 순서는 생성 규칙에도 정상적으로 작동한다.

v6에는 v5의 기본 자동 prop 필터가 없다. 스타일을 가까이 정의해도 완전한 격리막이 생기지는 않으므로 보간 경로와 prop 전달 경로를 각각 검토한다.


as prop을 이용한 태그 변경

Styled-components로 생성된 컴포넌트는 as prop을 사용하여 렌더링될 HTML 태그를 변경할 수 있습니다.

이는 동일한 스타일을 유지하면서 다른 시맨틱 태그를 사용해야 할 때 유용합니다.

src/components/AsPropExample.js
import React from 'react';
import styled from 'styled-components';

const ButtonLike = styled.button`
  background-color: #ffc107;
  color: #333;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1em;
  margin: 5px;
  transition: background-color 0.3s ease;

  &:hover {
    background-color: #e0a800;
  }
`;

function AsPropExample() {
  return (
    <div style={{ textAlign: 'center', margin: '20px', padding: '20px', border: '1px dashed #ccc', borderRadius: '8px' }}>
      <h2>'as' prop 예제</h2>
      <ButtonLike>나는 button 태그입니다</ButtonLike>
      {/* 같은 스타일로 실제 탐색 링크를 렌더링 */}
      <ButtonLike as="a" href="/docs">문서로 이동</ButtonLike>
    </div>
  );
}

export default AsPropExample;

App.js에 AsPropExample 컴포넌트를 추가하여 실행해 보세요.

두 요소는 같은 스타일을 가지지만 개발자 도구에서 서로 다른 HTML 태그로 렌더링됩니다. as는 스타일을 유지하며 target을 바꿀 뿐, 접근성 의미를 자동으로 보정하지는 않습니다. 동작에는 button, 탐색에는 유효한 href가 있는 a처럼 목적에 맞는 요소와 속성을 함께 선택해야 합니다.


다른 컴포넌트 확장하기

기존 컴포넌트를 감싸 공통 스타일 계약을 재사용하고 새로운 규칙을 추가할 수 있습니다.

src/components/ExtendedStyledComponent.js
import React from 'react';
import styled from 'styled-components';

const BaseButton = styled.button`
  padding: 10px 20px;
  font-size: 1em;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s ease, color 0.3s ease;
`;

// BaseButton을 확장하여 새로운 규칙 추가
const PrimaryButton = styled(BaseButton)`
  background-color: #28a745;
  color: white;
  border: 1px solid #28a745;

  &:hover {
    background-color: #218838;
    border-color: #218838;
  }
`;

// BaseButton을 확장하여 또 다른 규칙 추가
const DangerButton = styled(BaseButton)`
  background-color: #dc3545;
  color: white;
  border: 1px solid #dc3545;

  &:hover {
    background-color: #c82333;
    border-color: #c82333;
  }
`;

function ExtendedStyledComponent() {
  return (
    <div style={{ textAlign: 'center', margin: '20px', padding: '20px', border: '1px dashed #ccc', borderRadius: '8px' }}>
      <h2>컴포넌트 확장 예제</h2>
      <BaseButton>기본 버튼</BaseButton>
      <PrimaryButton style={{ marginLeft: '10px' }}>확장된 기본 버튼</PrimaryButton>
      <DangerButton style={{ marginLeft: '10px' }}>확장된 위험 버튼</DangerButton>
    </div>
  );
}

export default ExtendedStyledComponent;

App.js에 ExtendedStyledComponent 컴포넌트를 추가하여 실행해 보세요.

BaseButton의 기본 스타일을 재사용하면서 각각 다른 색상의 버튼을 만들 수 있습니다. 이때 styled(Base)는 JavaScript 클래스 상속이 아니라 스타일 가능한 React 컴포넌트를 감싸는 조합입니다. 사용자 정의 Base라면 className을 받아 최종 DOM 요소에 전달해야 하며, 기본 규칙과 추가 규칙은 일반 CSS 캐스케이드로 함께 계산됩니다.

.attrs(...)는 모든 인스턴스에 필요한 type="button" 같은 정적·계산 속성의 기본값을 붙일 때 사용합니다. ThemeProvider의 theme이나 보간 함수가 만든 규칙도 CSS로 출력된 뒤에는 전역 규칙, 사용자 className, 상속, 우선순위, 소스 순서의 영향을 받습니다. 따라서 “컴포넌트 가까이 정의된다”와 “스타일이 완전히 격리된다”를 같은 뜻으로 이해하면 안 됩니다.

아래 다이어그램은 렌더할 target만 바꾸는 as와, 새 이름·규칙을 가진 변형을 만드는 styled(Base)의 선택 경계를 정리합니다.

as는 렌더 target을 바꾸고 styled(Base)는 변형을 만듭니다

STYLED-COMPONENTS · TARGET & VARIANT

as는 렌더 target을 바꾸고 styled(Base)는 변형을 만듭니다

같은 시각 규칙으로 이번 요소의 의미를 바꾸는 경우와, Base 계약에 새 CSS를 더해 재사용할 컴포넌트를 정의하는 경우를 구분합니다.

본문 두 예제 · target과 새 이름

ButtonLike as="a" href="/docs"
→ 탐색 링크, 기존 스타일 유지
PrimaryButton = styled(BaseButton)
→ 공통 규칙 + 새 색상 규칙

as는 유효한 href나 접근성 동작을 자동으로 만들지 않습니다. styled(Base)는 JavaScript 클래스 상속이 아니라 컴포넌트와 스타일의 조합입니다.

as와 styled Base 선택 흐름예 분기는 선택한 도구로 끝나며 아니오는 다음 판단으로 갑니다. as는 같은 스타일의 렌더 target을 바꾸고, styled(Base)는 재사용할 변형 규칙을 정의합니다. 목적에 필요한 href와 DOM 의미는 별도 계약입니다. 설계할 역할바꾸려는 계약 확인같은 규칙에서target만 변경?재사용 Base와새 규칙이 있는가?as이번 렌더 target 변경필요한 속성 함께styled(Base)새 이름의 변형className 연결 유지새 styled 요소목적에 맞는 target예아니오예아니오
as와 styled Base 선택 흐름예 분기는 선택한 도구로 끝나며 아니오는 다음 판단으로 갑니다. as는 같은 스타일의 렌더 target을 바꾸고, styled(Base)는 재사용할 변형 규칙을 정의합니다. 목적에 필요한 href와 DOM 의미는 별도 계약입니다. 설계할 역할바꾸려는 계약 확인같은 규칙에서target만 변경?재사용 Base와새 규칙이 있는가?as이번 렌더 target 변경필요한 속성 함께styled(Base)새 이름의 변형className 연결 유지새 styled 요소목적에 맞는 target예아니오예아니오

예 분기는 선택한 도구로 끝나며 아니오는 다음 판단으로 갑니다. as는 같은 스타일의 렌더 target을 바꾸고, styled(Base)는 재사용할 변형 규칙을 정의합니다. 목적에 필요한 href와 DOM 의미는 별도 계약입니다.

as의 책임
스타일은 유지하지만 의미를 자동 보정하지 않는다. 동작은 button, 탐색은 유효한 href가 있는 a를 고른다.
styled(Base)의 책임
Base의 컴포넌트 계약을 감싸고 CSS 규칙을 추가한다. 추가 규칙은 기본 규칙과 캐스케이드한다.
.attrs와 theme
attrs는 공통 속성·기본값을, theme은 보간 입력을 제공한다. 어느 쪽도 잘못 고른 HTML 의미를 대신 고쳐 주지 않는다.

같은 모양은 같은 의미를 보장하지 않는다. target의 시맨틱 계약과 스타일 조합 계약을 별도로 검토한다.


Styled-components 기초는 여기까지입니다.

이 장에서는 Styled-components의 기본적인 설치와 사용법, props를 이용한 동적 스타일링, as prop을 이용한 태그 변경, 그리고 기존 컴포넌트를 확장하는 방법까지 상세하게 다루었습니다.

Styled-components는 스타일을 컴포넌트 로직과 함께 관리하고, JavaScript 값을 활용하여 동적인 스타일링을 유연하게 구현할 수 있게 해줍니다.

이 방식의 핵심은 스타일 정의의 위치만이 아니라 보간 값, prop 전달, 실제 HTML 의미, CSS 캐스케이드의 경계를 명시하는 데 있습니다.

다음 절에서는 같은 HTML 의미와 동작을 유지하면서 화면 폭에 맞춰 배치하는 반응형 디자인을 다룹니다. 테마와 전역 스타일의 API는 필요한 범위에서 공식 문서를 확인합니다.

공식 문서

예제 파일과 실행 환경 연결

이 절의 기존 .js 파일명은 JSX를 처리하도록 설정된 프로젝트를 전제로 합니다. 앞에서 만든 Vite React 프로젝트에서 따라 한다면 JSX가 있는 예제 파일은 같은 내용으로 .jsx 확장자로 저장합니다. 예를 들어 StyledComponentsBasic.js의 내용은 StyledComponentsBasic.jsx에 두고 App.js 예제는 App.jsx에 적용합니다. 확장자를 명시하는 import도 실제 파일명에 맞춥니다. CSS 작성 원리가 파일명에 따라 달라지는 것은 아닙니다.

설치 명령은 실습 프로젝트 폴더에서 실행하는 준비 단계입니다. 프로젝트의 lockfile과 설치 버전을 확인해야 하며, 이 절의 prop 필터 설명은 v6 계열의 변경 사항을 기준으로 합니다. 최신판이라는 이름만으로 예제의 세부 동작을 추정하지 않습니다.

태그드 템플릿에서 최종 요소까지

styled.button 뒤의 템플릿은 CSS 문자열과 보간 값을 전달하여 사용할 컴포넌트를 정의합니다. 렌더에서는 $primary를 읽는 보간 함수가 색을 선택하고, 생성된 클래스와 CSS 규칙이 실제 버튼에 연결됩니다. $primary가 스타일 계산에 쓰인다는 사실과 최종 DOM 속성으로 전달된다는 사실은 다릅니다.

사용자 컴포넌트를 감싸는 경우에는 그 연결을 직접 유지해야 합니다. 다음 예시는 별도 파일에서 확인하는 확장입니다.

src/components/ForwardedLink.jsx
import styled from 'styled-components';

function PlainLink({ className, href, children }) {
  return <a className={className} href={href}>{children}</a>;
}

const StyledLink = styled(PlainLink)`
  color: #065b77;
  text-decoration: underline;
`;

export default function ForwardedLink() {
  return <StyledLink href="/docs">문서로 이동</StyledLink>;
}

App.jsx에서 ForwardedLink를 import해 <ForwardedLink />를 렌더합니다. PlainLink가 받은 className을 a에 연결하므로 생성된 선택자가 실제 요소와 만납니다. 그 전달을 지우면 링크는 남지만 생성 클래스가 요소에 붙지 않아 이 스타일 연결이 끊깁니다. 유효한 탐색 경로는 실습 앱에서 따로 제공해야 합니다.

두 계약을 따로 판단하기

DynamicButton에 $primary={false}, type="button", aria-label="저장"을 전달한다고 가정합니다. 기본 배경색과 DOM에 남는 속성을 먼저 적습니다. 같은 모양의 탐색 링크가 필요하다면 as와 styled(Base) 중 무엇으로 target을 바꾸며, href가 없는 경우 어떤 계약이 빠졌는지도 설명해 봅니다.

스타일 입력과 target의 해설

기본 배경색은 #6c757d입니다. $primary는 스타일 전용 transient prop이므로 DOM으로 전달하지 않지만 type="button"과 aria-label="저장"은 버튼이 받는 속성입니다. 생성된 class는 CSS를 연결합니다.

이번 요소의 target을 a로 바꾸는 데에는 as="a"를 사용하고 실제 탐색 목적에 맞는 href를 함께 제공합니다. 이름 있는 변형과 추가 규칙이 반복해서 필요할 때에는 styled(Base)로 그 규칙을 정의합니다. 모양만 링크처럼 만들거나 as만 바꾸어도 탐색 경로와 키보드 동작이 자동으로 완성되지는 않습니다.