프론트엔드/Next.js

[Next.js] Next.js + styled-components + MUI 프로젝트 세팅(그리고 styled-components 글로벌 스타일링과 MUI 커스텀 테마 적용하기)

순코딩 2024. 12. 19. 16:21
이 글은 Next.js 15버전을 기준으로 작성되었습니다.

 

이 글에서는 아래 내용을 스텝 바이 스텝 방식으로 설명합니다.

1. Next.js + styled-components 세팅
2. Next.js + styled-components 글로벌 스타일 적용
3. Next.js + MUI 글로벌 테마 적용

 

 

0. 설치
// Next.js 프로젝트 세팅
$ npx create-next-app@latest {프로젝트 이름}

// 예시
$ npx create-next-app@latest nextJsProject
// styled-components 설치
$ npm install styled-components
// MUI 관련 라이브러리 설치
$ npm install @mui/material @mui/styled-engine-sc styled-components @emotion/styled @mui/icons-material
// MUI Icon 설치
npm install
// Next.js 최적화 MUI + 스타일 캐싱 라이브러리 설치 
$ npm install @mui/material-nextjs @emotion/cache

 

1. Next.js + styled-components 세팅
1) next.config.ts 파일 수정
{projectName} / next.config.ts

이 작업을 통해 SSR내에서의 styled-componet 사용을 지원합니다.

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  compiler: {
    styledComponents: true,
  },
};

export default nextConfig;

 

2) StyledComponentsRegistry.tsx 파일 생성
src / styles / StyledComponentsRegistry.tsx

이 작업을 통해 styled-components의 스타일이 SSR 시 서버에서 HTML로 렌더링될 때 <head> 태그에 포함되어 SSR 스타일 불일치 문제(FOUC)를 방지합니다.

'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
  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>
  )
}

 

3) 루트 레이아웃에서 children을 registry.tsx로 랩핑
src / app / layout.tsx
import StyledComponentsRegistry from "@/styles/StyledComponentsRegistry"; // ✨
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        {/* ✨ */}
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}

 

4) 테스트
src / app / page.tsx
"use client"

import styled from "styled-components";

export default function Home() {
  return <Container>Hello World</Container>;
}

const Container = styled.div`
  background-color:blue;
  color:white;
  font-size:100px;
`

1. Next.js + styled-components 세팅 끝

 

2. Next.js + styled-components 글로벌 스타일 적용
1) 글로벌 스타일 생성
src / styles / GlobalStyles.tsx

이 작업을 통해 styled-components의 글로벌 스타일을 정의합니다.

"use client";

import { createGlobalStyle } from "styled-components";

const GlobalStyles = createGlobalStyle`
  :root {
  /* 기본 색상 */
  --main-color: #2ECC71;
  
  /* 서브 색상 (보조 색상) */
  --secondary-color: #FFFFFF;
  --third-color: #FFA726; 
  }
  
  /* Anchor 태그 기본 스타일 */
  a {
    text-decoration: none;
    color: inherit;
  }

  /* 버튼 스타일 */
  button {
    cursor: pointer;
    border: none;
    background: none;
  }
`;

export default GlobalStyles;

 

2) 루트 레이아웃에서 children 상단에 GlobalStyles.tsx 컴포넌트 호출(랩핑 X, 닫힌 태그(<GlobalStyles/>)로 호출 O)
src / app / layout.tsx

이 작업을 통해 styled-components의 글로벌 스타일을 적용합니다.

import GlobalStyles from "@/styles/GlobalStyles"; // ✨
import StyledComponentsRegistry from "@/utils/registry";
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>
          {/* ✨ */}
          <GlobalStyles />
          {children}
        </StyledComponentsRegistry>
      </body>
    </html>
  );
}

 

3) 테스트 
src / app / page.tsx
"use client"

import styled from "styled-components";

export default function Home() {
  return <Container>Hello World</Container>;
}

const Container = styled.div`
  background-color:var(--main-color);
  color:white;
  font-size:100px;
`

2. Next.js + styled-components 글로벌 스타일 적용

 

3. Next.js + MUI 글로벌 테마 적용
1)  루트 레이아웃에서 children을 <AppRouterCacheProvider>로 랩핑
src / app / layout.tsx

이 작업을 통해 SSR과 CSR 간의 캐싱 통합과 스타일 삽입 최적화를 합니다.

import GlobalStyles from "@/styles/GlobalStyles";
import StyledComponentsRegistry from "@/utils/registry";
import type { Metadata } from "next";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter"; // ✨

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>
          <GlobalStyles />
          {/* ✨ */}
          <AppRouterCacheProvider>{children}</AppRouterCacheProvider> 
        </StyledComponentsRegistry>
      </body>
    </html>
  );
}

 

2) 커스텀 테마 파일 MuiTheme.ts 생성
app / styles / MuiTheme.ts

이 작업을 통해 Mui 커스텀 테마를 생성합니다.

import { createTheme } from '@mui/material/styles';

// MUI 테마 생성
const MuiTheme = createTheme({
    components: {
      // MUI의 TextField 컴포넌트에 대한 스타일 오버라이드
      MuiTextField: {
        styleOverrides: {
          root: { // MuiTextField의 최상위 DOM 요소를 대상으로 스타일을 적용
            // 포커스 상태에서 입력 필드의 보더(border) 색상 변경
            "& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": {
              borderColor: "var(--main-color)",
            },
          },
        },
      },
    },
});

export default MuiTheme;

 

3) 테마 관리 컴포넌트 MuiThemeRegistry.tsx 생성
app / styles / MuiThemeRegistry.tsx

