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 (예시)
"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
헬퍼 함수를 사용하여 클래스 기반으로 스타일을 적용할 수 있습니다. - 성능: 제로 런타임(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
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
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}
</StyleheetManager>
);
}
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`
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 설치 및 Babel, 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와 함께 사용하여 전역 테마를 쉽게 적용하고 변경할 수 있습니다.
- 데드 코드 제거: 사용되지 않는 컴포넌트와 그 스타일이 빌드 시 자동으로 제거됩니다.
단점
- 학습 곡선: 새로운 문법과 개념을 배워야 합니다.
- 런타임 오버헤드: 스타일을 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 설정을 신중하게 적용하여 성능을 최적화하는 것이 중요합니다.