본문으로 건너뛰기

안동민 개발노트

본문 시작

CSS-in-JS 솔루션

Styled Components와 Emotion의 동적 스타일 방식을 비교하고 App Router의 서버 렌더링 경계에 맞춰 통합합니다.

이전 절에서 CSS 모듈과 Sass를 활용하여 Next.js 프로젝트에서 스타일링을 효율적으로 관리하는 방법을 배웠습니다.

이 방법들은 전통적인 CSS 작성 방식의 단점을 보완하고 컴포넌트 기반 개발에 적합합니다.

하지만 React 생태계에는 또 다른 스타일링 방식인 CSS-in-JS가 존재합니다.

CSS-in-JS는 말 그대로 CSS 코드를 JavaScript 파일 안에 작성하는 방식입니다.

이는 컴포넌트와 스타일을 하나의 JavaScript 파일 안에서 관리하게 하여 개발 경험을 더욱 통합적이고 동적으로 만듭니다.

이 절에서는 CSS-in-JS의 개념, 주요 라이브러리, 장단점, 그리고 Next.js App Router에서 CSS-in-JS를 통합하는 방법을 정리합니다.

CSS-in-JS 솔루션

이전 절에서는 CSS 모듈의 클래스 스코핑과 Sass의 변수·중첩 문법으로 Next.js 프로젝트 스타일을 모듈 단위로 관리했습니다. 두 방식은 전역 클래스 충돌을 줄이고 컴포넌트 기반 개발에 맞습니다.

  1. CSS-in-JS 개념

    런타임 CSS-in-JS는 JavaScript로 컴포넌트 스타일을 정의하고 상태와 props에 따라 규칙을 생성합니다. CSS-in-JS

  2. 라이브러리별 통합 경로

    styled-components는 Next.js의 SWC 변환과 Registry를 사용합니다. Emotion은 App Router 지원 상태와 별도 SSR 통합 경로를 확인합니다. 지원 상태 확인

  3. App Router에서 CSS-in-JS 통합하기

    styled-components는 compiler.styledComponents 를 켜고 Client Registry로 서버 규칙을 수집·주입합니다. SWC + Registry

  4. CSS-in-JS 솔루션 기준

    정확성 컴포넌트 단위로 스타일과 상태 조건을 묶는 CSS-in-JS 방식도 선택지입니다. 비용 컴포넌트 단위 스타일 격리와 재사용 범위를 함께 봅니다. 확장성 조건부 스타일은 props, 상태, 테마 전환 기준을 분리해 둡니다. 예외 테마 전환 기준과 서버 렌더링 영향도를 함께 확인합니다.

  5. 정확성 컴포넌트 단위

    스타일과 상태 조건을 묶는 CSS-in-JS 방식도 선택지입니다.

  6. 비용 컴포넌트 단위 스타일 격리

    재사용 범위를 함께 봅니다.

  7. 확장성 조건부 스타일

    props, 상태, 테마 전환 기준을 분리해 둡니다.

  8. 예외 테마 전환 기준

    서버 렌더링 영향도를 함께 확인합니다.


CSS-in-JS란 무엇인가요?

CSS-in-JS는 JavaScript를 사용하여 컴포넌트의 스타일을 정의하고 관리하는 기술입니다.

CSS 코드가 .css.scss와 같은 별도의 파일에 분리되는 대신, React 컴포넌트 파일 .tsx (또는 .jsx) 내부에 JavaScript 객체나 템플릿 리터럴 형태로 작성됩니다.

런타임에 JavaScript가 이 스타일들을 파싱하여 실제 CSS로 변환하고 <style> 태그 형태로 HTML 문서의 <head>에 삽입하거나, 인라인 스타일로 적용합니다.

주요 특징
  • 컴포넌트 중심 스타일링: 스타일이 특정 컴포넌트와 밀접하게 결합되어 있어, 해당 컴포넌트의 로직과 스타일을 한곳에서 관리할 수 있습니다.
  • 동적 스타일링: JavaScript의 모든 기능을 활용하여 조건부 스타일링, 프롭스 기반 스타일링, 테마 변경 등 매우 동적인 스타일링이 가능합니다.
  • 자동 스코핑: 대부분의 CSS-in-JS 라이브러리는 스타일 충돌을 방지하기 위해 자동으로 고유한 클래스 이름을 생성하거나 인라인 스타일을 적용합니다.
  • 런타임 CSS 생성: 개발자가 작성한 JavaScript 스타일 정의를 기반으로 실제 CSS가 런타임 또는 빌드 시점에 생성됩니다.