이 작업을 통해 서버와 클라이언트 간 MUI의 Emotion 스타일을 동기화합니다.

"use client";

import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import MuiTheme from "@/styles/MuiTheme";

export default function MuiThemeRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ThemeProvider theme={MuiTheme}>
      <CssBaseline />
      {children}
    </ThemeProvider>
  );
}

 

4) 루트 레이아웃에서 children을 MuiThemeRegistry.tsx로 랩핑
import GlobalStyles from "@/styles/GlobalStyles";
import StyledComponentsRegistry from "@/utils/registry";
import type { Metadata } from "next";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
import MuiThemeRegistry from "@/styles/MuiThemeRegistry"; // ✨

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>
          <GlobalStyles />
          <AppRouterCacheProvider>
            {/* ✨ */}
            <MuiThemeRegistry>{children}</MuiThemeRegistry>
          </AppRouterCacheProvider>
        </StyledComponentsRegistry>
      </body>
    </html>
  );
}

 

5) 테스트
src / app / page.tsx
"use client";

import { TextField } from "@mui/material";
import styled from "styled-components";

export default function Home() {
  return (
    <div>
      <Container>Hello World</Container>
      <TextField />
    </div>
  );
}

const Container = styled.div`
  background-color: var(--main-color);
  color: white;
  font-size: 100px;
`;

 

최종 코드

{projectName} / package.json

{
  "name": "project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@emotion/cache": "^11.14.0",
    "@emotion/styled": "^11.14.0",
    "@mui/icons-material": "^6.2.1",
    "@mui/material": "^6.2.1",
    "@mui/material-nextjs": "^6.2.1",
    "@mui/styled-engine-sc": "^6.2.1",
    "next": "15.1.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "styled-components": "^6.1.13"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.1.1",
    "typescript": "^5"
  }
}

 

src / app / layout.tsx

import GlobalStyles from "@/styles/GlobalStyles";
import StyledComponentsRegistry from "@/utils/registry";
import type { Metadata } from "next";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
import MuiThemeRegistry from "@/styles/MuiThemeRegistry"; // ✨

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>
          <GlobalStyles />
          <AppRouterCacheProvider>
            {/* ✨ */}
            <MuiThemeRegistry>{children}</MuiThemeRegistry>
          </AppRouterCacheProvider>
        </StyledComponentsRegistry>
      </body>
    </html>
  );
}

 

src / app / page.tsx

"use client";

import { TextField } from "@mui/material";
import styled from "styled-components";

export default function Home() {
  return (
    <div>
      <Container>Hello World</Container>
      <TextField />
    </div>
  );
}

const Container = styled.div`
  background-color: var(--main-color);
  color: white;
  font-size: 100px;
`;

 

src / styles / GlobalStyles.tsx

"use client";

import { createGlobalStyle } from "styled-components";

const GlobalStyles = createGlobalStyle`
  :root {
  /* 기본 색상 */
  --main-color: #2ECC71;
  
  /* 서브 색상 (보조 색상) */
  --secondary-color: #FFFFFF;
  --third-color: #FFA726; 
  }
  
  /* Anchor 태그 기본 스타일 */
  a {
    text-decoration: none;
    color: inherit;
  }

  /* 버튼 스타일 */
  button {
    cursor: pointer;
    border: none;
    background: none;
  }
`;

export default GlobalStyles;

 

src / styles / MuiTheme.ts

import { createTheme } from '@mui/material/styles';

// MUI 테마 생성
const MuiTheme = createTheme({
    components: {
      // MUI의 TextField 컴포넌트에 대한 스타일 오버라이드
      MuiTextField: {
        styleOverrides: {
          root: { // MuiTextField의 최상위 DOM 요소를 대상으로 스타일을 적용
            // 포커스 상태에서 입력 필드의 보더(border) 색상 변경
            "& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": {
              borderColor: "var(--main-color)",
            },
          },
        },
      },
    },
});

export default MuiTheme;

 

src / styles / MuiThemeRegistry.tsx

"use client";

import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import MuiTheme from "@/styles/MuiTheme";

export default function MuiThemeRegistry({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider theme={MuiTheme}>
      <CssBaseline />
      {children}
    </ThemeProvider>
  );
}

 

src / styles / StyledComponentsRegistry.tsx

'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
  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>
  )
}
참고자료

https://sooncoding.tistory.com/191

 

[Next.js] Next.js + Styled-components 적용하기

https://nextjs.org/docs/app/building-your-application/styling/css-in-js#styled-components Styling: CSS-in-JS | Next.jsUse CSS-in-JS libraries with Next.jsnextjs.org 글로벌 스타일 적용하려면 GlobalStyles.ts을 생성해야하니 챗지피티에

sooncoding.tistory.com

https://sooncoding.tistory.com/196

 

[Next.js] Next.js에서 styled-components 글로벌 스타일 적용 방법

Next.js + styled-components 글로벌 스타일 적용1) 글로벌 스타일 생성src / styles / GlobalStyles.tsx"use client";import { createGlobalStyle } from "styled-components";const GlobalStyles = createGlobalStyle` :root { /* 기본 색상 */ --main-

sooncoding.tistory.com

https://sooncoding.tistory.com/195

 

[Next.js] Next.js 환경에서 MUI integration 방법 + MUI 전역 스타일링 방법

이 글에서는 Next.js 15버전 환경에서 MUI 테마를 적용하는 방법에 대해 스텝 바이 스텝 형식으로 설명한다. 문제 상황Next.js 15 버전에서 MUI를 활용한 UI 개발 중 반복되는 UI 스타일링과 상속 중첩에

sooncoding.tistory.com