Next.js App Router 가이드 정리: CSS-in-JS


1. 개요

  • 이 문서는 Next.js App Router(app 디렉터리) 환경에서 CSS-in-JS 라이브러리를 사용하는 방법을 설명한다.
  • React 18의 Concurrent Rendering, Streaming 과 같은 새로운 기능과 함께 사용하려면, 해당 CSS-in-JS 라이브러리가 최신 React 버전을 지원해야 한다는 점을 경고하고 있다.
  • 문서에서 소개하는 라이브러리들은 모두 Client Component에서 사용 가능 한 것을 전제로 한다.

2. 지원되는 CSS-in-JS 라이브러리

문서가 명시적으로 지원한다고 밝힌 라이브러리(알파벳 순)는 다음과 같다.

  • ant-design
  • chakra-ui
  • @fluentui/react-components
  • kuma-ui
  • @mui/material
  • @mui/joy
  • pandacss
  • styled-jsx
  • styled-components
  • stylex
  • tamagui
  • tss-react
  • vanilla-extract

추가로, emotion 은 지원을 개발 중인 라이브러리로 언급된다.

문서는 향후 React 18 및 app 디렉터리를 지원하는 CSS-in-JS 라이브러리에 대해 더 많은 예시를 추가할 계획이라고 안내한다.


3. App에서 CSS-in-JS를 설정하는 공통 3단계

Next.js App Router에서 CSS-in-JS를 제대로 SSR/Streaming과 함께 사용하려면, 다음 3단계 opt‑in 구조를 따라야 한다.

  1. 스타일 레지스트리(Style Registry)

    • 한 번의 렌더링에서 생성되는 모든 CSS 규칙을 수집/보관하는 레지스트리를 만든다.
    • 보통 Client Component로 구현한다.
  2. useServerInsertedHTML 훅 사용

    • next/navigation에서 제공하는 useServerInsertedHTML 훅을 사용하여, 수집한 스타일을 실제 HTML 콘텐츠가 렌더링되기 전에 <head> 에 주입 한다.
    • 이렇게 해야 스타일이 내용보다 먼저 로딩되어 FOUC(unstyled content)가 줄어든다.
  3. Root Layout에서 레지스트리로 앱 감싸기

    • app/layout.tsx(Root Layout)에서 스타일 레지스트리 컴포넌트로 전체 앱 트리를 감싼다.
    • 초기 서버 렌더링 시 스타일이 올바르게 삽입되고, 이후 클라이언트 하이드레이션 이후에는 라이브러리가 평소처럼 동작하도록 하기 위함이다.

이 3단계 패턴은 styled-jsx, styled-components 모두에서 동일하게 사용된다.


4. styled-jsx 설정 방법

4.1 styled-jsx 버전 요구사항

  • App Router의 Client Component에서 styled-jsx를 사용하려면 v5.1.0 이상을 사용해야 한다고 문서에서 명시한다.

4.2 스타일 레지스트리 컴포넌트 만들기

  • app/registry.tsx 같은 파일을 만들고, 다음과 같은 역할을 하는 Client Component를 작성한다.
    • createStyleRegistry()로 스타일 레지스트리를 생성한다.
    • React useState의 lazy initial state 기능을 사용하여 한 번만 레지스트리를 만들고 재사용 한다.
    • useServerInsertedHTML 훅 내부에서:
      • registry.styles()로 현재까지 수집된 스타일을 가져온 뒤,
      • registry.flush()를 호출하여 레지스트리를 비우고,
      • 반환된 스타일을 JSX로 감싸 반환한다.
    • 컴포넌트는 <StyleRegistry> 로 children을 감싸서 렌더링한다.

이 구조를 통해 서버 렌더링 시점에 수집된 모든 styled-jsx 스타일이 <head> 앞부분에 삽입된다.

4.3 Root Layout에서 사용하기

  • app/layout.tsx에서 위에서 만든 StyledJsxRegistry(예시 이름)를 import 한다.
  • Root Layout의 <body> 안에서 childrenStyledJsxRegistry로 감싸면, 전체 앱에서 생성되는 styled-jsx 스타일이 모두 레지스트리를 통해 관리된다.
// 개념 예시
import StyledJsxRegistry from './registry'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <StyledJsxRegistry>{children}</StyledJsxRegistry>
      </body>
    </html>
  )
}

5. styled-components 설정 방법

이 섹션은 styled-components@6 이상을 기준으로 한다.

