다크 모드
다크 모드 (Dark mode)
Material UI는 라이트 테마(기본값)와 다크 테마, 두 가지 팔레트 모드를 제공합니다. 사용자 취향에 따라 다크 모드를 기본값으로 고정하거나, 시스템 설정을 따라 자동으로 전환되도록 구성할 수 있어요. 이 문서에서는 그 방법을 하나씩 살펴볼게요.
출처: 문서
본문
Material UI는 두 가지 팔레트 모드를 제공합니다. 라이트(light, 기본값)와 다크(dark)가 그것이에요.
:::success Material UI theming agent skill을 사용하면 AI 코딩 어시스턴트에게 다크 모드, 색상 스킴, SSR 동작에 관한 전체 맥락을 제공할 수 있어요. :::
다크 모드만 사용하기 (Dark mode only)
사용자의 설정과 상관없이 애플리케이션에서 다크 테마를 기본값으로 쓰게 하려면, createTheme() 헬퍼에 mode: 'dark'를 추가하면 됩니다.
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
});
export default function App() {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<main>This app is using the dark mode</main>
</ThemeProvider>
);
}
createTheme() 헬퍼에 mode: 'dark'를 추가하면 아래 데모처럼 여러 팔레트 값이 바뀝니다.
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import { styled, ThemeProvider, useTheme, createTheme } from '@mui/material/styles';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
padding: theme.spacing(2),
borderRadius: 4,
[theme.breakpoints.up('md')]: {
padding: theme.spacing(3),
},
}));
const Color = styled(Grid)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
'& div:first-of-type': {
width: theme.spacing(5),
height: theme.spacing(5),
flexShrink: 0,
marginRight: theme.spacing(1.5),
borderRadius: theme.shape.borderRadius,
},
}));
function Demo() {
const theme = useTheme();
const item = (color, name, expanded = false, border = false) => (
<Color size={{ xs: 12, sm: 6, md: expanded ? 8 : 4 }}>
<div
style={{
backgroundColor: color,
border: border ? `1px solid ${theme.palette.divider}` : undefined,
}}
/>
<div>
<Typography variant="body2">{name}</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{color}
</Typography>
</div>
</Color>
);
return (
<Root>
<Typography gutterBottom sx={{ mb: 1.5 }}>
Typography
</Typography>
<Grid container spacing={1}>
{item(theme.palette.text.primary, 'palette.text.primary')}
{item(theme.palette.text.secondary, 'palette.text.secondary')}
{item(theme.palette.text.disabled, 'palette.text.disabled')}
</Grid>
<Typography gutterBottom sx={{ mt: 4, mb: 1.5 }}>
Buttons
</Typography>
<Grid container spacing={1}>
{item(theme.palette.action.active, 'palette.action.active')}
{item(theme.palette.action.hover, 'palette.action.hover')}
{item(theme.palette.action.selected, 'palette.action.selected')}
{item(theme.palette.action.disabled, 'palette.action.disabled')}
{item(
theme.palette.action.disabledBackground,
'palette.action.disabledBackground',
true,
)}
</Grid>
<Typography gutterBottom sx={{ mt: 4, mb: 1.5 }}>
Background
</Typography>
<Grid container spacing={1}>
{item(
theme.palette.background.default,
'palette.background.default',
false,
true,
)}
{item(
theme.palette.background.paper,
'palette.background.paper',
false,
true,
)}
</Grid>
<Typography gutterBottom sx={{ mt: 4, mb: 1.5 }}>
Divider
</Typography>
<Grid container spacing={1}>
{item(theme.palette.divider, 'palette.divider')}
</Grid>
</Root>
);
}
const darkTheme = createTheme({
palette: {
// Switching the dark mode on is a single property value change.
mode: 'dark',
},
});
export default function DarkTheme() {
return (
<Box sx={{ width: '100%' }}>
<ThemeProvider theme={darkTheme}>
<Demo />
</ThemeProvider>
</Box>
);
}
<ThemeProvider> 컴포넌트 안에 <CssBaseline />을 추가하면 앱의 배경도 다크 모드로 적용됩니다.
:::warning
이 방식으로 다크 모드를 설정하는 것은 기본 팔레트를 사용할 때만 동작해요. 커스텀 팔레트를 쓰고 있다면 mode에 맞는 올바른 값이 들어있는지 확인해야 합니다. 자세한 방법은 다음 섹션에서 설명할게요.
:::
다크 팔레트 오버라이딩 (Overriding the dark palette)
기본 팔레트를 오버라이드하려면 hex, RGB, HSL 형식의 커스텀 색상을 가진 팔레트 객체를 제공하면 됩니다.
const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#ff5252',
},
},
});
팔레트 구조에 대해 더 알아보려면 Palette 문서를 참고하세요.
시스템 환경설정 (System preference)
일부 사용자는 운영체제에서 시스템 전체 또는 사용자 에이전트별로 라이트/다크 모드 환경설정을 지정해두기도 해요. 아래 섹션에서는 이 환경설정을 앱 테마에 반영하는 방법을 설명할게요.
기본 제공 지원 (Built-in support)
colorSchemes 노드를 사용하면 여러 색상 스킴을 가진 애플리케이션을 만들 수 있어요. 기본 제공 색상 스킴은 light와 dark이고, 값을 true로 설정하면 활성화됩니다.
라이트 색상 스킴은 기본적으로 활성화되어 있으므로, 다크 색상 스킴만 설정해주면 됩니다.
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
function App() {
return <ThemeProvider theme={theme}>...</ThemeProvider>;
}
colorSchemes를 제공하면 다음 기능들이 활성화됩니다.
- 사용자 환경설정에 따라 라이트/다크 색상 스킴이 자동으로 전환됨
- 창(탭) 간 동기화 — 한 탭에서 색상 스킴을 바꾸면 다른 모든 탭에도 적용됨
- 색상 스킴 변경 시 트랜지션 비활성화 옵션
:::info
colorSchemes API는 이전의 제한적인 palette API를 개선한 버전이에요. 위 기능들은 colorSchemes API에서만 사용할 수 있으므로, palette API보다는 이 API 사용을 권장합니다. colorSchemes와 palette가 모두 제공되면 palette가 우선합니다.
:::
:::success
시스템 환경설정 기능을 테스트해보려면 CSS 미디어 기능 prefers-color-scheme 에뮬레이션 가이드를 따라해보세요.
:::
prefers-color-scheme 미디어 접근 (Accessing media prefers-color-scheme)
이 환경설정은 useMediaQuery 훅과 prefers-color-scheme 미디어 쿼리를 이용해 사용할 수 있어요.
아래 데모는 사용자의 OS나 브라우저 설정에서 그 환경설정을 확인하는 방법을 보여줍니다.
import * as React from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
function App() {
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return <div>prefersDarkMode: {prefersDarkMode.toString()}</div>;
}
색상 모드 토글 (Toggling color mode)
기본 제공 지원 환경에서 사용자가 모드를 직접 토글할 수 있게 하려면, useColorScheme 훅을 사용해 모드를 읽고 업데이트하면 됩니다.
:::info
mode는 첫 렌더링에서 항상 undefined예요. 그래서 아래 데모처럼 이 경우를 반드시 처리해줘야 합니다. 그렇지 않으면 hydration mismatch 오류가 발생할 수 있어요.
:::
import Box from '@mui/material/Box';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormLabel from '@mui/material/FormLabel';
import { ThemeProvider, createTheme, useColorScheme } from '@mui/material/styles';
function MyApp() {
const { mode, setMode } = useColorScheme();
if (!mode) {
return null;
}
return (
<Box
sx={{
display: 'flex',
width: '100%',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
color: 'text.primary',
borderRadius: 1,
p: 3,
minHeight: '56px',
}}
>
<FormControl>
<FormLabel id="demo-theme-toggle">Theme</FormLabel>
<RadioGroup
aria-labelledby="demo-theme-toggle"
name="theme-toggle"
row
value={mode}
onChange={(event) =>
setMode(event.target.value as 'system' | 'light' | 'dark')
}
>
<FormControlLabel value="system" control={<Radio />} label="System" />
<FormControlLabel value="light" control={<Radio />} label="Light" />
<FormControlLabel value="dark" control={<Radio />} label="Dark" />
</RadioGroup>
</FormControl>
</Box>
);
}
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
export default function ToggleColorMode() {
return (
<ThemeProvider theme={theme}>
<MyApp />
</ThemeProvider>
);
}
스토리지 매니저 (Storage manager)
기본적으로 색상 스킴의 기본 제공 지원은 브라우저의 localStorage API를 사용해 사용자의 모드와 스킴 환경설정을 저장합니다.
다른 스토리지 매니저를 사용하려면 이 시그니처를 가진 커스텀 함수를 만들면 됩니다.
type Unsubscribe = () => void;
function storageManager(params: { key: string }): {
get: (defaultValue: any) => any;
set: (value: any) => void;
subscribe: (handler: (value: any) => void) => Unsubscribe;
};
그다음 ThemeProvider 컴포넌트의 storageManager prop에 전달하세요.
import { ThemeProvider, createTheme } from '@mui/material/styles';
import type { StorageManager } from '@mui/material/styles';
const theme = createTheme({
colorSchemes: {
dark: true,
},
});
function storageManager(params): StorageManager {
return {
get: (defaultValue) => {
// Your implementation
},
set: (value) => {
// Your implementation
},
subscribe: (handler) => {
// Your implementation
return () => {
// cleanup
};
},
};
}
function App() {
return (
<ThemeProvider theme={theme} storageManager={storageManager}>
...
</ThemeProvider>
);
}
:::warning
InitColorSchemeScript 컴포넌트를 사용해 SSR 깜빡임(플리커)을 방지하려면, 커스텀 스토리지 매니저에 localStorage 구현을 반드시 포함해야 합니다.
:::
스토리지 비활성화 (Disable storage)
스토리지 매니저를 비활성화하려면 storageManager prop에 null을 전달하면 됩니다.
<ThemeProvider theme={theme} storageManager={null}>
...
</ThemeProvider>
:::warning 스토리지 매니저를 비활성화하면 사용자가 페이지를 새로고침할 때마다 앱이 기본 모드로 리셋됩니다. :::
트랜지션 비활성화 (Disable transitions)
트랜지션 없이 색상 스킴을 즉시 전환하려면 ThemeProvider 컴포넌트에 disableTransitionOnChange prop을 적용하세요.
<ThemeProvider theme={theme} disableTransitionOnChange>
...
</ThemeProvider>
이중 렌더링 비활성화 (Disable double rendering)
기본적으로 ThemeProvider는 테마에 라이트 그리고 다크 색상 스킴이 포함되어 있으면 SSR hydration 불일치를 방지하기 위해 다시 렌더링합니다.
이 동작을 비활성화하려면 noSsr prop을 사용하세요.
<ThemeProvider theme={theme} noSsr>
noSsr은 다음과 같은 경우에 유용합니다.
- 클라이언트 전용 애플리케이션, 예를 들어 단일 페이지 애플리케이션(SPA). 이 prop은 성능을 최적화하고 사용자가 페이지를 새로고침할 때 다크 모드 깜빡임을 방지해줍니다.
- Suspense를 사용하는 서버 렌더링 애플리케이션. 다만 서버 렌더 출력이 클라이언트의 초기 렌더 출력과 일치하는지 확인해야 합니다.
기본 모드 설정 (Setting the default mode)
colorSchemes를 제공하면 기본 모드는 system이 됩니다. 즉 사용자가 사이트를 처음 방문할 때 시스템 환경설정을 따른다는 뜻이에요.
다른 기본 모드를 설정하려면 ThemeProvider 컴포넌트에 defaultMode prop을 전달하세요.
<ThemeProvider theme={theme} defaultMode="dark">
:::info
defaultMode 값은 'light', 'dark', 'system'이 될 수 있어요.
:::
InitColorSchemeScript 컴포넌트
InitColorSchemeScript 컴포넌트를 사용해 SSR 플리커를 방지한다면, ThemeProvider 컴포넌트에 전달한 것과 같은 값으로 defaultMode를 설정해야 합니다.
<InitColorSchemeScript defaultMode="dark">
다크 모드에서 스타일링 (Styling in dark mode)
특정 모드에 대한 스타일을 적용하려면 theme.applyStyles() 유틸리티를 사용하세요.
스타일을 전환할 때 theme.palette.mode를 확인하는 것보다 이 함수를 사용하는 것을 권장합니다. 이점이 더 많기 때문이에요.
- 자체 제작 zero-runtime CSS-in-JS 솔루션인 Pigment CSS와 함께 사용할 수 있어요.
- 일반적으로 가독성과 유지보수성이 더 좋습니다.
- 스타일 재계산이 필요 없어 약간 더 성능이 좋지만, SSR 생성 스타일의 번들 크기는 더 큽니다.
사용법 (Usage)
styled 함수와 함께:
import { styled } from '@mui/material/styles';
const MyComponent = styled('div')(({ theme }) => [
{
color: '#fff',
backgroundColor: theme.palette.primary.main,
'&:hover': {
boxShadow: theme.shadows[3],
backgroundColor: theme.palette.primary.dark,
},
},
theme.applyStyles('dark', {
backgroundColor: theme.palette.secondary.main,
'&:hover': {
backgroundColor: theme.palette.secondary.dark,
},
}),
]);
sx prop과 함께:
import Button from '@mui/material/Button';
<Button
sx={[
(theme) => ({
color: '#fff',
backgroundColor: theme.palette.primary.main,
'&:hover': {
boxShadow: theme.shadows[3],
backgroundColor: theme.palette.primary.dark,
},
}),
(theme) =>
theme.applyStyles('dark', {
backgroundColor: theme.palette.secondary.main,
'&:hover': {
backgroundColor: theme.palette.secondary.dark,
},
}),
]}
>
Submit
</Button>;
:::warning
cssVariables: true일 때 theme.applyStyles()로 적용한 스타일은 그 밖에 정의된 스타일보다 특이도(specificity)가 더 높아요. 그래서 스타일을 오버라이드해야 한다면 아래처럼 반드시 theme.applyStyles()도 함께 사용해야 합니다.
const BaseButton = styled('button')(({ theme }) =>
theme.applyStyles('dark', {
backgroundColor: 'white',
}),
);
const AliceblueButton = styled(BaseButton)({
backgroundColor: 'aliceblue', // In dark mode, backgroundColor will be white as theme.applyStyles() has higher specificity
});
const PinkButton = styled(BaseButton)(({ theme }) =>
theme.applyStyles('dark', {
backgroundColor: 'pink', // In dark mode, backgroundColor will be pink
}),
);
:::
API
theme.applyStyles(mode, styles) => CSSObject
특정 모드에 대한 스타일을 적용합니다.
인자 (Arguments)
mode('light' | 'dark') - 스타일을 적용할 모드입니다.styles(CSSObject) - 지정한 모드에 적용할 스타일을 담은 객체입니다.
applyStyles 오버라이딩 (Overriding applyStyles)
theme.applyStyles()를 커스텀 함수로 오버라이드하면 반환되는 값을 완전히 제어할 수 있어요. 오버라이드하기 전에 소스 코드를 살펴 기본 구현이 어떻게 동작하는지 이해하는 것을 권장합니다. 예를 들어 템플릿 리터럴 안에서 사용할 수 있도록 함수가 객체 대신 문자열을 반환하게 하고 싶다면:
const theme = createTheme({
cssVariables: {
colorSchemeSelector: '.mode-%s',
},
colorSchemes: {
dark: {},
light: {},
},
applyStyles: function (key: string, styles: any) {
// return a string instead of an object
return `*:where(.mode-${key}) & {${styles}}`;
},
});
const StyledButton = styled('button')`
${theme.applyStyles(
'dark', `
background: white;
`
)}
`;
코더모드 (Codemod)
theme.palette.mode를 사용하던 코드베이스를 theme.applyStyles()를 사용하도록 마이그레이션하는 codemod를 제공합니다. 아래 codemod를 각각 실행하거나 한 번에 모두 실행할 수 있어요.
npx @mui/codemod@latest v6.0.0/styled <path/to/folder-or-file>
npx @mui/codemod@latest v6.0.0/sx-prop <path/to/folder-or-file>
npx @mui/codemod@latest v6.0.0/theme-v6 <path/to/theme-file>
커스텀
styleOverrides가 들어있는 파일에 대해서는v6.0.0/theme-v6를 실행하세요. 커스텀 테마가 없으면 이 codemod는 무시해도 됩니다.
다크 모드 깜빡임 (Dark mode flicker)
문제 (The problem)
서버 렌더링 애플리케이션은 사용자의 기기에 도달하기 전에 빌드됩니다. 그래서 처음 로드될 때 사용자가 선호하는 색상 스킴에 자동으로 맞출 수 없어요.
보통 이런 일이 발생합니다.
- 앱을 로드하고 다크 모드로 설정합니다.
- 페이지를 새로고침합니다.
- 앱이 잠시 라이트 모드(기본값)로 나타납니다.
- 앱이 완전히 로드되면 다시 다크 모드로 전환됩니다.
브라우저가 다크 모드 환경설정을 기억하고 있는 한, 이 라이트 모드의 "섬광"이 앱을 열 때마다 발생합니다.
이 갑작스러운 변화는 특히 어두운 환경에서 눈에 거슬릴 수 있어요. 눈의 피로를 유발하고, 특히 이 전환 중에 앱과 상호작용한다면 경험을 방해할 수 있습니다.
이 문제를 더 잘 이해하려면 아래 애니메이션 이미지를 확인해보세요.
해결책: CSS 변수 (The solution: CSS variables)
이 문제를 해결하려면 스타일링과 테마에 대한 새로운 접근 방식이 필요합니다. (이 기능의 구현에 대해 더 알고 싶다면 이 CSS 변수 지원 RFC를 참고하세요.)
CSS 미디어 prefers-color-scheme를 이용해 라이트/다크 모드를 지원해야 하는 애플리케이션의 경우, CSS 변수 기능을 활성화하면 이 문제가 해결됩니다.
하지만 모드를 수동으로 토글할 수 있어야 한다면, 플리커를 피하려면 CSS 변수와 InitColorSchemeScript 컴포넌트를 함께 사용해야 합니다.
자세한 내용은 SSR 플리커 방지 섹션을 확인하세요.
더 알아보기 (Learn more)
- Material UI 테마 커스터마이징