(Next.js) Next 앱에서 material ui와 styled-component를 적용하기

호두파파·2022년 4월 25일
2

Next.js

목록 보기
2/6


약 2주 전부터 팀에서 내게 할당된 프로젝트로 기존에 리액트 프로젝트로 구성된 웹 서비스를 next.js 기반 서비스로 컨버팅하는 업무를 수행하고 있다.

클라이언트 렌데링으로 구동되는 리액트 앱을 서버 사이드 랜더링으로 구동하는 넥스트 앱으로 컨버팅하는 것은 공식문서가 제안하는 방법처럼 그리 만만하지 않았다.

이전 작성한 글처럼, 서비스가 최초 랜더링될 때 window 객체를 참조할 수 없기에 관련 로직들을 수정해야 했고, CSS 스타일링 또한 리액트 앱에서 당연히 순조롭게 구성되던 방식들에서 에러가 뿜어지고는 했다.

이 글은 Next.js에서 스타일드 컴포넌트와 mui를 동시에 적용하기 위해 자료를 리서치하다 모은 집단지성의 흔적을 담은 글이다.

결론적으로, mui v5와 스타일드 컴포넌트의 조합은 과도기적인 단계에 있고 이 둘을 조합해서 사용하는 것은 그리 좋은 선택이 아닌 것 같다는 생각이 든다.

그럼에도 불구하고, 스타일드 컴포넌트를 사용하고 싶다면 이 포스팅의 결론부분으로 빠르게 스크롤을 내리길 바란다. 그곳에 해답이 있다.

간단한 임무가 될 줄 알았지만, 아주 피똥을 싸야했던 어느 미군 부대의 이야기를 담은 미드.. 이 프로젝트를 하면서 내내 이 드라마가 생각이 났다.


Next.js에서 mui5 적용하기

이 포스팅은 mui v5 버전 기준으로 작성되었습니다.

기존 mui 버전 4에서 버전 5으로 패치업되면서 몇 가지 방식이 변화되었는데 생각보다 중요한 문제였다.

mui5는 emotion 라이브러리를 기반으로 빌드업되었고, CSS 스타일을 생성한다. 따라서 mui5를 next.js 앱에 적용하기 위해서는 emotion의 cache와 server 패키치가 설치되어야 한다.

👹 필수로 설치되야할 패키지들

  • @emotion/cache
  • @emotion/react
  • @emotion/server
  • @emotion/styled

👺 emotion 라이브러리 : 이모션 라이브러리는 css-in-js 형식으로 스타일을 사용할 수 있게 도와준다. className이 자동으로 부여되기 때문에 스타일이 겹칠 염려가 없다. 재사용이 가능하고, prop과 조건에 따른 스타일을 지정가능하다.

만일 프로젝트에서 mui4를 사용하고 있다면,레거시로 정의된 패키지들을 제거해주는 것이 좋다.
yarn remove @material-ui/core @material-ui/icons @material-ui/lab @material-ui/pickers

자 이제, mui를 설치해주자.
npm i @mui/material혹은yarn add @mui/material


Step 1. MUI 테마 만들어주기

styles/theme.js 파일을 생성 후 다음과 같이 입력한다.

import { createTheme, responsiveFontSizes } from "@mui/material/styles";
import { deepPurple, amber } from "@mui/material/colors";

// Create a theme instance.
let theme = createTheme({
  palette: {
    primary: deepPurple,
    secondary: amber,
  },
});

theme = responsiveFontSizes(theme);

export default theme;

Step 2. createEmotionCache

util/createEmotionCache.js src 안에 utl 폴더(경우에 따라서는 styles)를 생성하고, createEmotionCache.js 파일을 생성한다.

이 파일은 앱이 css 스타일을 어떻게 적용해야 할지 빠르게 인식할 수 있도록 도와주는 역할을 한다.

import createCache from '@emotion/cache';

export default function createEmotionCache() {
  return createCache({ key: 'css' });
}

Step 3. _app.js에서 적용하기

앞서 만들어준 테마를 앱에 적용시키기 위해서 캐치를 생성하고, 이를 캐치프로바이더로 일괄적으로 적용시킬 수 있다.

import React from 'react';
import { CacheProvider } from '@emotion/react';
import { ThemeProvider, CssBaseline } from '@mui/material';

import createEmotionCache from '../util/createEmotionCache';
import theme from '../styles/theme';
import '../styles/globals.css';

const clientSideEmotionCache = createEmotionCache();

