CSS Layers
CSS Layers
CSS Laye(캐스케이드 레이어)를 활용해서 Material UI 스타일을 생성하는 방법을 알아볼게요. 캐스케이드 레이어는 고급 CSS 기능이라 처음 보는 분은 조금 낯설 수 있는데, 하나씩 차근차근 살펴보면서 어떤 문제를 해결해 주고 어떻게 적용하는지 함께 확인해 볼게요.
출처: 문서
본문
캐스케이드 레이어란? (What are cascade layers?)
캐스케이드 레이어(cascade layers)는 요소에 스타일이 적용되는 순서를 제어할 수 있게 해주는 고급 CSS 기능이에요. 캐스케이드 레이어가 익숙하지 않다면, 자세한 개요는 MDN 문서를 방문해서 확인해 보세요.
캐스케이드 레이어를 사용할 때 얻을 수 있는 이점은 다음과 같아요:
- 향상된 특이성(specificity): 캐스케이드 레이어는 스타일의 순서를 제어할 수 있게 해줘서, 특이성 충돌을 피하는 데 도움이 돼요. 예를 들어, 스타일의 기본 특이성을 건드리지 않고도 컴포넌트를 테마로 꾸밀 수 있어요.
- CSS 프레임워크와의 더 나은 통합: 캐스케이드 레이어를 사용하면 Tailwind CSS v4 유틸리티 클래스로
!important지시어 없이도 Material UI 스타일을 덮어쓸 수 있어요. - 더 나은 디버깅: 캐스케이드 레이어는 브라우저의 개발자 도구에 나타나서, 어떤 스타일이 어떤 순서로 적용되는지 더 쉽게 볼 수 있어요.
단일 캐스케이드 레이어 구현하기 (Implementing a single cascade layer)
이 방법은 모든 Material UI 컴포넌트와 전역 스타일을 위해 @layer mui이라는 단일 레이어를 만들어요. 이는 @layer 지시어를 사용하는 Tailwind CSS v4 같은 다른 스타일링 솔루션과 통합하기에 적합해요.
Next.js App Router
먼저 App Router 통합 가이드에 따라 Next.js로 Material UI를 설정해요. 그런 다음 이 단계를 따릅니다:
- 루트 레이아웃에서 CSS 레이어 기능을 활성화해요:
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
export default function RootLayout() {
return (
<html lang="en" suppressHydrationWarning>
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
{/* Your app */}
</AppRouterCacheProvider>
</body>
</html>
);
}
- Tailwind CSS v4와 함께 동작하도록 CSS 파일 맨 위에서 레이어 순서를 설정해요:
@layer theme, base, mui, components, utilities;
Next.js Pages Router
먼저 Pages Router 통합 가이드에 따라 Next.js로 Material UI를 설정해요. 그런 다음 이 단계를 따릅니다:
- 커스텀
_document에서 CSS 레이어 기능을 활성화해요:
import {
createCache,
documentGetInitialProps,
} from '@mui/material-nextjs/v15-pagesRouter';
// ...
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const finalProps = await documentGetInitialProps(ctx, {
emotionCache: createCache({ enableCssLayer: true }),
});
return finalProps;
};
- Tailwind CSS v4와 함께 동작하도록
GlobalStyles컴포넌트로 레이어 순서를 설정해요 — 이 컴포넌트는AppCacheProvider의 첫 번째 자식이 되어야 해요:
import { AppCacheProvider } from '@mui/material-nextjs/v15-pagesRouter';
import GlobalStyles from '@mui/material/GlobalStyles';
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
return (
<AppCacheProvider {...props}>
<GlobalStyles styles="@layer theme, base, mui, components, utilities;" />
<Component {...pageProps} />
</AppCacheProvider>
);
}
Vite 또는 기타 SPA
src/main.tsx에서 다음 변경 사항을 적용해요:
StyledEngineProvider컴포넌트에enableCssLayerprop을 전달해요.- Tailwind CSS v4와 함께 동작하도록
GlobalStyles컴포넌트로 레이어 순서를 설정해요.
import { StyledEngineProvider } from '@mui/material/styles';
import GlobalStyles from '@mui/material/GlobalStyles';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<StyledEngineProvider enableCssLayer>
<GlobalStyles styles="@layer theme, base, mui, components, utilities;" />
{/* Your app */}
</StyledEngineProvider>
</React.StrictMode>,
);
여러 캐스케이드 레이어 구현하기 (Implementing multiple cascade layers)
단일 캐스케이드 레이어를 설정한 후에는, 스타일을 여러 레이어로 나눠서 Material UI 내부에서 더 잘 정리할 수 있어요. 이렇게 하면 sx prop으로 테마를 적용하고 스타일을 덮어쓰는 것이 더 단순해져요.
먼저 이전 섹션의 단계를 따라 CSS 레이어 기능을 활성화해요. 그런 다음 새 파일을 만들고 Material UI의 ThemeProvider를 감싸는 컴포넌트를 내보내요. 마지막으로 createTheme 함수에 modularCssLayers: true 옵션을 전달해요:
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
import { createTheme, ThemeProvider } from '@mui/material/styles';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import FormHelperText from '@mui/material/FormHelperText';
const theme = createTheme({
modularCssLayers: true,
cssVariables: true,
});
export default function CssLayersInput() {
return (
<ThemeProvider theme={theme}>
<FormControl variant="outlined">
<InputLabel
shrink
htmlFor="css-layers-input"
sx={{
width: 'fit-content',
transform: 'none',
position: 'relative',
mb: 0.25,
fontWeight: 'medium',
pointerEvents: 'auto',
}}
>
Label
</InputLabel>
<OutlinedInput
id="css-layers-input"
placeholder="Type something"
slotProps={{
input: {
sx: { py: 1.5, height: '2.5rem', boxSizing: 'border-box' },
},
}}
/>
<FormHelperText sx={{ marginLeft: 0 }}>Helper text goes here</FormHelperText>
</FormControl>
</ThemeProvider>
);
}
이 기능이 활성화되면 Material UI는 다음 레이어들을 생성해요:
@layer mui.global:GlobalStyles및CssBaseline컴포넌트의 전역 스타일.@layer mui.components: 모든 Material UI 컴포넌트의 기본 스타일.@layer mui.theme: 모든 Material UI 컴포넌트의 테마 스타일.@layer mui.custom: Material UI가 아닌 styled 컴포넌트의 커스텀 스타일.@layer mui.sx:sxprop에서 나온 스타일.
아래 섹션에서는 일반적인 React 프레임워크에서 Material UI용으로 여러 캐스케이드 레이어를 설정하는 방법을 보여줄게요.
Next.js App Router
'use client';
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: React.ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
import AppTheme from '../theme';
export default function RootLayout() {
return (
<html lang="en" suppressHydrationWarning>
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<AppTheme>{/* Your app */}</AppTheme>
</AppRouterCacheProvider>
</body>
</html>
);
}
Next.js Pages Router
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
import AppTheme from '../src/theme';
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
return (
<AppCacheProvider {...props}>
<AppTheme>
<Component {...pageProps} />
</AppTheme>
</AppCacheProvider>
);
}
import {
createCache,
documentGetInitialProps,
} from '@mui/material-nextjs/v15-pagesRouter';
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const finalProps = await documentGetInitialProps(ctx, {
emotionCache: createCache({ enableCssLayer: true }),
});
return finalProps;
};
Vite 또는 기타 SPA
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
modularCssLayers: true,
});
export default function AppTheme({ children }: { children: ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
import AppTheme from './theme';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<StyledEngineProvider enableCssLayer>
<AppTheme>{/* Your app */}</AppTheme>
</StyledEngineProvider>
</React.StrictMode>,
);
다른 스타일링 솔루션과 함께 사용하기 (Usage with other styling solutions)
Tailwind CSS v4 같은 다른 스타일링 솔루션과 통합하려면, modularCssLayers의 불리언 값을 레이어 순서를 지정하는 문자열로 바꿔요. Material UI는 mui 식별자를 찾아서 레이어를 올바른 순서로 생성해요:
const theme = createTheme({
- modularCssLayers: true,
+ modularCssLayers: '@layer theme, base, mui, components, utilities;',
});
생성된 CSS는 다음과 같이 보여요:
@layer theme, base, mui.global, mui.components, mui.theme, mui.custom, mui.sx, components, utilities;
주의사항 (Caveats)
이미 커스텀 스타일과 테마 오버라이드가 적용된 앱에서 modularCssLayers를 활성화하면, 적용 전후의 특이성 차이 때문에 UI의 룩앤필에 예상치 못한 변화가 생길 수 있어요.
예를 들어 Accordion 컴포넌트에 대해 다음과 같은 테마 스타일 오버라이드가 있다고 가정해 볼게요:
const theme = createTheme({
components: {
MuiAccordion: {
styleOverrides: {
root: {
margin: 0,
},
},
},
},
});
기본적으로 아코디언이 펼쳐졌을 때는 테마의 margin이 기본 margin 스타일보다 특이성이 높으므로 이 테마의 margin이 우선하지 않아요 — 그래서 이 코드는 아무 효과가 없어요.
modularCssLayers 옵션을 활성화하면, 캐스케이드 순서에서 테마 레이어가 컴포넌트 레이어보다 뒤에 오기 때문에 테마의 margin이 우선하게 돼요 — 그래서 스타일 오버라이드가 적용되고 아코디언이 펼쳐졌을 때 margin이 없는 상태가 돼요.
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Box from '@mui/material/Box';
import Switch from '@mui/material/Switch';
export default function CssLayersCaveat() {
const [cssLayers, setCssLayers] = React.useState(false);
const theme = React.useMemo(() => {
return createTheme({
modularCssLayers: cssLayers,
cssVariables: true,
components: {
MuiAccordion: {
styleOverrides: {
root: {
margin: 0,
},
},
},
},
});
}, [cssLayers]);
return (
<div>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px',
}}
>
<Typography
component="span"
sx={{ marginRight: '8px', fontSize: '14px', color: 'text.secondary' }}
>
No CSS Layers
</Typography>
<Switch checked={cssLayers} onChange={() => setCssLayers(!cssLayers)} />
<Typography
component="span"
sx={{ marginLeft: '8px', fontSize: '14px', color: 'text.secondary' }}
>
With CSS Layers
</Typography>
</Box>
<ThemeProvider theme={theme}>
<div>
<Accordion defaultExpanded>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1-content"
id="panel1-header"
>
<Typography component="span">Accordion 1</Typography>
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel2-content"
id="panel2-header"
>
<Typography component="span">Accordion 2</Typography>
</AccordionSummary>
<AccordionDetails>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</AccordionDetails>
</Accordion>
</div>
</ThemeProvider>
</div>
);
}