주요 CSS-in-JS 라이브러리

React 생태계에는 다양한 CSS-in-JS 라이브러리들이 존재하며, 각각 고유한 특징과 사용법을 가지고 있습니다.

Styled Components

Styled Components는 가장 인기 있고 널리 사용되는 CSS-in-JS 라이브러리 중 하나입니다.

태그드 템플릿 리터럴(Tagged Template Literals)을 사용하여 CSS를 작성하는 방식입니다.

src/app/css-in-js/StyledButton.tsx (예시)
"use client"; // 클라이언트 컴포넌트임을 명시

import styled from 'styled-components';

// styled.button을 사용하여 <button> 요소를 기반으로 하는 스타일링된 컴포넌트 생성
const StyledButton = styled.button`
  background-color: ${props => (props.$primary ? '#007bff' : '#f0f0f0')};
  color: ${props => (props.$primary ? 'white' : '#333')};
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1em;
  transition: background-color 0.3s ease;

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

export default function MyStyledButton({ label, primary, onClick }) {
  return (
    <StyledButton $primary={primary} onClick={onClick}>
      {label}
    </StyledButton>
  );
}
특징
  • 컴포넌트 기반: 스타일이 적용된 HTML 요소를 나타내는 React 컴포넌트를 생성합니다.
  • 프롭스 기반 스타일링: 컴포넌트의 props에 따라 동적으로 스타일을 변경하기 쉽습니다.
  • 자동 프리픽싱 및 벤더 프리픽스: 브라우저 호환성을 위한 CSS 접두사를 자동으로 추가합니다.
  • 서버 사이드 렌더링(SSR) 지원: Next.js와 같은 SSR 환경에서 초기 로딩 시 스타일이 올바르게 적용되도록 지원합니다.

Emotion

Emotion은 또 다른 인기 있는 CSS-in-JS 라이브러리로, Styled Components와 유사한 기능을 제공하지만, 더 유연하고 성능에 중점을 둡니다.

src/app/css-in-js/EmotionButton.tsx (예시)
"use client"; // 클라이언트 컴포넌트임을 명시

import { css } from '@emotion/react'; // css 헬퍼 함수
import styled from '@emotion/styled'; // styled 헬퍼 함수

// styled 함수 사용 (styled components와 유사)
const StyledEmotionButton = styled.button`
  background-color: ${props => (props.$primary ? '#28a745' : '#f0f0f0')};
  color: ${props => (props.$primary ? 'white' : '#333')};
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 1em;
  transition: background-color 0.3s ease;

  &:hover {
    background-color: ${props => (props.$primary ? '#218838' : '#e0e0e0')};
  }
`;

// css 헬퍼 함수 사용 (클래스 기반으로 스타일 적용)
const dangerButtonStyle = css`
  background-color: #dc3545;
  color: white;
  &:hover {
    background-color: #c82333;
  }