const MyApp = (props) => {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;

  return (
    <CacheProvider value={emotionCache}>
      <ThemeProvider theme={lightTheme}>
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
};

export default MyApp;
  • 프로젝트를 실행하면 브라우저마다 각기 다른 기본 css가 적용되어 있을 것이다. 서비스를 제공할대 모든 브라우저에서 일관적인 스타일을 보여줘야 하는데, 이때 <CssBaseLine />을 사용한다.

  • 앱에서의 스타일링은 주로 mui 컴포넌트 내부에서 인라인 방식으로 스타일 객체를 작성하는 것이 권장되기 때문에 next.js에서 css를 사용하기 위해 사용하는또 다른 방식인 module.css 파일을 제거할 수 있다.

  • 사용하지 않는 파일들은 제거해주자.
    rm styles/Home.module.css
    rm pages/api/hello.js

👹 react-app에서 글로벌 스타일을 적용하기 위해서는 createGlobalStyle()을 이용했지만, next.js에서는 styles/golobal.css파일에 공통적으로 적용할 스타일을 넣어주면 된다.


Step 4. _document.js 파일 커스텀하기

_document는 서버 사이드 랜더링에 관여하는 로직 혹은 정적인 페이지를 로드하는데 사용되는 로직을 추가하는데 사용한다. 따라서, 서버 사이드 랜더링 지원을 위해 이 작업이 필수적으로 필요하다.

  • 👺 이 작업을 해주지 않는다면, 서버에서 받아온 html, css / 클라이언트에서 렌더링한 html, css가 달라 경고를 띄우게 된다. 따라서, 서버와 클라이언트의 싱크를 맞추기 위해 이 작업이 필요하다.

  • mui는 Roboto 폰트를 디폴트로 사용한다. 따라서, npm i @fontsource/roboto or yarn add @fontsource/roboto를 입력해 디폴트로 사용할 폰트를 설치해주는 것이 좋다.

import * as React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import createEmotionServer from '@emotion/server/create-instance'; // 서버사이드 렌더링 시 캐칭된 css 옵션을 이용할 수 있게 해준다.
import createEmotionCache from '../util/createEmotionCache'; // css 옵션을 파싱해서 하나의 객체로 묶어주는 역할을 한다. 

import theme from '../styles/theme';

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          <link
            rel="stylesheet"
            href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
  const originalRenderPage = ctx.renderPage;
  const cache = createEmotionCache(); // 캐치된 객체를 정의해주고, 
  const { extractCriticalToChunks } = createEmotionServer(cache); // 서버사이드 랜더링 시 할당된 스타일 객체를 스타일 오브젝트 객체에 입혀줄 것이다.

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => 
    	<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 fragment is rendered after the app and page rendering finish.
    styles: [...React.Children.toArray(initialProps.styles), ...emotionStyleTags],
  };
};

여기까지 세팅을 제대로 마쳤다면, 이제 next.js 앱에서 mui5를 적용해서 빠르게 앱을 만들 수 있다.
다만, 내 경우처럼 레거시에서 스타일드 컴포넌트를 사용하고 있다면 추가적으로 작성해줘야 할 것들이 있다.


Next.js에서 styled-components 적용하기

이 문제 때문에 꽤나 골치를 겪었는데,
일부 상황에서는 styled-components가 제대로 적용되지 않는 문제가 발생하고는 했다.
아마, Next.js는 기본적으로 서버 사이드 환경에서 미리 랜더링을 제공하는데, 서버 사이드 랜더링 시에 styled-components가 함께 적용되어야 하는, 그것이 제대로 되지 않는 문제였다.

이 문제에 관해서는 mui github issue에서 최근까지도 논의가 되고 있는데, 뾰족하게 해결이 되지 않은 것 같다. 하여, mui 5에서는 emotion을 이용해서 next.js에 프레임웍을 적용할 것을 권장하고 있다.

관련 이슈 살펴보기 : [styled-engine-sc] MUI + styled-components + Next.js CSS rules ordering issue


때문에 Next.js에서 styled-components를 쓰기 위해서는 bable-plugin 설치가 필요하다.

Step 1) babel-plugin 설치

// with npm 
npm install @babel-plugin-styled-components --save-dev

// width yarn 
yarn add @babel-plugin-styled-components --dev

@babel-plugin-styled-components를 설치 후 루트 디렉토리에 .babelrc파일을 생성해준다.

// .babelrc
{
	"presets": ["next/babel"],
  	"plugins": [
    	["styled-components", { "ssr": true }]
    ]
}

