주의사항 (Warning): 서버 컴포넌트(Server Components)나 스트리밍(Streaming) 같은 React의 최신 기능들과 함께 CSS-in-JS를 사용하려면, 해당 라이브러리 제작자가 동시성 렌더링(concurrent rendering)을 포함한 최신 버전의 React를 지원해야만 합니다.
💡 강사의 부연 설명 & 팁: React 18부터 도입된 '동시성 렌더링'이나 Next.js의 '서버 컴포넌트' 환경에서는 UI가 서버에서 조각조각 나뉘어 스트리밍 방식으로 클라이언트에 전달됩니다. 과거의 CSS-in-JS 라이브러리들은 브라우저(클라이언트)가 렌더링될 때 동적으로 <style> 태그를 삽입하는 방식이었기 때문에, 이런 최신 서버 렌더링 환경에서는 화면이 깜빡이거나(FOUC 현상) 스타일이 깨지는 문제가 발생할 수 있어요. 그래서 반드시 최신 React를 완벽히 지원하는 라이브러리를 선택하는 것이 매우 중요합니다!
현재 app 디렉토리 내부의 클라이언트 컴포넌트(Client Components)에서 공식적으로 지원되는 라이브러리들은 다음과 같습니다 (알파벳 순서):
ant-designchakra-ui@fluentui/react-componentskuma-ui@mui/material@mui/joypandacssstyled-jsxstyled-componentsstylextamaguitss-reactvanilla-extract다음 라이브러리는 현재 지원을 위해 개발/작업 중입니다:
💡 강사의 실무 팁:
리스트를 보시면 vanilla-extract나 pandacss 같은 라이브러리들이 눈에 띄죠? 최근 실무 트렌드는 런타임(브라우저 실행 시점)에 CSS를 생성해서 성능을 깎아먹는 전통적인 방식보다는, 빌드 타임에 CSS를 미리 뽑아내는 Zero-runtime CSS-in-JS 라이브러리들을 선호하는 추세입니다. emotion이 아직 작업 중이라는 점이 아쉽지만, 새로운 프로젝트를 시작하신다면 Zero-runtime 라이브러리를 도입해 보시는 것도 강력히 추천합니다!
알아두면 좋은 점 (Good to know): 저희(Next.js 팀)는 현재 다양한 CSS-in-JS 라이브러리들을 테스트하고 있으며, React 18의 기능들이나
app디렉토리를 지원하는 라이브러리들에 대한 예제를 앞으로 더 많이 추가할 예정입니다.
app 디렉토리에서 CSS-in-JS 설정하기app 라우터 환경에서 CSS-in-JS를 설정하는 과정은 사용자가 직접 세팅해야 하는(opt-in) 3단계 프로세스로 이루어져 있습니다:
useServerInsertedHTML을 사용합니다.💡 강사의 부연 설명:
"왜 이렇게 복잡하게 설정해야 하죠?"라고 생각하실 수 있어요. 예전 pages 라우터 시절에는 _document.js 파일 하나에서 모든 걸 처리했죠. 하지만 app 라우터는 서버 컴포넌트가 중심이 되기 때문에, 서버에서 렌더링될 때 발생하는 스타일 태그들을 차곡차곡 모아놨다가 클라이언트로 한 번에 깔끔하게 넘겨주는 '바구니(레지스트리)' 역할이 필요해진 거랍니다.
styled-jsx 설정하기클라이언트 컴포넌트에서 styled-jsx를 사용하시려면 반드시 v5.1.0 버전을 사용해야 합니다.
자, 먼저 새로운 레지스트리 파일을 만들어 볼까요?
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
export default function StyledJsxRegistry({
children,
}: {
children: React.ReactNode
}) {
// Only create stylesheet once with lazy initial state
// x-ref: [https://reactjs.org/docs/hooks-reference.html#lazy-initial-state](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state)
const [jsxStyleRegistry] = useState(() => createStyleRegistry())
useServerInsertedHTML(() => {
const styles = jsxStyleRegistry.styles()
jsxStyleRegistry.flush()
return <>{styles}</>
})
return <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>
}
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
export default function StyledJsxRegistry({ children }) {
// Only create stylesheet once with lazy initial state
// x-ref: [https://reactjs.org/docs/hooks-reference.html#lazy-initial-state](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state)
const [jsxStyleRegistry] = useState(() => createStyleRegistry())
useServerInsertedHTML(() => {
const styles = jsxStyleRegistry.styles()
jsxStyleRegistry.flush()
return <>{styles}</>
})
return <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>
}
💡 강사의 부연 설명:
코드 중간에 useState(() => createStyleRegistry()) 보이시죠? 이렇게 함수 형태로 초기값을 넣는 걸 '게으른 초기화(Lazy initialization)'라고 부릅니다. 컴포넌트가 리렌더링 될 때마다 불필요하게 레지스트리가 다시 생성되는 걸 막아주는 리액트의 아주 유용한 패턴이에요. 그리고 flush() 메서드는 모아둔 스타일을 비워주는 역할을 해서 동일한 스타일이 중복으로 들어가는 걸 막아줍니다.
그다음, 방금 만든 레지스트리 컴포넌트를 이용해 최상위 레이아웃(root layout)을 감싸주면 됩니다:
import StyledJsxRegistry from './registry'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<StyledJsxRegistry>{children}</StyledJsxRegistry>
</body>
</html>
)
}
import StyledJsxRegistry from './registry'
export default function RootLayout({ children }) {
return (
<html>
<body>
<StyledJsxRegistry>{children}</StyledJsxRegistry>
</body>
</html>
)
}
이번에는 실무에서 정말 많이 쓰이는 styled-components (버전 6 이상)를 설정하는 방법입니다.
먼저, next.config.js 파일에서 styled-components 사용을 활성화(true)로 켜주세요.
module.exports = {
compiler: {
styledComponents: true,
},
}
💡 강사의 팁: 예전에는 babel-plugin-styled-components 같은 외부 플러그인을 따로 설치해서 복잡하게 설정해야 했지만, 이제는 Next.js가 자체 내장 컴파일러(SWC)를 통해 이 설정을 지원해 주기 때문에 저렇게 코드 몇 줄만 넣으면 알아서 최적화가 끝납니다. 엄청 편해졌죠!
그다음, styled-components API를 사용해서 서버 렌더링 중 발생하는 모든 CSS 스타일 규칙을 수집할 글로벌 레지스트리 컴포넌트를 만들고, 그 규칙들을 반환해 주는 함수를 작성해야 합니다. 그리고 useServerInsertedHTML 훅을 사용해서 수집된 스타일들을 최상위 레이아웃의 <head> 태그 안에 주입(inject)해 줍니다.
'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
}) {
// Only create stylesheet once with lazy initial state
// x-ref: [https://reactjs.org/docs/hooks-reference.html#lazy-initial-state](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state)
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}</>
})
if (typeof window !== 'undefined') return <>{children}</>
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
)
}
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
export default function StyledComponentsRegistry({ children }) {
// Only create stylesheet once with lazy initial state
// x-ref: [https://reactjs.org/docs/hooks-reference.html#lazy-initial-state](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state)
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}</>
})
if (typeof window !== 'undefined') return <>{children}</>
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
)
}
마지막으로 최상위 레이아웃(root layout)의 children을 우리가 만든 스타일 레지스트리 컴포넌트로 감싸줍니다:
import StyledComponentsRegistry from './lib/registry'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
)
}
import StyledComponentsRegistry from './lib/registry'
export default function RootLayout({ children }) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
)
}
반드시 알아두면 좋은 점 (Good to know):
- 서버 렌더링을 하는 동안, 스타일들은 글로벌 레지스트리로 추출된 다음 여러분의 HTML
<head>태그 안으로 전부 쏟아져 들어갑니다(flushed). 이렇게 하면 해당 스타일을 사용하는 실제 콘텐츠들이 화면에 그려지기 전에 스타일 규칙들이 미리 자리를 잡게 되죠. 참고로, 앞으로는 React의 새로운 기능을 사용해서 이 스타일들을 정확히 어디에 주입할지 결정하는 방식으로 바뀔 수도 있습니다.- 스트리밍(streaming) 과정 중에는, 데이터를 쪼개서 가져오는 각 청크(chunk) 단위마다 스타일을 수집해서 기존 스타일에 차곡차곡 이어 붙이게 됩니다. 이후 클라이언트 측에서 하이드레이션(hydration)이 완료되고 나면, 평소처럼
styled-components가 주도권을 넘겨받아 동적인 스타일들을 알아서 주입하게 됩니다.- 💡 (강사 부연: '하이드레이션'이란 서버에서 만들어진 뼈대 HTML에 리액트가 생명(이벤트 리스너, 상태 등)을 불어넣는 과정을 말해요!)
- 저희가 굳이 스타일 레지스트리를 트리 최상단(top level)에서 클라이언트 컴포넌트(
'use client')로 사용하는 이유가 있습니다. 이렇게 하는 것이 CSS 규칙들을 추출해 내는 데 훨씬 효율적이기 때문이에요. 이 방식을 쓰면 다음번 서버 렌더링 때 스타일을 불필요하게 다시 생성하는 걸 막아주고, 서버 컴포넌트의 페이로드(전송 데이터 크기)에 스타일 데이터가 섞여 들어가는 것도 방지할 수 있습니다.styled-components를 컴파일할 때 개별적인 속성들을 세밀하게 설정해야 하는 고급 사용자 분들은, Next.js styled-components API 레퍼런스 문서를 통해 더 자세한 내용을 확인하실 수 있습니다.
모든 문서에 대한 의미론적 개요(semantic overview)를 보시려면 https://nextjs.org/docs/sitemap.md 를 참고해 주세요.
사용 가능한 전체 문서의 색인(index)을 확인하시려면 https://nextjs.org/docs/llms.txt 를 참고해 주세요.