[Next.js] Next.js + styled-components + MUI 프로젝트 세팅(그리고 styled-components 글로벌 스타일링과 MUI 커스텀 테마 적용하기)
이 글은 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
https://sooncoding.tistory.com/196
https://sooncoding.tistory.com/195