DEV Community

Cover image for Next.Js 12 + MUI 5 (Material UI) New Tutorial
soom
soom

Posted on • Updated on

Next.Js 12 + MUI 5 (Material UI) New Tutorial

Abstract

Introducing how to Material UI 5 apply to Next.js 12 framework.

SSR Framework 에는 Next.js, Nuxt, Sveltekit 등 여러가지가 있지만 가장 대표적인 framework는 react기반의 Next.js일 것이다.

본 포스팅에서는 Next.js에 best react component library 중 하나인 MUI를 적용하는 가장 최신 방법을 소개하고자 한다.

기존의 @mui/styles 패키지를 통해 적용하는 방법은 react 18의 호환성 문제로 deprecated 되었으며 관련 내용은 하기 링크를 참조

The legacy styling solution of MUI: https://mui.com/system/styles/basics/


Getting Started

원하는 프로젝트 폴더에 Next.js TypeScript 프로젝트를 생성

Terminal
pnpm create next-app . --typescript
Enter fullscreen mode Exit fullscreen mode

MUI 관련 패키지 설치

Terminal
pnpm add -S @emotion/cache @emotion/react @emotion/server @emotion/styled @mui/icons-material @mui/material
Enter fullscreen mode Exit fullscreen mode

createEmotionCache 함수 모듈인 emotionCache.tslib 폴더에 생성

Note

  • 클라이언트에서 <head/> 의 가장 최상단에 <meta> 태그를 생성하고 이를 insertionPoint로 지정한다.

  • 이는 페이지 로딩시 MUI style 을 가장 먼저 로딩하는것을 보장

  • 이렇게 먼저 로딩될 경우 다른 Style solution들 보다 높은 우선순위를 가지게 되어 MUI로 개발하는데 이점을 가지게 된다.

lib/emotionCache.ts
import createCache from '@emotion/cache';

const isBrowser = typeof document !== 'undefined';

const createEmotionCache = () => {
    let insertionPoint;

    if (isBrowser) {
        const emotionInsertionPoint = document.querySelector(
            'meta[name="emotion-insertion-point"]'
        ) as HTMLElement;
        insertionPoint = emotionInsertionPoint ?? undefined;
    }

    return createCache({ key: 'mui-style', insertionPoint });
};

export default createEmotionCache;
Enter fullscreen mode Exit fullscreen mode

lib 폴더에 theme.ts 도 생성

Note

  • 나중에 global style을 적용하기 위해 theme.ts 생성
lib/theme.ts
import { createTheme } from '@mui/material/styles';
import { indigo } from '@mui/material/colors';

const theme = createTheme({
    palette: {
        primary: {
            main: indigo.A400,
        },
    },
});

export default theme;
Enter fullscreen mode Exit fullscreen mode

_app.tsx 파일을 다음과 같이 수정

pages/_app.tsx
import * as React from 'react';
import Head from 'next/head';
import { ThemeProvider } from '@mui/material/styles';
import { CacheProvider } from '@emotion/react';
import CssBaseline from '@mui/material/CssBaseline';

import theme from '../lib/theme';
import createEmotionCache from '../lib/emotionCache';

import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import type { EmotionCache } from '@emotion/react';

type AppPropsWithCache = AppProps & {
    Component: NextPage;
    emotionCache?: EmotionCache;
};

const clientSideEmotionCache = createEmotionCache();

const MyApp = ({
    Component,
    emotionCache = clientSideEmotionCache,
    pageProps,
}: AppPropsWithCache) => {
    return (
        <CacheProvider value={emotionCache}>
            <Head>
                <meta name='viewport' content='initial-scale=1, width=device-width' />
            </Head>
            <ThemeProvider theme={theme}>
                <CssBaseline />
                <Component {...pageProps} />
            </ThemeProvider>
        </CacheProvider>
    );
};

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

_document.tsx 파일을 생성한 뒤 다음과 같이 작성

Note

  • _Document.getInitialProps_app이 아닌 _document에 상속되며 static 으로 생성됨
