Next.js 통합
Next.js 통합 (Next.js integration)
Material UI를 Next.js와 함께 사용하는 방법을 배워볼게요.
출처: 문서
본문
:::success
Material UI + Next.js 에이전트 스킬을 사용하면 App Router, SSR 캐시 프로바이더, next/font, CSS 레이어를 포함한 이 통합에 대한 완전한 맥락을 AI 코딩 어시스턴트에게 줄 수 있어요.
:::
App Router
이 절은 Next.js App Router와의 Material UI 통합을 안내해요. App Router는 Pages Router의 진화형이며, 현재 버전 13부터 새로운 Next.js 애플리케이션을 만드는 데 권장되는 방식이에요.
의존성 설치하기 (Installing the dependencies)
먼저 @mui/material과 next가 이미 설치되어 있는지 확인하세요. 그런 다음 아래 명령 중 하나를 실행해서 의존성을 설치해요:
npm install @mui/material-nextjs @emotion/cache
pnpm add @mui/material-nextjs @emotion/cache
yarn add @mui/material-nextjs @emotion/cache
설정 (Configuration)
app/layout.tsx 안에서 AppRouterCacheProvider를 임포트하고 <body> 아래의 모든 요소를 감싸주세요:
+import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
// or `v1X-appRouter` if you are using Next.js v1X
export default function RootLayout(props) {
return (
<html lang="en">
<body>
+ <AppRouterCacheProvider>
{props.children}
+ </AppRouterCacheProvider>
</body>
</html>
);
}
:::info
AppRouterCacheProvider 컴포넌트는 Next.js가 .html 페이지의 청크를 클라이언트로 스트리밍하는 동안 서버에서 MUI System이 생성한 CSS를 수집하는 역할을 해요.
AppRouterCacheProvider 컴포넌트를 사용하는 것이 필수는 아니지만, 스타일이 <head>에 추가되고 <body>에 렌더링되지 않도록 보장하려면 사용을 권장해요. 왜 더 나은지에 대해서는 https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 를 참고하세요.
:::
커스텀 캐시 (선택 사항)
options prop을 사용해 기본 cache options을 덮어쓸 수 있어요. 예를 들어 아래 코드 스니펫은 CSS key를 css로 바꾸는 방법을 보여줘요 (기본값은 mui):
<AppRouterCacheProvider
+ options={{ key: 'css' }}
>
{children}
</AppRouterCacheProvider>
폰트 최적화 (Font optimization)
Next.js font optimization을 Material UI와 통합하려면 'use client'; 지시문이 있는 새 파일을 만드세요. 그런 다음 typography.fontFamily 필드의 값으로 var(--font-roboto)를 사용하는 테마를 생성해요.
'use client';
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
typography: {
fontFamily: 'var(--font-roboto)',
},
});
export default theme;
마지막으로 src/app/layout.tsx에서 테마를 ThemeProvider에 전달해요:
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
+import { Roboto } from 'next/font/google';
+import { ThemeProvider } from '@mui/material/styles';
+import theme from '../theme';
+const roboto = Roboto({
+ weight: ['300', '400', '500', '700'],
+ subsets: ['latin'],
+ display: 'swap',
+ variable: '--font-roboto',
+});
export default function RootLayout(props) {
const { children } = props;
return (
+ <html lang="en" className={roboto.variable}>
<body>
<AppRouterCacheProvider>
+ <ThemeProvider theme={theme}>
{children}
+ </ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}
테마에 대해 더 배우고 싶다면 theming 가이드 페이지를 확인해보세요.
CSS 테마 변수 (CSS theme variables)
CSS 테마 변수를 사용하려면 cssVariables 플래그를 활성화하세요:
'use client';
const theme = createTheme({
+ cssVariables: true,
});
CSS 테마 변수의 장점과 SSR 깜빡임 방지 방법에 대해 더 알아보세요.
다른 스타일링 솔루션 사용하기 (Using other styling solutions)
Emotion 외의 스타일링 솔루션으로 Material UI 컴포넌트를 커스터마이즈한다면, options prop에서 enableCssLayer: true를 설정하세요:
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
이 옵션은 Material UI가 생성한 스타일이 CSS @layer mui 규칙에 감싸지도록 보장해요. 이 규칙은 CSS Modules, Tailwind CSS, 또는 @layer 없이 작성한 일반 CSS와 함께 Material UI를 사용할 때 익명 레이어 스타일에 의해 덮어써져요.
이에 대해 더 배우려면 MDN CSS layer 문서를 참고하세요.
Next.js v16 클라이언트 컴포넌트 제약 (Next.js v16 Client Component restriction)
Next.js Link를 Material UI의 component prop에 전달할 때 Functions cannot be passed directly to Client Components 오류가 발생한다면, 아래처럼 use client 지시문이 있는 래퍼 컴포넌트를 만들어야 해요:
'use client';
import Link, { LinkProps } from 'next/link';
export default Link;
그런 다음 Next.js Link를 래퍼 컴포넌트로 교체하세요:
- import Link from 'next/link';
+ import Link from '../components/Link';
...
<Button component={Link} href="/about" variant="contained">
Go to About Page
</Button>
URL 기반 UI와 Suspense 경계 (URL-driven UI and the Suspense boundary)
클라이언트 컴포넌트가 URL을 읽는 Next.js App Router 훅을 사용할 때 — 예를 들어 필터, 탭, 페이지네이션에 next/navigation의 useSearchParams()를 사용하는 경우 — Next.js는 React 트리에서 해당 부분 주위에 <Suspense> 경계를 기대해요. 없으면 빌드 실패 또는 Suspense 경계 누락에 대한 런타임 메시지를 볼 수 있어요 (동작은 Next.js 버전과 정적/동적 렌더링 여부에 따라 달라져요).
이 패턴은 Material UI에서 흔해요: Table, Tabs, TextField 및 기타 컨트롤은 종종 쿼리 문자열에 동기화되는 클라이언트 컴포넌트로 구현되곤 해요.
권장 구조: 가능하면 page.tsx를 서버 컴포넌트로 유지하고, useSearchParams를 호출하는 클라이언트 하위 트리만 <Suspense>로 감싸세요.
레이아웃에서 공간을 예약하는 UI(툴바, 필터, 탭 바 등)에는 fallback={null}(또는 빈 fallback)을 피하세요. 그러면 서버와 초기 스트리밍 HTML이 해당 하위 트리를 생략하고, 실제 콘텐츠는 클라이언트 하이드레이션 후에만 나타나게 되어 레이아웃 시프트가 자주 발생하고 CLS에 악영향을 줘요. 최종 UI의 크기와 구조에 근접한 fallback을 선호하세요. 예를 들어 로드되는 컴포넌트와 같은 minHeight, flex 방향, 브레이크포인트를 가진 Stack이나 Box 안의 Material UI Skeleton을 사용하면 돼요.
import { Suspense } from 'react';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import OrdersToolbar from './OrdersToolbar';
function OrdersToolbarFallback() {
return (
<Stack
direction="row"
spacing={2}
useFlexGap
sx={{ flexWrap: 'wrap', alignItems: 'center', minHeight: 56 }}
>
<Skeleton
variant="rounded"
height={40}
sx={{ minWidth: 200, flexGrow: { xs: 1, sm: 0 } }}
/>
<Skeleton variant="rounded" width={120} height={40} />
<Box sx={{ flexGrow: 1 }} />
<Skeleton variant="rounded" width={100} height={40} />
</Stack>
);
}
export default function Page() {
return (
<Suspense fallback={<OrdersToolbarFallback />}>
<OrdersToolbar />
</Suspense>
);
}
OrdersToolbar는 useSearchParams()를 호출하고 Material UI 컴포넌트를 렌더링하는 'use client'로 표시된 파일이 될 거예요. fallback의 레이아웃과 Skeleton 크기를 실제 툴바(또는 필터 행)와 최대한 가깝게 조정하세요.
자세한 내용과 버전별 참고 사항은 useSearchParams의 Next.js 문서를 참고하세요.
Pages Router
이 절은 Server-side Rendering (SSR)과 Static Site Generation (SSG) 모두에 대해 Next.js Pages Router와의 Material UI 통합을 안내해요.
의존성 설치하기 (Installing the dependencies)
먼저 @mui/material과 next가 이미 설치되어 있는지 확인하세요. 그런 다음 아래 명령 중 하나를 실행해서 의존성을 설치해요:
npm install @mui/material-nextjs @emotion/cache @emotion/server
pnpm add @mui/material-nextjs @emotion/cache @emotion/server
yarn add @mui/material-nextjs @emotion/cache @emotion/server
설정 (Configuration)
pages/_document.tsx 파일 안에서:
documentGetInitialProps를 임포트하고 이를 Document의getInitialProps로 사용하세요.DocumentHeadTags를 임포트하고<Head>안에서 렌더링하세요.
+import {
+ DocumentHeadTags,
+ documentGetInitialProps,
+} from '@mui/material-nextjs/v15-pagesRouter';
// or `v1X-pagesRouter` if you are using Next.js v1X
export default function MyDocument(props) {
return (
<Html lang="en">
<Head>
+ <DocumentHeadTags {...props} />
...
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
+MyDocument.getInitialProps = async (ctx) => {
+ const finalProps = await documentGetInitialProps(ctx);
+ return finalProps;
+};
그런 다음 pages/_app.tsx 안에서 AppCacheProvider 컴포넌트를 임포트하고 루트 요소로 렌더링하세요:
+import { AppCacheProvider } from '@mui/material-nextjs/v15-pagesRouter';
// Or `v1X-pages` if you are using Next.js v1X
export default function MyApp(props) {
return (
+ <AppCacheProvider {...props}>
<Head>
...
</Head>
...
+ </AppCacheProvider>
);
}
:::info
AppCacheProvider 컴포넌트는 Next.js가 .html 페이지를 클라이언트로 렌더링하는 동안 서버에서 MUI System이 생성한 CSS를 수집하는 역할을 해요.
AppCacheProvider 컴포넌트를 사용하는 것이 필수는 아니지만, 스타일이 <head>에 추가되고 <body>에 렌더링되지 않도록 보장하려면 사용을 권장해요. 왜 더 나은지에 대해서는 https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 를 참고하세요.
:::
커스텀 캐시 (선택 사항)
커스텀 Emotion cache를 사용하려면 _document.tsx의 emotionCache 프로퍼티에 전달하세요:
...
MyDocument.getInitialProps = async (ctx) => {
const finalProps = await documentGetInitialProps(ctx, {
+ emotionCache: createCustomCache(),
});
return finalProps;
};
캐스케이드 레이어 (선택 사항)
cascade layers (@layer)를 활성화하려면 enableCssLayer: true로 새 캐시를 만들고 _document.tsx와 _app.tsx 양쪽의 emotionCache 프로퍼티에 전달하세요:
+import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
...
MyDocument.getInitialProps = async (ctx) => {
const finalProps = await documentGetInitialProps(ctx, {
+ emotionCache: createEmotionCache({ enableCssLayer: true }),
});
return finalProps;
};
+import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
...
const clientCache = createEmotionCache({ enableCssLayer: true });
+ export default function MyApp({ emotionCache = clientCache }) {
return (
+ <AppCacheProvider emotionCache={emotionCache}>
<Head>
...
</Head>
...
</AppCacheProvider>
);
}
앱 확장 (선택 사항)
앱에 추가 기능을 더하려면 plugins 프로퍼티에 배열을 전달하세요. 예를 들어 JSS와 styled-components를 사용한다면 서버 사이드 렌더링 스타일을 지원할 수 있어요.
각 플러그인은 다음 프로퍼티를 가져야 해요:
enhanceApp:App컴포넌트를 받아 새 앱 컴포넌트를 반환하는 고차 컴포넌트(higher-order component).resolveProps: 초기 props를 받아 새 props 객체를 반환하는 함수.
실행 시 각 플러그인의 enhanceApp이 먼저 위에서 아래로 호출되고, 그 다음에 resolveProps에 대해 이 과정이 반복돼요.
import { ServerStyleSheet } from 'styled-components';
MyDocument.getInitialProps = async (ctx) => {
const jssSheets = new JSSServerStyleSheets();
const styledComponentsSheet = new ServerStyleSheet();
try {
const finalProps = await documentGetInitialProps(ctx, {
emotionCache: createEmotionCache(),
plugins: [
{
// styled-components
enhanceApp: (App) => (props) =>
styledComponentsSheet.collectStyles(<App {...props} />),
resolveProps: async (initialProps) => ({
...initialProps,
styles: [
styledComponentsSheet.getStyleElement(),
...initialProps.styles,
],
}),
},
{
// JSS
enhanceApp: (App) => (props) => jssSheets.collect(<App {...props} />),
resolveProps: async (initialProps) => {
const css = jssSheets.toString();
return {
...initialProps,
styles: [
...initialProps.styles,
<style
id="jss-server-side"
key="jss-server-side"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: css }}
/>,
<style id="insertion-point-jss" key="insertion-point-jss" />,
],
};
},
},
],
});
return finalProps;
} finally {
styledComponentsSheet.seal();
}
};
TypeScript
TypeScript를 사용한다면 Document의 props 인터페이스에 DocumentHeadTagsProps를 추가하세요:
+import type { DocumentHeadTagsProps } from '@mui/material-nextjs/v15-pagesRouter';
// or `v1X-pagesRouter` if you are using Next.js v1X
+export default function MyDocument(props: DocumentProps & DocumentHeadTagsProps) {
...
}
폰트 최적화 (Font optimization)
Next.js font optimization을 Material UI와 통합하려면 pages/_app.tsx를 열고 typography.fontFamily 필드의 값으로 var(--font-roboto)를 사용하는 테마를 생성하세요.
import * as React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/app';
import { AppCacheProvider } from '@mui/material-nextjs/v15-pagesRouter';
+import { ThemeProvider, createTheme } from '@mui/material/styles';
+import { Roboto } from 'next/font/google';
+const roboto = Roboto({
+ weight: ['300', '400', '500', '700'],
+ subsets: ['latin'],
+ display: 'swap',
+ variable: '--font-roboto',
+});
+const theme = createTheme({
+ typography: {
+ fontFamily: 'var(--font-roboto)',
+ },
+});
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
return (
<AppCacheProvider {...props}>
<Head>...</Head>
+ <ThemeProvider theme={theme}>
+ <main className={roboto.variable}>
<Component {...pageProps} />
+ </main>
+ </ThemeProvider>
</AppCacheProvider>
);
}
테마에 대해 더 배우고 싶다면 Theming 가이드를 확인하세요.
CSS 테마 변수 (CSS theme variables)
CSS 테마 변수를 사용하려면 cssVariables 플래그를 활성화하세요:
'use client';
const theme = createTheme({
+ cssVariables: true,
});
CSS 테마 변수의 장점과 SSR 깜빡임 방지 방법에 대해 더 알아보세요.