Stpe 2) document.tsx 파일 커스텀하기

// _document.tsx
import Document, { DocumentContext } from 'next/document'
import { ServerStyleSheet } from 'styled-components' // 서버사이드 랜더링이 가능하도록 설정

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet() // 스타일드 컴포넌트 스타일 시트
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles} 
            {sheet.getStyleElement()} // 스타일드 컴포넌트 스타일 시트를 스타일 객체로 등록한다.
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }
}

🧲 결론 : emotion styled를 사용하자

결론을 우선 말하면, 위처럼 styled-components를 사용가능하도록 세팅을 해주더라도,
mui와 styled-components를 아주 깔끔하게 조합하기란 쉽지 않은 일이었다.

next.js를 만들어낸 vercel과 mui 양쪽의 공식 깃헙 예제를 살펴보면 emotion을 사용할 것을 권고 있다. 따라서, next.js에서 스타일드 컴포넌트를 사용하고 싶다면 @emotion/react를 사용하는 것이 정신건강에 좋다.

@emotion/react이 라이브러리는 css prop 지원하고 ssr시 아무런 설정이 필요없다.

앞서 언급하지 못했지만, mui4에서 5로 버전업하면서 생긴 주요한 변화로

mui 컴포넌트를 커스텀하는 방식이었던 makeStyle()이 폐지되었다는 것이다. 따라서 커스텀이 필요하다면 sx 객체 안에 스타일링 객체를 넣던가, emotion의 styled()를 이용해서 스타일드 컴포넌트를 만들어주는 방식으로 진행하게 된다.

관련 링크 살펴보기

import { styled } from '@mui/system';

export const ContentContainer = styled('div')({
	width: '100%',
	backgroundColor: 'white',
	overflowY: 'auto',
	display: 'flex',
	flexDirection: 'column',
	justifyContent: 'space-between',
});

Retrospect

emotion을 이용해서 styled-components를 사용할 수 있게 되었지만, mui를 사용하고 있는 이상, 굳이 이 두 가지를 섞어서 프로젝트를 진행하는 것이 효율적인가라는 점에서는 스스로 의심이 된다.

더불어, emotion은 자바스크립트로 작성되어 있기 때문에 타입스크립트로 프로젝트를 진행해야 한다면, 더더욱이 mui를 next.js 앱에서 사용하는 것이 어려울 수 있다고 생각한다.

디자이너가 팀에 편성되어 있어서 스타일링 프레임워크를 고민하지 않아도 된다면, 스타일드 컴포넌트를 사용할 것 같다. 혹여나 프레임워크를 적용해야 한다면, 아직 사용해보지는 못했지만 테일윈드 CSS를 고려해볼 것 같다.


출처

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

1개의 댓글

comment-user-thumbnail
2023년 2월 14일

글 잘 봤습니다.
저는 styled-component, CSS, SCSS, CSS in JS, CSS Module, LESS 등을 좀 깊게 써본 경험이 있는데요.
CSS 관련 도구들은 제 개인적으로도 취약한 부분이라 생각되서 좀 깊이 다뤄 보려고 하는 편입니다.
그래서 말인데요...
글 말미의 회고글에 관해 질문하고 싶습니다.
저는 tailwind css 같은 경우 사용성 측면에서는 좋다고 생각은 듭니다. 하지만 몇가지 문제점이 있다고 생각하는데요.
1. className의 string이 너무 길어지면서 가독성에 문제가 생길수 있습니다.
2. 1번의 문제로 string을 제가공 처리하는 함수를 만들어도 팀원 모두에게 공유될수 있는 체계를 만들어야 합니다.

  • 또다른 업무가 추가되는 꼴, 어렵고 쉽고를 떠나서 말이죠
  1. tailwind css가 제공해주는 유틸리티성 className들 만으로는 css 최적화가 많이 어렵습니다.
  • 미리 만들어진 유틸리티 className으로 스타일링을 하기때문에 얼마든지 중복되는 CSS가 들어갈 수 있다.

(잡소리)
저는 MUI와 typescript를 병행해 사용하면서 참 좋았다라는 경험이 있는데요.
style의 핸들링을 MUI가 담당해주고 그 MUI는 typescript가 타입체킹으로 안정성을 어느정도 보장하고 code assistant 까지 해주니 확장 가능여부와 잘못 사용될 여지 까지 많이 줄여주는 경험을 했습니다.

답글 달기