본문 바로가기

Styled-components 기초

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

앞 글에서는 CSS 모듈로 클래스 이름의 충돌을 막고 파일 단위로 스타일을 나누는 방법을 배웠습니다. 이번에는 5장의 세 번째 방법으로, CSS-in-JS의 대표적인 라이브러리 가운데 하나인 styled-components를 소개합니다. styled-components는 자바스크립트 코드 안에서 CSS를 작성하고, 그 스타일을 컴포넌트에 바로 연결해 쓰는 방식입니다. 스타일의 정의를 컴포넌트 코드 가까이에 둘 수 있지만, 결과는 결국 브라우저가 처리하는 CSS와 클래스이므로 CSS의 캐스케이드나 DOM 속성의 규칙이 사라지는 것은 아닙니다.


CSS-in-JS란?

CSS-in-JS는 말 그대로 자바스크립트 코드 안에 CSS를 작성하는 방식을 통틀어 부르는 말입니다. 예전에는 CSS 파일을 따로 나눠 관리했지만, CSS-in-JS는 컴포넌트의 로직과 스타일을 하나의 자바스크립트 파일 안에서 함께 작성합니다.

styled-components의 핵심 아이디어는 몇 가지로 정리할 수 있습니다. 스타일이 적용된 React 컴포넌트를 만들고, 라이브러리가 스타일 규칙과 이어지는 클래스를 자동으로 만들어 붙입니다. 이 생성 문자열의 구체적인 모양에 애플리케이션이 기대어서는 안 됩니다. 보간 함수에서 props와 theme을 읽어 스타일을 유연하게 바꿀 수 있고, state 값도 prop으로 넘겨 쓸 수 있습니다. 또 스타일을 이름 있는 컴포넌트로 묶어 재사용하지만, 전역 CSS와 바깥에서 넘긴 className, 선택자의 우선순위, 소스 순서, 상속은 여전히 일반 CSS의 캐스케이드에 참여합니다.


Styled-components 설치

styled-components를 쓰려면 먼저 실습 프로젝트 폴더에서 라이브러리를 설치합니다. 이 글은 2026년 11월 기준 최신인 styled-components 6.6을 기준으로 하며, 뒤에서 설명할 prop 필터의 동작도 v6 계열의 변경 사항을 따릅니다. 설치한 뒤에는 프로젝트의 lockfile에서 실제로 설치된 버전을 확인하세요.

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

styled-components는 앱이 실행되는 동안 스타일 규칙을 만들어 문서에 넣으므로, 개발 의존성이 아니라 일반 의존성으로 설치합니다. Vite 프로젝트에서는 설치 뒤에 따로 설정하지 않아도 import해서 쓸 수 있습니다.


Styled-components 기본 사용법

styled-components는 태그드 템플릿 리터럴(tagged template literal) 문법으로 스타일이 적용된 컴포넌트를 만듭니다. styled.button처럼 HTML 태그 이름을 고른 뒤 바로 백틱 문자열을 붙이면, 그 문자열이 함수의 인자로 전달되어 스타일이 적용된 새 컴포넌트가 만들어집니다. 그래서 아래 StyledButton은 변수이지만 JSX에서는 대문자 이름의 일반 컴포넌트처럼 씁니다.

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>
  );
}

styled.button 뒤의 백틱 안에 쓴 CSS 문자열과 보간 값으로 쓸 컴포넌트를 정의하고, JSX에서는 그 컴포넌트를 일반 컴포넌트처럼 씁니다. &:hover처럼 &로 자기 자신을 가리켜 가상 클래스도 바로 쓸 수 있습니다.

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

src/components/StyledComponentsBasic.jsx 파일을 만들고 다음 코드를 작성합니다. 이 교재의 Vite 프로젝트에서는 JSX가 들어 있는 파일을 .jsx로 저장합니다.

src/components/StyledComponentsBasic.jsx
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;

네 컴포넌트는 모두 함수 바깥, 모듈 최상위에서 한 번 정의합니다. 각 컴포넌트에는 라이브러리가 만든 고유한 클래스 이름이 붙으므로, 다른 컴포넌트의 클래스 규칙과 이름이 겹치지 않습니다. StyledComponentsBasic 함수는 이 컴포넌트들을 조합해 화면 구조만 정합니다.

App.jsx에서 렌더링