`;

export default function MyEmotionButton({ label, primary, danger, onClick }) {
  if (danger) {
    return (
      <button css={dangerButtonStyle} onClick={onClick}>
        {label}
      </button>
    );
  }
  return (
    <StyledEmotionButton $primary={primary} onClick={onClick}>
      {label}
    </StyledEmotionButton>
  );
}
특징
  • 유연성: styled API뿐만 아니라 css 프롭스를 통해 인라인 스타일처럼 객체를 전달하거나, css 헬퍼 함수를 사용하여 클래스 기반으로 스타일을 적용할 수 있습니다.
  • 성능: Emotion은 런타임 CSS-in-JS 라이브러리입니다. @emotion/babel-plugin은 라벨과 소스 맵, 일부 최적화를 돕지만 런타임 자체를 없애지는 않습니다.
  • 프롭스 기반 스타일링: Styled Components와 마찬가지로 프롭스에 따른 동적 스타일링이 가능합니다.
  • App Router 제약: Next.js 16의 공식 CSS-in-JS 안내에서 Emotion은 App Router 지원 작업이 진행 중인 라이브러리로 분류됩니다. 새 App Router 실습에서는 지원 상태를 먼저 확인하고, 이 절에서는 현행 통합 경로가 안내된 Styled Components를 사용합니다.

App Router에서 CSS-in-JS 통합하기

styled-components는 SWC 변환과 Registry를 함께 설정한다

compiler.styledComponents 로 SSR class 변환을 켠 뒤 요청별 규칙을 수집해 콘텐츠보다 먼저 보낸다.

  1. 1
    SWC transform

    compiler.styledComponents: true 로 SSR 변환 활성화

  2. 2
    Render & collect

    ServerStyleSheet 가 요청별 style rule 수집

  3. 3
    Insert & clear

    useServerInsertedHTML 로 주입한 뒤 tag 비우기

  4. 4
    Hydrate

    client가 같은 class와 순서를 재사용

Next.js App Router는 기본적으로 React 서버 컴포넌트를 사용하므로, CSS-in-JS 라이브러리 사용 시 몇 가지 특별한 설정이 필요합니다.

CSS-in-JS 라이브러리는 클라이언트 측에서 스타일을 주입하므로, 반드시 클라이언트 컴포넌트 내에서 사용해야 합니다.

또한 SSR 시 스타일이 올바르게 추출되어 초기 HTML에 포함되도록 추가 설정이 필요합니다.

여기서는 Styled Components를 예시로 통합 방법을 설명합니다.

Emotion은 App Router 지원이 완성된 것으로 간주하지 않으며, 공식 지원 상태가 바뀌기 전까지 아래 통합 절차의 대체재로 사용하지 않습니다.

Styled Components 설치

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

Next.js는 기본 컴파일러로 SWC를 사용합니다.

Styled Components 변환도 SWC 옵션으로 활성화하므로 Babel 플러그인은 설치하지 않습니다.

Next.js 컴파일러 설정

프로젝트 루트의 next.config.ts에서 Styled Components 변환을 켭니다.

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  compiler: {
    styledComponents: true,
  },
};

export default nextConfig;

커스텀 Babel 설정을 추가하면 해당 파일은 SWC 변환을 사용하지 못하므로 특별한 이유가 없다면 만들지 않습니다.

Styled Components Provider 설정

Next.js App Router에서는 모든 페이지에 공통으로 적용되는 스타일 처리를 위해 Root Layout(src/app/layout.tsx)에 Styled Components의 StyleSheetManager (또는 Emotion의 CacheProvider)를 설정해야 합니다.

src/app/layout.tsx
import './globals.css'; // 전역 CSS 임포트 (필요시)
import StyledComponentsRegistry from './lib/registry'; // 새로 생성할 레지스트리 파일 임포트

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        {/* Styled Components를 위한 레지스트리 Provider로 감싸기 */}
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}

Styled Components Registry 파일 생성

SSR 환경에서 Styled Components가 스타일을 올바르게 추출하고 주입하도록 돕는 유틸리티 컴포넌트를 생성해야 합니다.

src/app/lib/registry.tsx
"use client"; // 🚨 이 파일은 클라이언트 컴포넌트여야 합니다.

import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  // SSR 환경에서 한 번만 시트를 생성
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    // 서버에서 렌더링 시 스타일을 추출하여 HTML에 삽입
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag(); // 추출 후 시트 초기화
    return <>{styles}</>;
  });

  if (typeof window !== 'undefined') return <>{children}</>;

  // 서버에서 스타일시트 매니저로 children을 감싸 렌더링
  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}

CSS-in-JS 컴포넌트 사용

이제 Styled Components를 사용하여 컴포넌트를 스타일링할 수 있습니다.

중요한 것은 스타일링된 컴포넌트가 사용되는 모든 파일은 "use client" 지시어가 있어야 한다는 것입니다.

실습: Styled Components를 사용한 UI 컴포넌트
src/app/css-in-js/page.tsx (서버 컴포넌트)
import MyStyledButton from './MyStyledButton'; // 클라이언트 컴포넌트 임포트

export default function CssInJsPage() {
  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', textAlign: 'center', border: '1px solid #ccc', borderRadius: '8px' }}>
      <h1>CSS-in-JS (Styled Components) 예제</h1>
      <p>아래 버튼은 Styled Components로 스타일링되었습니다.</p>
      <div style={{ display: 'flex', gap: '20px', justifyContent: 'center', marginTop: '30px' }}>
        <MyStyledButton label="기본 버튼" />
        <MyStyledButton label="강조 버튼" primary={true} />
      </div>
    </div>
  );
}
src/app/css-in-js/MyStyledButton.tsx (클라이언트 컴포넌트)
"use client"; // 🚨 반드시 필요

import styled from 'styled-components';

const ButtonContainer = styled.button<{ $primary?: boolean }>`
  background-color: ${props => (props.$primary ? '#007bff' : '#f0f0f0')};
  color: ${props => (props.$primary ? 'white' : '#333')};
  padding: 12px 25px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 1.1em;
  font-weight: bold;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  transition: background-color 0.3s ease, transform 0.1s ease;

  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
  }

  &:active {
    transform: translateY(0);
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  }
`;

interface MyStyledButtonProps {
  label: string;
  primary?: boolean;
  onClick?: () => void;
}

export default function MyStyledButton({ label, primary = false, onClick }: MyStyledButtonProps) {
  return (
    <ButtonContainer $primary={primary} onClick={onClick}>
      {label}
    </ButtonContainer>
  );
}
실습 확인

위에서 설명한 Styled Components 설치 및 Next.js 컴파일러, Registry 설정을 완료합니다.

src/app/css-in-js 폴더를 만들고 위 page.tsxMyStyledButton.tsx 파일을 생성합니다.

개발 서버(npm run dev)를 실행한 후, http://localhost:3000/css-in-js로 접속합니다.

  • 버튼들이 Styled Components로 스타일링되어 나타나는 것을 확인할 수 있습니다.
  • 페이지 소스 보기를 통해 초기 HTML에 Styled Components가 주입한 <style> 태그가 포함되어 있는지 확인하여 SSR이 올바르게 작동하는지 검증할 수 있습니다.

CSS-in-JS의 장단점

장점
  • 동적 스타일링: JavaScript를 활용하여 조건부 및 프롭스 기반 스타일링을 구현할 수 있습니다.
  • 컴포넌트 로직과의 응집성: 스타일과 컴포넌트 로직이 한 파일에 있어 관련 코드를 찾고 관리하기 쉽습니다.
  • 자동 스코핑: 스타일 충돌 걱정 없이 자유롭게 클래스 이름을 지을 수 있습니다.
  • 쉬운 테마 시스템 구축: Context API와 함께 사용하여 전역 테마를 쉽게 적용하고 변경할 수 있습니다.
  • 데드 코드 제거: 사용되지 않는 컴포넌트와 그 스타일이 빌드 시 자동으로 제거됩니다.
단점
  • 학습 곡선: 새로운 문법과 개념을 배워야 합니다.
  • 런타임 오버헤드: 스타일을 JavaScript로 파싱하고 CSS로 변환하는 과정에서 약간의 런타임 성능 저하가 발생할 수 있습니다 (최적화 옵션으로 완화 가능).
  • 초기 로딩 시 FOUC (Flash Of Unstyled Content): SSR 설정이 제대로 되지 않으면 초기 렌더링 시 스타일이 잠시 적용되지 않은 콘텐츠가 노출될 수 있습니다. (위 Styled Components Registry 설정으로 방지)
  • 디버깅: 개발자 도구에서 실제 CSS 클래스 이름이 해시화되어 있어 디버깅이 다소 어려울 수 있습니다 (styled components의 displayName 옵션으로 개선 가능).
  • 번들 크기 증가: CSS-in-JS 라이브러리 자체의 번들 크기가 추가됩니다.
CSS-in-JS는 동적 가치가 runtime·SSR 비용보다 클 때 고른다

컴포넌트 응집도뿐 아니라 style 생성과 캐시, 서버 수집 비용을 함께 본다.

  1. 정적 UI
    CSS Modules

    runtime 없이 지역 scope와 빌드 결과 사용

  2. 토큰 조립
    Tailwind

    정해진 규칙을 class 조합으로 빠르게 적용

  3. props style
    CSS-in-JS

    동적 variant 가치가 분명할 때 선택

  4. SSR 사용
    Registry 필수

    style collect·insert·hydrate 계약을 운영


어떤 스타일링 방식을 선택해야 할까요?

Next.js에서 스타일링 방식은 여러 가지가 있으며, 프로젝트의 요구사항, 팀의 선호도, 그리고 개발자의 숙련도에 따라 최적의 선택이 달라질 수 있습니다.

스타일 도구는 변경 시점과 렌더 경계로 고른다

한 프로젝트에서도 전역·지역·동적 영역의 책임에 따라 서로 다른 도구를 조합할 수 있다.

  1. 정적·지역
    CSS Modules

    컴포넌트 범위를 runtime 비용 없이 격리

  2. 토큰 중심
    Tailwind

    반복 디자인 규칙을 utility 조합으로 유지

  3. props·theme
    CSS-in-JS

    실행 중 스타일 변화와 SSR registry 필요

  4. 앱 전체
    Global CSS

    reset·layout 기반·CSS 변수에 한정

  • CSS 모듈 / Sass 모듈: 컴포넌트 단위 스타일링과 충돌 방지를 선호하며, CSS 문법에 익숙한 경우 좋은 선택입니다. 별도의 런타임 오버헤드가 거의 없습니다.
  • CSS-in-JS (Styled Components, Emotion): 매우 동적인 스타일링이 필요하거나, 컴포넌트와 스타일의 강한 응집성을 선호하는 경우 적합합니다. JavaScript 환경 내에서 모든 것을 해결하고 싶을 때 유용합니다. Next.js App Router에서는 SSR 설정을 반드시 해야 합니다.
  • Tailwind CSS: 유틸리티 우선(Utility-first) CSS 프레임워크로, HTML에 직접 클래스를 추가하여 스타일을 적용합니다. 빠른 프로토타이핑과 일관된 디자인 시스템 구축에 강력합니다. (다음 절에서 다룸)

CSS-in-JS는 스타일을 컴포넌트 단위로 관리하게 해 줍니다.

Next.js App Router와 함께 사용할 때는 SSR 설정과 런타임 비용을 함께 검토해야 합니다.

CSS-in-JS를 선택했다면 서버 렌더링 중 생성된 스타일이 클라이언트 전환까지 안정적으로 이어지는지 반드시 확인해야 합니다.

styled-components 서버 스타일 수집 경로

App Router에서는 SWC 변환과 Client Registry가 각자 맡는 책임을 구분해야 첫 화면과 hydration이 일치합니다.

  1. 설정
    SWC transform

    compiler.styledComponents: true 로 SSR class 변환을 활성화합니다.

  2. 서버
    Render & collect

    Client Registry의 ServerStyleSheet 가 요청별 규칙을 모읍니다.

  3. 전송
    Insert & clear

    useServerInsertedHTML 로 style을 먼저 넣고 보낸 규칙을 비웁니다.

  4. 인계
    Hydration

    클라이언트가 서버 태그를 재사용해 같은 규칙을 중복 생성하지 않습니다.

  5. 이후
    동적 스타일 갱신

    상태와 테마 변화로 생기는 런타임 규칙만 클라이언트에서 추가합니다.

이 다이어그램은 CSS-in-JS 솔루션을 Next.js 프로젝트에 넣을 때 결정해야 할 파일 위치와 런타임 경계를 정리합니다.

CSS-in-JS는 streaming 전에 스타일을 먼저 흘려보낸다

런타임 스타일이 필요할 때 registry가 서버 render와 client hydration 사이의 규칙 소유권을 잇는다.

  1. 1
    Registry 생성

    한 요청의 style rule을 모을 저장소 준비

  2. 2
    Server render

    컴포넌트가 현재 chunk의 규칙을 수집

  3. 3
    Insert before content

    본문보다 먼저 style tag를 head에 주입

  4. 4
    Flush

    주입한 규칙을 비워 다음 chunk 중복 방지

  5. 5
    Hydrate

    브라우저가 이후 동적 스타일 갱신을 인계

마지막으로 CSS-in-JS를 App Router에서 사용할 때 SSR, registry, Provider 설정 책임을 정리합니다.

스트리밍 CSS는 collect·insert·clear·hydrate 순서를 지킨다

각 chunk의 콘텐츠가 보이기 전에 대응하는 style이 도착해야 첫 화면과 hydration이 일치한다.

  1. render
    Rule collect

    현재 서버 render의 style을 sheet에 모음

  2. before chunk
    Server insert

    useServerInsertedHTML로 콘텐츠 앞에 주입

  3. after insert
    Clear tag

    미 보낸 rule이 다음 chunk에 중복되지 않게 비움

  4. browser
    Hydration

    서버 class를 재사용하며 client 주입으로 전환