신비한 개발사전
MUI 컴포넌트의 SSR flickering 이슈 해결법 본문
사용자 기기 설정에 따라 화면을 라이트모드나 다크모드로 보여주는 웹앱을 만드는 도중, 사용자의 기본 테마가 다크모드로 설정되어있으면 페이지를 처음 로딩했을 때 라이트모드의 색상이 먼저 보였다가 다크모드로 바뀌는 flickering 현상을 발견했다.
다행히 이미 잘 알려진 이슈여서 해결방안을 빨리 찾을 수 있었다. 다만 MUI 라이브러리를 사용하기 때문에 해결할 수 있는거라, 근본적인 문제에 대한 솔루션은 나중에 따로 찾아봐야 되겠다.
발생 원인
SSR로 MUI 컴포넌트를 렌더링할 경우 컴포넌트가 사용할 색상이 사전에 결정되는데, 그걸 서버에서 결정하다보니 웹 API를 통해 사용자의 기기 설정을 읽을 수가 없다. 어쩔 수 없이 디폴트 값인 라이트모드의 색상으로 prerendering하게 되고, 클라이언트에 도달한 후 훅이 발동해 prefers-color-scheme 미디어 쿼리를 확인해서 사용자 환경에 일치하는 테마로 리렌더링하게 된다.
솔루션
MUI에서는 이에 대한 솔루션으로 CSS theme variables라는 피쳐를 제공한다. 공식으로 추가된 피쳐는 아니고 아직 experimental 단계에 있지만, SSR을 차용하는 앱에는 필수적으로 지원해야 하는 부분인 만큼 당장 프로젝트에 적용해도 무방해보인다. 실제로 사용해보니 문제되는 부분을 찾지 못했고, 적용 방법도 간단하다.
(1) extendTheme()으로 테마 생성
기존의 createTheme() 함수가 아닌 extendTheme()으로 테마 객체를 생성한다. 본래 함수명이 experimental_extendTheme이기 때문에 가독성을 위해 네이밍을 따로 해주면 좋다.
import { experimental_extendTheme as extendTheme } from "@mui/material/styles";
const theme = extendTheme({
colorSchemes: {
light: {
// ...
},
dark: {
// ...
},
},
});
(2) CssVarsProvider로 컨텍스트 전달
테마를 전달할 때 CssVarsProvider 컴포넌트를 통해 전달한다. 마찬가지로 아직 experimental한 피쳐라서 import할 때 이름을 바꿔줬다.
"use client";
import { Experimental_CssVarsProvider as CssVarsProvider } from "@mui/material/styles";
export default function ThemeContextProvider({ children }: { children: React.ReactNode }) {
return (
<CssVarsProvider defaultMode="system" theme={theme}>
{children}
</CssVarsProvider>
);
}
(3) 부모 컴포넌트에 InitColorSchemeScript 추가
MUI가 사용할 디폴트 테마를 명시한다. <InitColorSchemeScript /> 컴포넌트를 레이아웃에 추가하면 되는데, 사용자 환경에서 테마를 읽으려면 defaultMode 값을 "system"으로 설정하면 된다. 추가로 InitColorSchemeScript 컴포넌트로 인해 개발자툴의 콘솔창에 오류 메세지가 뜰 수 있는데, <html> 태그에 suppressHydrationWarning를 추가해 없앨 수 있다.
이때 주의할 점은 이 defaultMode 값이 localStorage에 저장된다는 부분이다. 이미 페이지에 방문한 적이 있는 사용자라면 나중에 defaultMode 값을 바꿨을 때 다른 테마를 보게될 수도 있다.
1번과 2번 코드를 합치고 Next.js 앱의 layout에 적용한 전체 코드:
// ./src/contexts/ThemeContextProvider.tsx
"use client";
import {
experimental_extendTheme as extendTheme,
Experimental_CssVarsProvider as CssVarsProvider,
} from "@mui/material/styles";
export default function ThemeContextProvider({ children }: { children: React.ReactNode }) {
const theme = extendTheme({
colorSchemes: {
// 생략
},
});
return (
<CssVarsProvider defaultMode="system" theme={theme}>
{children}
</CssVarsProvider>
);
}
// ./src/app/layout.tsx
import ThemeContextProvider from "@/contexts/theme/ThemeContextProvider";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko" suppressHydrationWarning>
<body>
<InitColorSchemeScript defaultMode="system" />
<ThemeContextProvider>
<main>{children}</main>
</ThemeContextProvider>
</body>
</html>
);
}
참고:
'Frontend' 카테고리의 다른 글
UnoCSSㅡNext.js 셋업 (0) | 2024.09.10 |
---|---|
Vuetify 시작하기 (0) | 2024.08.19 |
FSD 아키텍쳐 알아보기 (0) | 2024.08.01 |
ARIA란? (0) | 2024.07.31 |
zod 라이브러리로 프론트에서 데이터 구조 검증하기 (0) | 2024.07.29 |