App.jsx를 고쳐 StyledComponentsBasic 컴포넌트를 렌더링합니다. 스타일은 컴포넌트 파일 안에 모두 있으므로, 앞 글의 CSS 모듈 예제처럼 별도의 CSS 파일을 만들 필요가 없습니다. 아래 코드는 앞 글의 예제 import를 주석으로 남기고 새 컴포넌트만 렌더링합니다.

src/App.jsx
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)를 열어 Elements 탭을 보면, 각 HTML 요소에 styled-components가 스타일 규칙과 연결하려고 만든 클래스 토큰이 붙어 있습니다. 그 토큰의 정확한 문자열 형식은 버전과 빌드 설정에 따라 달라질 수 있으므로 코드에서 직접 참조하지 않습니다.


Props를 이용한 동적 스타일링

styled-components의 큰 장점 가운데 하나는 props로 스타일을 동적으로 바꿀 수 있다는 점입니다. 태그드 템플릿 리터럴 안에 자바스크립트 코드를 넣을 수 있듯이, props를 인자로 받는 함수를 넣어 동적인 스타일을 적용합니다.

src/components/DynamicStyledButton.jsx
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;

이 파일이 기본으로 내보내는 DynamicStyledComponents를 App.jsx에 넣어 실행해 보세요. $primary가 있는 “기본 버튼”은 #007bff의 파란 배경으로, 없는 “보조 버튼”은 #6c757d의 회색 배경으로 보이고, 두 문단은 $size와 $color에 따라 크고 빨간 글자와 작고 초록색인 글자로 보입니다. 인라인 스타일처럼 동적인 값을 다루면서도 &:hover 같은 CSS 문법을 함께 쓸 수 있다는 것이 이 방식의 장점입니다.

$로 시작하는 transient prop은 보간 함수에서 읽지만 최종 요소(target)로는 넘기지 않는 스타일 전용 값입니다. styled-components v6 계열은 v5에 있던 기본 자동 prop 필터를 제공하지 않으므로, 이름이 일반적인 스타일 전용 prop을 그대로 두면 DOM 요소까지 넘어갈 수 있습니다. 이름만으로 충분하지 않은 조합에서는 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 전달 경로를 각각 검토한다.

위 그림은 스타일의 보간과 DOM prop의 전달이 서로 다른 경로라는 점을 보여 줍니다. JSX에서 넘긴 $primary와 type, aria-label 같은 props는 styled 컴포넌트에서 평가되어 두 갈래로 나뉩니다. 한쪽은 보간 함수가 $primary로 색을 계산해 CSS와 클래스를 만드는 경로이고, 다른 쪽은 transient prop을 빼고 필요하면 설정의 필터로 걸러 props를 넘기는 경로입니다. 두 경로는 나란히 진행되다가 최종 DOM에서 클래스와 허용된 표준 속성으로 합쳐집니다.

그림 위쪽의 예처럼 $primary가 참이면 배경이 #007bff, 없으면 #6c757d가 되며, 이 값은 CSS와 클래스로 적용될 뿐 DOM에는 $primary 속성이 남지 않습니다. 그림 아래 칸이 정리하듯 단순한 HTML 요소가 target이라면 표준 HTML과 ARIA, data- 속성은 넘기고 스타일 전용 값은 $ 접두사로 나누며, 직접 만든 컴포넌트가 target이라면 그 컴포넌트가 스타일을 적용할 DOM 요소에 className을 넘겨야 합니다. 또 생성된 규칙에도 전역 CSS와 추가로 넘긴 className, 상속, 우선순위, 소스 순서가 그대로 작용하므로, 스타일을 가까이 정의했다고 완전히 격리되는 것은 아닙니다.

더 깊이: 보간 결과가 바뀔 때마다 생기는 CSS 클래스

보간 함수가 돌려준 결과는 그대로 CSS 규칙이 됩니다. styled-components 문서는 prop 보간에서 서로 다른 결과마다 새 CSS 클래스가 하나씩 만들어진다고 설명하므로, 이 글의 $primary처럼 가능한 값이 몇 개뿐이라면 문제가 없습니다. 반대로 슬라이더의 위치나 마우스 좌표처럼 계속 바뀌는 값을 보간에 넣으면, 값이 바뀔 때마다 새 클래스와 CSS 규칙을 만들어야 합니다. 같은 문서는 이런 값을 .attrs()로 인라인 style에 넘기라고 권하는데, 인라인 선언은 클래스를 만들지 않기 때문입니다(styled-components FAQ).