_Document.getInitialProps = async (ctx: DocumentContext): Promise<DocumentInitialProps> => {...}
Enter fullscreen mode Exit fullscreen mode
  • cache를 새로 생성하기보단 동일한 Emotion cache를 SSR Request에 사용한다면 퍼포먼스를 개선하는 효과를 얻을 수 있다. (하지만 global side effect가 발생할 수 있는 단점이 있다는 것도 인지해야한다.)

  • 여기서는 SSR Request 마다 cache를 생성하기로 한다.

const cache = createEmotionCache();
Enter fullscreen mode Exit fullscreen mode
  • styles fragmentapppage의 렌더링이 끝난뒤에 렌더링된다.
styles: [...React.Children.toArray(initialProps.styles), ...emotionStyleTags],
Enter fullscreen mode Exit fullscreen mode
  • 잘못된 HTML 생성 방지를 위해 다음 구문은 중요.

참고링크: https://github.com/mui/material-ui/issues/26561#issuecomment-855286153

const emotionStyles = extractCriticalToChunks(initialProps.html);
Enter fullscreen mode Exit fullscreen mode

Generating Order

서버

  1. app.getInitialProps

  2. page.getInitialProps

  3. document.getInitialProps

  4. app.render

  5. page.render

  6. document.render

서버에러

  1. document.getInitialProps

  2. app.render

  3. page.render

  4. document.render

클라이언트

  1. app.getInitialProps

  2. page.getInitialProps

  3. app.render

  4. page.render

pages/_document.tsx
import * as React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import theme from '../lib/theme';
import createEmotionCache from '../lib/emotionCache';

import type { DocumentContext, DocumentInitialProps } from 'next/document';

export default class _Document extends Document {
    render() {
        return (
            <Html lang='ko'>
                <Head>
                    <meta name='theme-color' content={theme.palette.primary.main} />
                    <link rel='shortcut icon' href='/static/favicon.ico' />
                    <link
                        rel='stylesheet'
                        href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'
                    />
                    <meta name='emotion-insertion-point' content='' />
                    {this.props.styles}
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

_Document.getInitialProps = async (ctx: DocumentContext): Promise<DocumentInitialProps> => {
    const originalRenderPage = ctx.renderPage;

    const cache = createEmotionCache();
    const { extractCriticalToChunks } = createEmotionServer(cache);

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) =>
                function EnhanceApp(props) {
                    return <App emotionCache={cache} {...props} />;
                },
        });

    const initialProps = await Document.getInitialProps(ctx);

    const emotionStyles = extractCriticalToChunks(initialProps.html);
    const emotionStyleTags = emotionStyles.styles.map((style) => (
        <style
            data-emotion={`${style.key} ${style.ids.join(' ')}`}
            key={style.key}
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: style.css }}
        />
    ));

    return {
        ...initialProps,
        styles: [...React.Children.toArray(initialProps.styles), ...emotionStyleTags],
    };
};
Enter fullscreen mode Exit fullscreen mode

index.tsx 를 다음과 같이 작성

pages/index.tsx
import { Button } from '@mui/material';

import type { NextPage } from 'next';

const Home: NextPage = () => {
    return (
        <main
            style={{
                width: '100vw',
                height: '100vh',
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center',
            }}
        >
            <div>
                <header>
                    <h1>MUI5 + Next.js 12</h1>
                </header>
                <section>
                    <Button variant={'contained'}>Hello MUI</Button>
                </section>
            </div>
        </main>
    );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Result

👉 CodeSandBox Sample Link

Result


Conclusion

본 포스팅에서는 MUI 5Next.Js 12에 적용하는 최신 방법을 소개하였다. (2022년 8월 기준)

react 18 이 등장하면서 기존 방법보다 고려해야할 부분이 늘어난건 사실이나 여전히 reference가 많은 조합을 선호하는 트렌트 안에서는 Best 라고 할수있는 SSR Framework + UI Library 조합인 Next.Js+ MUI는 꾸준히 이용될 것이다.

이외에도 추천하는 React UI Framework 중 하나인 Mantine을 소개하면서 글을 마무리 짓고자 한다.
Mantine: https://mantine.dev/

Top comments (0)