안동민 개발노트

안동민 개발노트

CSS 모듈 활용Sass 통합CSS-in-JS 솔루션Tailwind CSS 설정 및 사용
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 9장 : 스타일링과 CSS
  5. CSS-in-JS 솔루션
  1. Next.js
  2. CSS-in-JS 솔루션

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-in-JS는 JavaScript를 사용하여 컴포넌트의 스타일을 정의하고 관리하는 기술입니다.

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

이 절의 Styled Components와 Emotion은 런타임에 규칙을 생성하는 방식입니다. CSS-in-JS 전체에는 빌드 시 CSS를 추출하는 방식도 있으므로 라이브러리별 처리 시점을 구분해야 합니다.

주요 특징
  • 컴포넌트 중심 스타일링: 스타일이 특정 컴포넌트와 밀접하게 결합되어 있어, 해당 컴포넌트의 로직과 스타일을 한곳에서 관리할 수 있습니다.
  • 동적 스타일링: 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.jsx (문법 예시)
"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에 따라 동적으로 스타일을 변경하기 쉽습니다.
  • 벤더 프리픽스: styled-components v6에서는 자동 추가가 기본적으로 꺼져 있습니다. 대상 브라우저에 필요하면 StyleSheetManager의 enableVendorPrefixes로 활성화할 수 있습니다.
  • 서버 사이드 렌더링(SSR) 지원: Next.js와 같은 SSR 환경에서 초기 로딩 시 스타일이 올바르게 적용되도록 지원합니다.

Emotion

Emotion은 styled와 css prop 등의 API로 스타일을 작성할 수 있는 CSS-in-JS 라이브러리입니다. 아래는 API 문법 비교용 예제이며, css prop에는 Emotion용 JSX 변환 설정이 필요합니다. 이 파일만 추가한 App Router 통합 실습으로 해석하지 않습니다.

src/app/css-in-js/EmotionButton.jsx (문법 예시)
"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 통합하기

Registry 코드의 분기와 스타일 처리

Registry 코드의 분기와 스타일 처리을 비교합니다.

Registry 코드의 분기와 스타일 처리
단계원문의 실행 위치처리 내용
시트 준비Registry의 useState 초기화새 ServerStyleSheet를 만들어 해당 Registry 인스턴스의 상태로 보관
서버 렌더window가 없는 서버 분기StyleSheetManager에 시트를 전달하여 렌더 중 스타일 규칙 수집
HTML 주입useServerInsertedHTML 콜백getStyleElement로 스타일을 가져온 뒤 clearTag로 수집 태그를 비우고 스타일 반환
브라우저 전환window가 존재하는 분기Registry는 children을 그대로 반환; 이후 동적 스타일은 styled-components가 처리
시트 준비
원문의 실행 위치: Registry의 useState 초기화
처리 내용: 새 ServerStyleSheet를 만들어 해당 Registry 인스턴스의 상태로 보관
서버 렌더
원문의 실행 위치: window가 없는 서버 분기
처리 내용: StyleSheetManager에 시트를 전달하여 렌더 중 스타일 규칙 수집
HTML 주입
원문의 실행 위치: useServerInsertedHTML 콜백
처리 내용: getStyleElement로 스타일을 가져온 뒤 clearTag로 수집 태그를 비우고 스타일 반환
브라우저 전환
원문의 실행 위치: window가 존재하는 분기
처리 내용: Registry는 children을 그대로 반환; 이후 동적 스타일은 styled-components가 처리

컴파일러의 styledComponents 옵션과 런타임 Registry는 역할이 다릅니다.

Next.js App Router는 기본적으로 React 서버 컴포넌트를 사용하므로, 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 설정

아래 실습에서는 Root Layout(src/app/layout.tsx)이 StyledComponentsRegistry로 children을 감쌉니다. Root Layout은 서버 컴포넌트로 남으며, 서버에서 전달한 children을 감싸는 것만으로 그 하위 전체가 클라이언트 모듈로 바뀌지는 않습니다.

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"는 서버 컴포넌트에서 직접 가져올 클라이언트 진입점에 둡니다. 그 경계 안에서 가져오는 모든 파일에 반복해서 붙일 필요는 없습니다. 아래 서버 페이지는 클라이언트 MyStyledButton을 렌더링합니다.

실습: 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.tsx와 MyStyledButton.tsx 파일을 생성합니다.

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

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

CSS-in-JS의 장단점

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

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

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

  • CSS 모듈 / Sass 모듈: 컴포넌트 단위 스타일링과 충돌 방지를 선호하며, CSS 문법에 익숙한 경우 좋은 선택입니다. 별도의 런타임 오버헤드가 거의 없습니다.
  • 런타임 CSS-in-JS: props와 테마에 따른 스타일 생성이 필요할 때 고려합니다. App Router 지원 여부와 라이브러리별 서버 스타일 수집 경로를 확인합니다.
  • Tailwind CSS: 유틸리티 우선(Utility-first) CSS 프레임워크로, HTML에 직접 클래스를 추가하여 스타일을 적용합니다. 빠른 프로토타이핑과 일관된 디자인 시스템 구축에 강력합니다. (다음 절에서 다룸)

Sass 통합

이전 페이지

Tailwind CSS 설정 및 사용

다음 페이지

이 페이지의 목차

CSS-in-JS란 무엇인가요?주요 CSS-in-JS 라이브러리Styled ComponentsEmotionApp Router에서 CSS-in-JS 통합하기Styled Components 설치Next.js 컴파일러 설정Styled Components Provider 설정Styled Components Registry 파일 생성CSS-in-JS 컴포넌트 사용CSS-in-JS의 장단점어떤 스타일링 방식을 선택해야 할까요?