같은 이유로 styled 컴포넌트는 이 글의 예제처럼 모듈 최상위에서 한 번만 정의합니다. 컴포넌트 함수 안에서 styled.button을 호출하면 렌더할 때마다 새 컴포넌트 타입이 만들어지고, React는 타입이 바뀐 자리를 새 컴포넌트로 보고 그 아래의 DOM과 state를 다시 만듭니다. styled-components FAQ도 렌더 안에서 정의하는 방식을 성능 병목과 예측하기 어려운 동작의 원인으로 꼽습니다.


as prop을 이용한 태그 변경

styled-components로 만든 컴포넌트는 as prop으로 렌더링할 HTML 태그를 바꿀 수 있습니다. 같은 스타일을 유지하면서 다른 시맨틱 태그를 써야 할 때 유용합니다.

src/components/AsPropExample.jsx
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.jsx에 AsPropExample을 넣어 실행해 보면, 두 요소는 같은 스타일이지만 개발자 도구에서 하나는 button, 다른 하나는 href="/docs"를 가진 a 태그로 렌더링됩니다. as는 스타일을 유지한 채 target을 바꿀 뿐, 접근성의 의미를 자동으로 맞춰 주지는 않습니다. 동작에는 button을, 페이지 이동에는 올바른 href가 있는 a를 고르는 것처럼, 목적에 맞는 요소와 속성을 함께 골라야 합니다.


다른 컴포넌트 확장하기

기존 컴포넌트를 감싸 공통 스타일을 재사용하면서 새 규칙을 더할 수 있습니다. styled(BaseButton)처럼 태그 이름 대신 이미 만든 스타일 컴포넌트를 넘기면, 기존 규칙을 모두 가진 채 새 규칙을 덧붙인 컴포넌트가 만들어집니다. 아래 코드에서 크기와 모양은 BaseButton에만 적고, 색상은 PrimaryButton과 DangerButton에서 각각 정합니다.

src/components/ExtendedStyledComponent.jsx
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.jsx에 ExtendedStyledComponent를 넣어 실행해 보면, BaseButton의 기본 스타일을 재사용하면서 초록색과 빨간색의 버튼을 각각 만들 수 있습니다. 이때 styled(Base)는 자바스크립트의 클래스 상속이 아니라 스타일을 입힐 수 있는 React 컴포넌트를 감싸는 조합입니다. 직접 만든 Base라면 className을 받아 최종 DOM 요소에 넘겨야 하고, 기본 규칙과 추가 규칙은 일반 CSS의 캐스케이드로 함께 계산됩니다.

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

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의 시맨틱 계약과 스타일 조합 계약을 별도로 검토한다.

위 그림은 렌더할 target만 바꾸는 as와, 새 이름과 규칙을 가진 변형을 만드는 styled(Base)를 고르는 기준을 정리합니다. 먼저 무엇을 바꾸려는지 확인하고, 같은 규칙에서 target만 바꾸면 된다면 as로 이번 렌더의 target을 바꾸며 필요한 속성을 함께 넘깁니다. 그렇지 않고 재사용할 Base에 새 규칙을 더해야 한다면 styled(Base)로 새 이름의 변형을 만들고 className의 연결을 유지하며, 둘 다 아니라면 목적에 맞는 target으로 새 styled 요소를 만듭니다.

그림 위쪽의 예는 이 글의 두 코드입니다. ButtonLike as="a" href="/docs"는 기존 스타일을 유지한 탐색 링크가 되고, PrimaryButton = styled(BaseButton)은 공통 규칙에 새 색상 규칙을 더한 변형이 됩니다. 그림의 설명처럼 as는 올바른 href나 접근성 동작을 자동으로 만들지 않고, styled(Base)는 클래스 상속이 아니라 컴포넌트와 스타일의 조합입니다. .attrs는 공통 속성과 기본값을, theme은 보간의 입력을 제공할 뿐 잘못 고른 HTML의 의미를 대신 고쳐 주지 않으며, 같은 모양이 같은 의미를 보장하지는 않습니다.


