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를 작성하는 방식입니다.
"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 통합 실습으로 해석하지 않습니다.
"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>
);
}- 유연성:
styledAPI뿐만 아니라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가 style rule을 모으고 useServerInsertedHTML에서 style 결과를 얻은 뒤 clearTag로 버퍼를 비우면서 얻은 결과는 유지합니다. 이 서버 버퍼 상태를 단계로 추적하고 브라우저 직접 children 분기는 위 관계도와 표에서 독립으로 보존합니다.
NEXT · 9.3 · CSS-in-JS
Registry는 서버에서 스타일을 수집합니다
예제의 Registry는 인스턴스 상태에 시트를 보관합니다. 서버의 수집·HTML 주입과 브라우저의 children 반환은 실행 환경에 따라 나뉩니다.
| 처리 구분 | 코드의 실행 위치 | 처리 내용 |
|---|---|---|
| 시트 준비 | 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는 역할이 다릅니다. 이 표는 styled-components 예제의 분기이며, Emotion의 App Router 통합까지 구현한 예제가 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 노드 6개와 관계 5개입니다. window가 없는 서버 실행과 있는 브라우저 실행은 선택 분기이며 모바일의 아래 배치가 두 분기를 차례로 실행한다는 뜻은 아닙니다. 서버에서는 StyleSheetManager가 렌더 중 규칙을 수집하고 useServerInsertedHTML이 결과를 얻어 수집 태그를 비운 뒤 스타일을 반환합니다. 브라우저 분기는 children을 그대로 반환하고 이후 라이브러리가 동적 스타일을 처리합니다. clearTag는 이미 얻은 style 결과를 취소하지 않습니다. 그림은 원 styled-components 6 Registry 예제이며 다른 버전·Emotion 통합을 대신 구현하지 않습니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 6 | 노드 6개와 관계 5개입니다. window가 없는 서버 실행과 있는 브라우저 실행은 선택 분기이며 모바일의 아래 배치가 두 분기를 차례로 실행한다는 뜻은 아닙니다. 서버에서는 StyleSheetManager가 렌더 중 규칙을 수집하고 useServerInsertedHTML이 결과를 얻어 수집 태그를 비운 뒤 스타일을 반환합니다. 브라우저 분기는 children을 그대로 반환하고 이후 라이브러리가 동적 스타일을 처리합니다. clearTag는 이미 얻은 style 결과를 취소하지 않습니다. 그림은 원 styled-components 6 Registry 예제이며 다른 버전·Emotion 통합을 대신 구현하지 않습니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 규칙 수집 window 없는 서버 분기에서 StyleSheetManager가 렌더 중 규칙을 모읍니다. | 실행: 서버 분기 · sheet: 규칙 수집 · 브라우저 분기: 독립 | 서버 수집 버퍼가 채워집니다. |
| style 결과 캡처 useServerInsertedHTML이 현재 style 결과를 얻습니다. | 결과: getStyleElement · 버퍼: 수집 규칙 있음 | 반환할 style 결과가 버퍼와 별도로 확보됩니다. |
| 수집 태그 비우기 clearTag가 수집 태그를 비우되 얻은 style 결과를 취소하지 않습니다. | 버퍼: clearTag · 확보한 결과: 유지 | 다음 수집을 위한 버퍼 상태가 정리됩니다. |
| style 반환·삽입 확보한 style을 반환해 서버 HTML에 삽입합니다. | 반환: 확보한 style · 대상: 서버 HTML · 버전: styled-components 6 예제 | 버퍼 정리 뒤에도 style 출력이 전달됩니다. |
Registry 코드의 분기와 스타일 처리
서버 registry가 style rule을 모으고 useServerInsertedHTML에서 style 결과를 얻은 뒤 clearTag로 버퍼를 비우면서 얻은 결과는 유지합니다. 이 서버 버퍼 상태를 단계로 추적하고 브라우저 직접 children 분기는 위 관계도와 표에서 독립으로 보존합니다.
NEXT · 9.3 · CSS-in-JS
Registry는 서버에서 스타일을 수집합니다
예제의 Registry는 인스턴스 상태에 시트를 보관합니다. 서버의 수집·HTML 주입과 브라우저의 children 반환은 실행 환경에 따라 나뉩니다.
| 처리 구분 | 코드의 실행 위치 | 처리 내용 |
|---|---|---|
| 시트 준비 | 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는 역할이 다릅니다. 이 표는 styled-components 예제의 분기이며, Emotion의 App Router 통합까지 구현한 예제가 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 노드 6개와 관계 5개입니다. window가 없는 서버 실행과 있는 브라우저 실행은 선택 분기이며 모바일의 아래 배치가 두 분기를 차례로 실행한다는 뜻은 아닙니다. 서버에서는 StyleSheetManager가 렌더 중 규칙을 수집하고 useServerInsertedHTML이 결과를 얻어 수집 태그를 비운 뒤 스타일을 반환합니다. 브라우저 분기는 children을 그대로 반환하고 이후 라이브러리가 동적 스타일을 처리합니다. clearTag는 이미 얻은 style 결과를 취소하지 않습니다. 그림은 원 styled-components 6 Registry 예제이며 다른 버전·Emotion 통합을 대신 구현하지 않습니다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 6 | 노드 6개와 관계 5개입니다. window가 없는 서버 실행과 있는 브라우저 실행은 선택 분기이며 모바일의 아래 배치가 두 분기를 차례로 실행한다는 뜻은 아닙니다. 서버에서는 StyleSheetManager가 렌더 중 규칙을 수집하고 useServerInsertedHTML이 결과를 얻어 수집 태그를 비운 뒤 스타일을 반환합니다. 브라우저 분기는 children을 그대로 반환하고 이후 라이브러리가 동적 스타일을 처리합니다. clearTag는 이미 얻은 style 결과를 취소하지 않습니다. 그림은 원 styled-components 6 Registry 예제이며 다른 버전·Emotion 통합을 대신 구현하지 않습니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 규칙 수집 window 없는 서버 분기에서 StyleSheetManager가 렌더 중 규칙을 모읍니다. | 실행: 서버 분기 · sheet: 규칙 수집 · 브라우저 분기: 독립 | 서버 수집 버퍼가 채워집니다. |
| style 결과 캡처 useServerInsertedHTML이 현재 style 결과를 얻습니다. | 결과: getStyleElement · 버퍼: 수집 규칙 있음 | 반환할 style 결과가 버퍼와 별도로 확보됩니다. |
| 수집 태그 비우기 clearTag가 수집 태그를 비우되 얻은 style 결과를 취소하지 않습니다. | 버퍼: clearTag · 확보한 결과: 유지 | 다음 수집을 위한 버퍼 상태가 정리됩니다. |
| style 반환·삽입 확보한 style을 반환해 서버 HTML에 삽입합니다. | 반환: 확보한 style · 대상: 서버 HTML · 버전: styled-components 6 예제 | 버퍼 정리 뒤에도 style 출력이 전달됩니다. |
Registry의 두 분기를 읽는 방법
초기 서버 렌더에서는 Client Component도 초기 HTML을 만드는 과정에 참여합니다. 그래서 use client라는 선언과 typeof window 조건을 같은 구분으로 읽지 않습니다. 원 Registry는 인스턴스에 만든 시트를 StyleSheetManager에 전달해 서버 렌더의 스타일을 수집하고, useServerInsertedHTML 콜백이 수집분을 HTML에 돌려줍니다. clearTag는 이미 얻은 스타일 결과를 취소하는 코드가 아니라 다음 주입에 같은 수집 태그를 반복하지 않도록 비우는 단계입니다.
브라우저에서는 window가 있으므로 Registry가 children을 그대로 반환합니다. 이후의 동적 스타일은 라이브러리가 처리합니다. 이때 서버가 prop으로 넘긴 children의 모든 모듈이 자동으로 클라이언트 모듈로 바뀌는 것은 아닙니다. 이 구성의 단계와 지원 조건은 Next CSS-in-JS 공식 안내에서 확인할 수 있습니다.
Next.js App Router는 기본적으로 React 서버 컴포넌트를 사용하므로, CSS-in-JS 라이브러리 사용 시 몇 가지 특별한 설정이 필요합니다.
이 절의 런타임 스타일 컴포넌트는 클라이언트 경계 안에서 정의합니다. 이 컴포넌트를 가져와 배치하는 상위 페이지까지 클라이언트 컴포넌트가 될 필요는 없습니다.
같은 6판 안에서도 지원 경로를 구별하기
아래 코드는 Client Component의 스타일을 SSR에서 수집하는 Registry 경로입니다. styled-components 공식 RSC 안내는 6.3.0부터 Server Component에서 직접 사용하는 스타일의 네이티브 RSC 지원도 설명합니다. 그 서버 전용 경로에서는 스타일 정의 때문에 use client나 이 Registry를 추가할 필요가 없지만, Client Component 스타일의 SSR에는 여전히 Registry가 필요합니다. 따라서 “CSS-in-JS는 항상 Client Component에서만 쓸 수 있다”거나 “6판이면 Registry가 언제나 불필요하다”로 일반화하지 않습니다.
이 절에서는 기존 Client Component 예제를 유지합니다. 설치한 세부 버전·App Router·컴포넌트의 서버/클라이언트 위치를 먼저 확인하고 그에 맞는 공식 통합 경로를 선택합니다. 서버 네이티브 경로의 ThemeProvider는 Context를 제공하지 않는 pass-through 동작이므로, 클라이언트 Context 테마 예제를 그 경로로 그대로 옮겨 같은 결과를 기대하지 않습니다.
또한 SSR 시 스타일이 올바르게 추출되어 초기 HTML에 포함되도록 추가 설정이 필요합니다.
여기서는 Styled Components를 예시로 통합 방법을 설명합니다.
이 실습에서 선택할 버전과 파일
아래 Registry와 enableVendorPrefixes 설명은 styled-components 6판을 기준으로 읽습니다. 현재 API 안내는 7판에서 접두사 설정이 플러그인 방식으로 바뀐 조건을 따로 설명합니다. 버전을 지정하지 않은 원 설치 명령이 앞으로도 같은 판을 고른다고 가정하지 마세요.
root layout·lib/registry·MyStyledButton·page와 컴파일러 설정을 모두 준비한 뒤 /css-in-js에서 확인합니다. 먼저 layout만 저장하면 아직 만들지 않은 registry를 import하는 오류가 날 수 있습니다. 원 Emotion 코드는 문법을 비교하는 부분 예제이고 이 styled-components Registry의 대체 파일이 아닙니다.
Emotion은 App Router 지원이 완성된 것으로 간주하지 않으며, 공식 지원 상태가 바뀌기 전까지 아래 통합 절차의 대체재로 사용하지 않습니다.
문법 비교 코드의 분기를 먼저 읽기
앞의 Emotion MyEmotionButton에서 danger=true이면 danger용 css를 붙인 일반 button을 바로 반환합니다. 이 분기는 StyledEmotionButton을 거치지 않으므로, 그 컴포넌트에만 선언한 padding·border-radius까지 자동으로 합쳐지지 않습니다. danger와 primary를 모두 true로 넘겨도 먼저 반환하는 danger 분기를 택합니다. 공통 모양을 두 분기에서 유지하려면 공통 스타일을 별도로 공유하거나 같은 스타일 컴포넌트 안에서 변형을 표현하도록 설계해야 합니다.
스타일 컴포넌트 정의도 React 컴포넌트 함수 바깥에서 유지합니다. 렌더 함수 안에서 매번 새 styled.button 정의를 만들면 React가 새 컴포넌트 유형으로 볼 수 있어 자식 상태나 입력 포커스가 다시 만들어지는 문제가 생길 수 있습니다. 원 예제처럼 모듈 수준에서 한 번 정의하고 props를 바꾸어 사용하세요. styled-components 사용 주의점도 이 정의 위치를 구분합니다.
Styled Components 설치
npm install styled-components
# 또는
yarn add styled-components원 명령은 보존했습니다. 이 절의 6판을 선택해 새 실습을 재현할 때는 위의 npm 또는 yarn 설치 한 줄 대신 다음 중 자신의 패키지 관리자를 사용하는 한 줄을 실행합니다. 두 버전을 같은 프로젝트에 차례로 설치하는 절차가 아닙니다. 이미 다른 판을 사용하는 프로젝트라면 먼저 의존성 버전과 마이그레이션 범위를 확인합니다.
npm install styled-components@6
# yarn을 사용하는 프로젝트의 대안: yarn add styled-components@6Next.js는 기본 컴파일러로 SWC를 사용합니다.
Styled Components 변환도 SWC 옵션으로 활성화하므로 Babel 플러그인은 설치하지 않습니다.
Next.js 컴파일러 설정
프로젝트 루트의 next.config.ts에서 Styled Components 변환을 켭니다.
기존 nextConfig를 통째로 버리지 않고 compiler.styledComponents 필드를 기존 설정에 합칩니다. 이미 있는 다른 compiler 옵션과 앞 장에서 선택한 설정도 유지합니다. 이 변환 설정은 스타일 식별자 등의 컴파일 작업이고, 아래 Registry가 수행하는 렌더 중 수집·HTML 주입을 대신하지 않습니다.
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을 감싸는 것만으로 그 하위 전체가 클라이언트 모듈로 바뀌지는 않습니다.
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가 스타일을 올바르게 추출하고 주입하도록 돕는 유틸리티 컴포넌트를 생성해야 합니다.
"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을 렌더링합니다.
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>
);
}"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의 장단점
최종 버튼 코드로 결과를 예상하기
앞의 StyledButton.jsx 문법 소개와 지금 MyStyledButton.tsx 실습은 서로 다른 코드입니다. 최종 MyStyledButton에 primary=true를 넘기고 외부 규칙이 없다고 가정합니다. 기본 배경·글자색, 마우스를 올릴 때 바뀌는 속성을 적어 보세요. $primary가 DOM의 일반 속성으로 전달되는지, SWC 옵션만 켜고 Registry를 준비하지 않은 경우 서버 스타일 수집까지 구현했다고 말할 수 있는지도 판단합니다.
props·CSS 규칙·서버 수집을 나눈 해설
최종 버튼의 배경은 #007bff, 글자색은 white입니다. 이 코드의 hover는 이동과 그림자를 바꾸며 배경색을 다시 선언하지 않습니다. 앞의 문법 소개에서 보았던 #0056b3를 최종 코드에도 있다고 가정하지 않습니다. $primary는 styled-components의 transient prop이므로 스타일 계산에 쓰되 실제 DOM에 같은 이름의 속성으로 전달하지 않습니다.
컴파일러 설정과 서버 수집은 별도 책임입니다. SWC 옵션을 켰다는 사실만으로 원 Registry의 StyleSheetManager·useServerInsertedHTML 수집과 주입 절차까지 준비되었다고 말할 수 없습니다. 실제 초기 HTML과 스트리밍·브라우저 전환 결과는 선택한 버전 묶음에서 따로 확인해야 합니다.
- 동적 스타일링: JavaScript를 활용하여 조건부 및 프롭스 기반 스타일링을 구현할 수 있습니다.
- 컴포넌트 로직과의 응집성: 스타일과 컴포넌트 로직이 한 파일에 있어 관련 코드를 찾고 관리하기 쉽습니다.
- 자동 스코핑: 스타일 충돌 걱정 없이 자유롭게 클래스 이름을 지을 수 있습니다.
- 쉬운 테마 시스템 구축: 클라이언트 경계에서는 Context API로 테마를 전달할 수 있습니다. 앞서 구분한 서버 네이티브 RSC 경로의 ThemeProvider는 Context 테마를 제공하지 않으므로 같은 사용법으로 일반화하지 않습니다.
- 사용처 추적: 컴포넌트와 스타일 정의가 가까이 있어 사용처를 찾기 쉽습니다. 실제 번들 제거 여부는 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에 직접 클래스를 추가하여 스타일을 적용합니다. 빠른 프로토타이핑과 일관된 디자인 시스템 구축에 강력합니다. (다음 절에서 다룸)
추가 연습: 스타일 분기와 수집 범위를 구별하기
- 원 Emotion MyEmotionButton에
danger=true,primary=true를 함께 넘깁니다. 어떤 요소를 반환하고, StyledEmotionButton에만 정의된 padding·border-radius가 이 분기에도 자동 적용될까요? 외부 CSS는 없다고 가정합니다. - 하나의 원 Registry가 서버 스트림의 첫 주입 직전에 A 규칙, 두 번째 직전에 B 규칙을 수집합니다. 각
getStyleElement뒤clearTag를 호출했을 때 각 주입의 규칙은 무엇인가요? 첫 HTML에 이미 보낸 A가 취소되는지, 서로 다른 요청의 Registry 시트를 전역 하나로 공유해도 되는지도 설명하세요. - styled-components 6.3 이상에서 서버 전용 스타일 컴포넌트를 사용하는 경우와, 이 절처럼 Client Component의 스타일을 초기 서버 HTML에 수집하는 경우를 비교합니다. 두 경우 모두 Registry가 불필요한가요? 서버 네이티브 경로의 ThemeProvider로 클라이언트 Context 테마와 같은 동작을 기대할 수 있나요?
- 입력창을 가진 자식의 스타일 컴포넌트를 부모의 렌더 함수 안에서 매번 정의합니다. 부모가 다시 렌더될 때 자식 상태·포커스가 재생성될 위험의 원인을 쓰고, 원 코드의 정의 위치를 기준으로 개선하세요. CSS 색을 바꾸는 것 자체와 컴포넌트 유형을 다시 만드는 것을 구별합니다.
원 코드의 실행 경로와 버전 조건을 따른 풀이
- 먼저 검사하는 danger 분기의 일반 button입니다. danger css의 빨강 배경과 해당 hover 규칙은 있지만, StyledEmotionButton을 렌더하지 않아 그 정의에만 있는 공통 padding·border-radius가 자동 합성되지는 않습니다. primary=true가 뒤 분기로 넘어가게 만들지도 않습니다.
- 첫 주입은 A, clearTag 후 새 수집분을 얻는 두 번째 주입은 B입니다. 이미 첫 HTML로 보낸 A를 브라우저에서 지우는 동작이 아닙니다. 원 Registry처럼 렌더 인스턴스별로 시트를 만들어 요청 간 규칙이 섞이지 않게 합니다. 스트리밍을 계속할 시트를 매 주입마다 seal하는 방식으로 바꾸지도 않습니다.
- 서버 전용 스타일의 네이티브 RSC 지원 경로는 스타일만을 위해 Registry를 요구하지 않습니다. 반면 Client Component 스타일의 SSR 수집은 여전히 Registry 경로를 사용하므로 둘 다 불필요하다는 답은 틀립니다. 서버 네이티브 ThemeProvider는 pass-through이므로 클라이언트 Context 테마 공급과 같은 동작을 기대하지 않습니다.
- 반복해서 만든 styled 정의가 새 React 컴포넌트 유형이 되어 기존 자식을 대체할 수 있는 것이 원인입니다. 모듈 수준의 안정된 정의를 사용하고 props로 스타일 값을 전달합니다. 색상이 달라졌다는 사실만으로 컴포넌트 유형이 바뀌는 것은 아닙니다.