5.1 next.config.js 설정

  • 먼저 next.config.js에서 compiler.styledComponents 옵션을 활성화한다.
// next.config.js (개념 예시)
module.exports = {
  compiler: {
    styledComponents: true,
  },
}

이 설정을 통해 Next.js는 styled-components용 트랜스파일 과정을 적용한다.

5.2 styled-components용 레지스트리 컴포넌트 만들기

  • 예: lib/registry.tsx 파일에 StyledComponentsRegistry 컴포넌트를 구현한다.
  • 주요 역할:
    1. new ServerStyleSheet() 로 서버용 스타일 시트를 생성한다.
    2. useState의 lazy initial state를 이용해 한 번만 스타일 시트를 만들고 재사용한다.
    3. useServerInsertedHTML 훅 안에서:
      • getStyleElement() 로 현재까지 수집된 스타일 엘리먼트를 가져온다.
      • instance.clearTag() 로 내부 스타일 태그를 초기화한다.
      • JSX로 스타일 엘리먼트를 반환하여 <head> 에 삽입되도록 한다.
    4. 브라우저 환경(typeof window !== 'undefined')에서는 서버 스타일 시트 없이 그대로 children만 반환한다.
    5. 서버 환경에서는 <StyleSheetManager sheet={styledComponentsStyleSheet.instance}> 로 children을 감싸서, styled-components가 해당 시트를 사용하도록 한다.

이 구조를 통해, 서버 렌더링 시 수집된 styled-components 스타일이 <head>에 삽입되고, 스트리밍 시 각 청크의 스타일이 기존 스타일에 붙는다.

5.3 Root Layout에서 레지스트리 사용

  • app/layout.tsx에서 StyledComponentsRegistry를 import 하고, Root Layout의 <body> 안에서 children을 감싼다.
import StyledComponentsRegistry from './lib/registry'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  )
}

5.4 동작 방식에 대한 메모

styled-components 동작 특성은 다음과 같다.

  • 서버 렌더링 동안
    • 스타일은 전역 레지스트리에 누적되고, <head> 로 플러시(flush) 된다.
    • 스타일은 실제 콘텐츠보다 먼저 삽입되어, 스타일이 적용되지 않은 상태로 보이는 시간을 줄인다.
  • 스트리밍 중
    • 각 스트리밍 청크에서 나온 스타일은 기존 스타일에 계속 이어 붙는다.
  • 클라이언트 하이드레이션 이후
    • 이후에는 평소와 같이 styled-components가 동작하며, 동적 스타일을 포함한 추가 스타일을 클라이언트에서 직접 주입한다.
  • 스타일 레지스트리를 트리의 최상단 Client Component로 두는 이유:
    • 서버 렌더링 시 CSS 규칙을 더 효율적으로 추출할 수 있다.
    • 매 요청마다 스타일을 다시 생성하지 않고 재사용하여 불필요한 오버헤드를 줄인다.
    • 스타일이 Server Component 페이로드에 포함되는 것을 방지하여 전송량을 줄인다.

6. 설계 시 고려사항 정리

  1. 어떤 컴포넌트에서 CSS-in-JS를 사용할 것인지

    • 문서의 전제는, 소개된 CSS-in-JS 라이브러리들이 Client Component에서 사용 된다는 점이다.
    • Server Component에서는 CSS Modules, 전역 CSS, Tailwind CSS 등 파일 기반 스타일링을 우선 고려하고, Client Component 경계에서 CSS-in-JS를 사용하는 패턴이 일반적이다(이 부분은 Next.js 전반 스타일링 가이드를 기반으로 한 일반적인 권장 사항이다).
  2. React 18 기능 지원 여부 확인

    • 사용하는 CSS-in-JS 라이브러리가 Concurrent Rendering, Streaming, Server Components 와의 호환성을 공식적으로 밝히고 있는지 확인하는 것이 중요하다.
  3. SSR 및 Streaming과의 통합

    • useServerInsertedHTML 훅과 스타일 레지스트리를 사용하지 않으면, 서버 렌더링 시 스타일이 제대로 삽입되지 않아 FOUC나 스타일 미적용 문제가 발생할 수 있다.
  4. 성능과 DX 균형

    • CSS-in-JS는 컴포넌트 단위로 스타일을 캡슐화하고 타입/테마 통합 등에 장점이 있지만, 런타임 비용이 존재한다.
    • SSR/Streaming 환경에서는 스타일 추출 전략과 캐싱 전략을 함께 고려해야 한다.