className을 넘기는 사용자 컴포넌트

직접 만든 컴포넌트를 감쌀 때는 생성된 클래스와 실제 요소의 연결을 직접 유지해야 합니다. 다음은 별도의 파일에서 확인하는 예입니다.

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를 가져와 <ForwardedLink />를 렌더링합니다. PlainLink가 받은 className을 a에 연결하므로, 생성된 선택자가 실제 요소와 만나 밑줄과 색이 적용됩니다. 실습 앱에서 이동할 수 있는 실제 경로는 따로 마련해야 합니다.

styled-components의 보간 함수와 전달되는 props, .attrs의 기본 사용법은 styled-components의 The Basics에서, as와 transient props, shouldForwardProp, ThemeProvider는 API Reference에서 확인할 수 있습니다. v6에서 기본 자동 prop 필터가 빠진 내용은 v6 마이그레이션 FAQ에, HTML 요소가 받는 표준 속성과 사용자 지정 속성은 React DOM 공통 props에, DOM으로 넘기면 안 되는 컴포넌트 전용 prop을 다루는 방법은 React의 Unknown Prop 경고에 있습니다.


정리

  • styled-components는 태그드 템플릿 리터럴로 스타일이 적용된 컴포넌트를 만들고, 그 규칙과 이어지는 클래스를 자동으로 붙입니다.
  • 보간 함수에서 props와 theme을 읽어 동적인 스타일을 만들며, 스타일 전용 값은 $로 시작하는 transient prop으로 DOM에 넘기지 않습니다.
  • v6 계열은 자동 prop 필터가 없으므로, 일반 이름의 스타일 전용 prop은 shouldForwardProp으로 걸러야 할 수 있습니다.
  • as는 같은 스타일로 렌더할 태그만 바꾸며, href나 접근성 동작은 직접 갖춰야 합니다.
  • styled(Base)는 공통 스타일에 새 규칙을 더한 변형을 만들며, 직접 만든 컴포넌트를 감쌀 때는 className을 DOM 요소에 넘겨야 합니다.
  • 생성된 규칙도 일반 CSS의 캐스케이드에 참여하므로, 스타일이 가까이 정의되었다고 완전히 격리되지는 않습니다.

연습 문제

1. 스타일 입력과 DOM 속성

DynamicButton에 $primary={false}, type="button", aria-label="저장"을 넘긴다고 가정합니다. 버튼의 기본 배경색은 무엇이고, 최종 DOM에는 어떤 속성이 남을까요?

해설 보기

기본 배경색은 #6c757d입니다. $primary는 스타일 전용 transient prop이라서 DOM으로 넘어가지 않지만, type="button"과 aria-label="저장"은 버튼이 받는 속성이므로 DOM에 남습니다. 여기에 생성된 class가 붙어 CSS 규칙과 이어집니다.

2. 같은 모양의 탐색 링크

DynamicButton과 같은 모양의 탐색 링크가 필요합니다. as와 styled(Base) 가운데 무엇으로 target을 바꿔야 할까요? href가 없다면 무엇이 빠진 것일까요?

해설 보기

이번 요소의 target만 a로 바꾸면 되므로 as="a"를 쓰고, 실제로 이동할 목적지인 href를 함께 넘깁니다. 이름 있는 변형과 추가 규칙이 되풀이해서 필요하다면 styled(Base)로 그 변형을 정의합니다.

href가 없으면 탐색할 목적지가 빠진 것입니다. 모양만 링크처럼 만들거나 as만 바꿔서는 이동 경로와 키보드 동작이 저절로 갖춰지지 않습니다.

3. className 전달을 지우면

ForwardedLink의 PlainLink에서 className={className}을 지우면 화면은 어떻게 될까요?

해설 보기

링크는 그대로 나타나지만, 생성된 클래스가 a 요소에 붙지 않아 styled-components가 만든 색과 밑줄 규칙이 적용되지 않습니다. 브라우저의 기본 링크 스타일만 남습니다. 직접 만든 컴포넌트를 styled()로 감쌀 때는 받은 className을 스타일을 적용할 DOM 요소까지 넘겨야 합니다.

다음 글 반응형 디자인 기초에서는 같은 HTML의 의미와 동작을 유지하면서 화면 폭에 맞춰 배치를 바꾸는 반응형 디자인을 다룹니다.