icon
9장 : 스타일링과 CSS

CSS-in-JS 솔루션

이전 절에서 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 객체나 템플릿 리터럴 형태로 작성됩니다.

런타임에 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
// 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
// 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 헬퍼 함수를 사용하여 클래스 기반으로 스타일을 적용할 수 있습니다.
  • 성능: 제로 런타임(Zero-runtime) CSS-in-JS를 위한 @emotion/babel-plugin과 같은 빌드 타임 최적화 옵션을 제공하여 런타임 오버헤드를 줄일 수 있습니다.
  • 프롭스 기반 스타일링: Styled Components와 마찬가지로 프롭스에 따른 동적 스타일링이 가능합니다.
  • SSR 지원: Next.js와 함께 SSR 환경에서 잘 작동합니다.

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

Next.js App Router는 기본적으로 React 서버 컴포넌트를 사용하며, 이는 CSS-in-JS 라이브러리 사용에 몇 가지 특별한 설정이 필요함을 의미합니다. CSS-in-JS 라이브러리는 클라이언트 측에서 스타일을 주입하므로, 반드시 클라이언트 컴포넌트 내에서 사용되어야 합니다. 또한, SSR 시 스타일이 올바르게 추출되어 초기 HTML에 포함되도록 추가적인 설정이 필요합니다.

여기서는 Styled Components를 예시로 통합 방법을 설명합니다. Emotion도 유사한 방식으로 설정할 수 있습니다.

Styled Components 설치

npm install styled-components
npm install --save-dev babel-plugin-styled-components
# 또는
yarn add styled-components
yarn add -D babel-plugin-styled-components

babel-plugin-styled-components는 SSR 시 Styled Components의 스타일을 추출하는 데 필요합니다.

Babel 설정

프로젝트 루트에 babel.config.js 파일을 생성하거나 수정하여 Styled Components 플러그인을 추가합니다. (Next.js는 기본적으로 Babel을 사용합니다.)

babel.config.js
// babel.config.js
module.exports = {
  presets: ['next/babel'],
  plugins: [
    [
      'babel-plugin-styled-components',
      {
        ssr: true, // SSR 시 스타일 추출 활성화
        displayName: true, // 개발 모드에서 컴포넌트 이름으로 클래스명 표시
      },
    ],
  ],
};

Styled Components Provider 설정

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

src/app/layout.tsx
// 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
// 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}
    </StyleheetManager>
  );
}

CSS-in-JS 컴포넌트 사용

이제 Styled Components를 사용하여 컴포넌트를 스타일링할 수 있습니다. 중요한 것은 스타일링된 컴포넌트가 사용되는 모든 파일은 "use client" 지시어가 있어야 한다는 것입니다.

실습: Styled Components를 사용한 UI 컴포넌트

src/app/css-in-js/MyStyledButton.tsx
// 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
// src/app/css-in-js/MyStyledButton.tsx (클라이언트 컴포넌트)
"use client"; // 🚨 반드시 필요

import styled from 'styled-components';

const ButtonContainer = styled.button`
  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>
  );
}

실습 확인

  1. 위에서 설명한 Styled Components 설치 및 Babel, Registry 설정을 완료합니다.
  2. src/app/css-in-js 폴더를 만들고 위 page.tsxMyStyledButton.tsx 파일을 생성합니다.
  3. 개발 서버(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 라이브러리 자체의 번들 크기가 추가됩니다.

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

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

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

CSS-in-JS는 React의 컴포넌트 기반 아키텍처를 스타일링 영역까지 확장하여 개발자에게 강력한 유연성과 통합된 경험을 제공합니다. Next.js App Router와 함께 사용할 때는 SSR 설정을 신중하게 적용하여 성능을 최적화하는 것이 중요합니다.