MUI (Material UI) — Google Material Design의 React 구현
프론트엔드 개발 · 스타일링 & UI 시리즈 #7
세계에서 가장 많이 사용되는 React UI 컴포넌트 라이브러리 MUI. 방대한 컴포넌트 생태계, Emotion 기반 커스터마이징, 그리고 디자인 시스템 구축까지 완벽하게 다룹니다.
1. MUI란 무엇인가
MUI(Material UI) 는 Google의 Material Design 가이드라인을 React로 구현한 UI 컴포넌트 라이브러리입니다. 2014년 첫 릴리스 이후 꾸준히 성장해 현재 GitHub 스타 수 기준 가장 많이 사용되는 React UI 라이브러리입니다.
npm install @mui/material @emotion/react @emotion/styled
MUI는 단순한 UI 컴포넌트 라이브러리를 넘어 디자인 시스템 구축을 위한 플랫폼으로 발전했습니다. 버튼, 입력 필드 같은 기본 컴포넌트부터 데이터 그리드, 날짜 피커, 차트까지 방대한 생태계를 제공합니다.
2. MUI v5의 핵심 변화
MUI v5(2021년 출시)는 이전 버전에서 큰 변화를 가져왔습니다.
스타일 엔진 교체: JSS → Emotion
v4까지 사용하던 JSS를 버리고 Emotion을 기본 스타일 엔진으로 채택했습니다. 성능, 개발 경험, 생태계 측면에서 모두 개선되었습니다.
새로운 커스터마이징 API
- makeStyles → sx prop + styled() API
- withStyles → styled() API
- classes prop 방식은 유지
패키지 분리
@mui/material → 핵심 컴포넌트
@mui/icons-material → Material 아이콘
@mui/lab → 실험적 컴포넌트
@mui/x-data-grid → 고급 데이터 그리드 (별도 라이선스)
@mui/x-date-pickers → 날짜/시간 피커
3. 핵심 컴포넌트 가이드
레이아웃 컴포넌트
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
// Box: 범용 레이아웃 컴포넌트 (sx prop 지원)
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
p: 3,
backgroundColor: 'background.paper',
borderRadius: 2,
}}
>
내용
</Box>
// Container: 콘텐츠 최대 너비 제한
<Container maxWidth="lg">
<Grid container spacing={3}>
<Grid item xs={12} md={8}>
<Typography>메인 콘텐츠</Typography>
</Grid>
<Grid item xs={12} md={4}>
<Typography>사이드바</Typography>
</Grid>
</Grid>
</Container>
// Stack: 단방향 정렬에 최적화
<Stack direction="row" spacing={2} alignItems="center">
<Avatar src="/user.jpg" />
<Typography>홍길동</Typography>
<Button size="small">팔로우</Button>
</Stack>
폼 컴포넌트
import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
// TextField
<TextField
label="이메일"
type="email"
variant="outlined"
fullWidth
required
error={!!emailError}
helperText={emailError || '업무용 이메일을 입력하세요'}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
// Select
<FormControl fullWidth>
<InputLabel>역할</InputLabel>
<Select value={role} label="역할" onChange={(e) => setRole(e.target.value)}>
<MenuItem value="admin">관리자</MenuItem>
<MenuItem value="editor">편집자</MenuItem>
<MenuItem value="viewer">뷰어</MenuItem>
</Select>
</FormControl>
// Checkbox
<FormControlLabel
control={<Checkbox checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />}
label="이용약관에 동의합니다"
/>
피드백 컴포넌트
import Alert from '@mui/material/Alert';
import Snackbar from '@mui/material/Snackbar';
import Dialog from '@mui/material/Dialog';
import CircularProgress from '@mui/material/CircularProgress';
import Skeleton from '@mui/material/Skeleton';
// Alert
<Alert severity="success" onClose={() => setAlert(null)}>
데이터가 성공적으로 저장되었습니다.
</Alert>
<Alert severity="error">네트워크 오류가 발생했습니다.</Alert>
<Alert severity="warning">비밀번호 만료 30일 전입니다.</Alert>
// Snackbar + Alert 조합
<Snackbar open={open} autoHideDuration={4000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success" variant="filled">
저장 완료!
</Alert>
</Snackbar>
// Dialog
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>확인</DialogTitle>
<DialogContent>
<DialogContentText>정말 삭제하시겠습니까?</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)}>취소</Button>
<Button onClick={handleDelete} color="error" variant="contained">삭제</Button>
</DialogActions>
</Dialog>
// Skeleton — 로딩 UI
<Skeleton variant="rectangular" width="100%" height={200} />
<Skeleton variant="circular" width={40} height={40} />
<Skeleton variant="text" width="60%" />
네비게이션
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Drawer from '@mui/material/Drawer';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Breadcrumbs from '@mui/material/Breadcrumbs';
// AppBar + Drawer 조합 (대시보드 레이아웃)
<Box sx={{ display: 'flex' }}>
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
<Toolbar>
<Typography variant="h6" noWrap>Admin Dashboard</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
sx={{
width: 240,
'& .MuiDrawer-paper': { width: 240, boxSizing: 'border-box' },
}}
>
<Toolbar /> {/* AppBar 높이만큼 여백 */}
<List>
<ListItem button>
<ListItemIcon><HomeIcon /></ListItemIcon>
<ListItemText primary="홈" />
</ListItem>
</List>
</Drawer>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
{/* 페이지 내용 */}
</Box>
</Box>
4. 테마 시스템 심화
MUI의 테마는 디자인 시스템의 핵심입니다. createTheme으로 전체 앱의 디자인 토큰을 정의합니다.
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const theme = createTheme({
// 색상 팔레트
palette: {
mode: 'light', // 'light' | 'dark'
primary: {
main: '#3498db',
light: '#5dade2',
dark: '#2980b9',
contrastText: '#ffffff',
},
secondary: {
main: '#2ecc71',
contrastText: '#ffffff',
},
error: { main: '#e74c3c' },
warning: { main: '#f39c12' },
info: { main: '#3498db' },
success: { main: '#2ecc71' },
background: {
default: '#f5f5f5',
paper: '#ffffff',
},
text: {
primary: 'rgba(0, 0, 0, 0.87)',
secondary: 'rgba(0, 0, 0, 0.6)',
disabled: 'rgba(0, 0, 0, 0.38)',
},
},
// 타이포그래피
typography: {
fontFamily: "'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif",
h1: { fontSize: '2.5rem', fontWeight: 700, lineHeight: 1.2 },
h2: { fontSize: '2rem', fontWeight: 700, lineHeight: 1.3 },
h6: { fontSize: '1rem', fontWeight: 600 },
body1: { fontSize: '1rem', lineHeight: 1.6 },
body2: { fontSize: '.875rem', lineHeight: 1.6 },
button: { textTransform: 'none', fontWeight: 600 }, // 대문자 변환 해제
},
// 형태 (모서리 둥글기)
shape: {
borderRadius: 8,
},
// 간격 (기본 8px)
spacing: 8,
// 반응형 브레이크포인트
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1200,
xl: 1536,
},
},
// 컴포넌트 전역 기본값 오버라이드
components: {
MuiButton: {
defaultProps: {
disableElevation: true, // 박스 섀도우 제거
},
styleOverrides: {
root: {
borderRadius: 8,
padding: '8px 20px',
},
containedPrimary: {
'&:hover': {
backgroundColor: '#2980b9',
},
},
},
},
MuiTextField: {
defaultProps: {
variant: 'outlined',
size: 'small',
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: '0 2px 8px rgba(0,0,0,.08)',
},
},
},
MuiCssBaseline: {
styleOverrides: {
body: {
scrollbarWidth: 'thin',
},
},
},
},
});
// 다크 모드 테마
const darkTheme = createTheme({
...theme,
palette: {
...theme.palette,
mode: 'dark',
background: {
default: '#0f1923',
paper: '#1a2535',
},
},
});
// 적용
function App() {
const [mode, setMode] = useState('light');
return (
<ThemeProvider theme={mode === 'light' ? theme : darkTheme}>
<CssBaseline /> {/* CSS 리셋 + 테마 background 적용 */}
<Router>...</Router>
</ThemeProvider>
);
}
5. sx prop — 인라인 스타일의 진화
MUI의 sx prop은 단순한 인라인 스타일을 훨씬 뛰어넘습니다. 테마 값 참조, 반응형, 가상 선택자를 모두 지원합니다.
<Box
sx={{
// 테마 spacing 함수: 숫자 * 8px
p: 3, // padding: 24px
mt: 2, // margin-top: 16px
gap: 1.5, // gap: 12px
// 테마 색상 참조
backgroundColor: 'primary.main',
color: 'primary.contrastText',
borderColor: 'divider',
// 반응형 (모바일 퍼스트)
width: {
xs: '100%',
sm: '50%',
md: '33%',
},
fontSize: {
xs: '14px',
md: '16px',
},
// 가상 선택자
'&:hover': {
backgroundColor: 'primary.dark',
transform: 'translateY(-2px)',
transition: 'all .2s',
},
// 자식 선택자
'& .MuiTypography-root': {
fontWeight: 600,
},
// 다크 모드 조건
'.dark-mode &': {
backgroundColor: 'grey.900',
},
}}
>
내용
</Box>
// 타입스크립트: sx prop의 자동완성
import type { SxProps, Theme } from '@mui/material';
interface CardProps {
sx?: SxProps<Theme>;
}
6. 컴포넌트 커스터마이징
styled() API
import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
// MUI 컴포넌트 확장
const GradientButton = styled(Button)(({ theme }) => ({
background: `linear-gradient(45deg, ${theme.palette.primary.main}, ${theme.palette.secondary.main})`,
border: 0,
borderRadius: 25,
color: 'white',
padding: '10px 28px',
fontWeight: 700,
letterSpacing: '.05em',
'&:hover': {
background: `linear-gradient(45deg, ${theme.palette.primary.dark}, ${theme.palette.secondary.dark})`,
},
}));
// ownerState prop으로 조건부 스타일
const StatusChip = styled(Chip, {
shouldForwardProp: (prop) => prop !== 'status',
})(({ theme, status }) => ({
...(status === 'active' && {
backgroundColor: theme.palette.success.light,
color: theme.palette.success.dark,
}),
...(status === 'inactive' && {
backgroundColor: theme.palette.grey[200],
color: theme.palette.text.secondary,
}),
...(status === 'pending' && {
backgroundColor: theme.palette.warning.light,
color: theme.palette.warning.dark,
}),
}));
theme.components로 전역 오버라이드
// createTheme의 components 섹션
components: {
// 특정 컴포넌트의 모든 인스턴스에 적용
MuiButton: {
variants: [
// 커스텀 variant 추가
{
props: { variant: 'dashed' },
style: {
border: '2px dashed currentColor',
backgroundColor: 'transparent',
},
},
],
},
}
// 사용
<Button variant="dashed">커스텀 버튼</Button>
7. MUI 생태계
MUI X — 고급 컴포넌트
# 데이터 그리드 (무료 버전)
npm install @mui/x-data-grid
# 날짜/시간 피커
npm install @mui/x-date-pickers date-fns
import { DataGrid } from '@mui/x-data-grid';
import { DatePicker } from '@mui/x-date-pickers';
const columns = [
{ field: 'id', headerName: 'ID', width: 90 },
{ field: 'name', headerName: '이름', width: 150, editable: true },
{ field: 'email', headerName: '이메일', width: 200 },
{
field: 'status',
headerName: '상태',
renderCell: (params) => (
<StatusChip status={params.value} label={params.value} size="small" />
),
},
];
<DataGrid
rows={rows}
columns={columns}
pageSize={10}
checkboxSelection
disableSelectionOnClick
autoHeight
/>
MUI Joy UI
Material Design이 아닌 MUI 자체 디자인 시스템입니다. Material Design의 강한 시각적 언어를 피하고 싶을 때 선택합니다.
import Button from '@mui/joy/Button';
import Sheet from '@mui/joy/Sheet';
// Joy UI는 Material UI와 독립적으로 사용
8. 장단점 및 결론
장점
- 방대한 컴포넌트: 대부분의 UI 패턴을 즉시 사용 가능
- 접근성(a11y): WAI-ARIA 기준을 준수하는 컴포넌트
- Material Design: Google의 검증된 디자인 시스템 기반
- TypeScript 완전 지원: 모든 props, sx, 테마의 타입 안전성
- 강력한 테마: 디자인 시스템 수준의 테마 커스터마이징
- 생태계: MUI X 고급 컴포넌트, Joy UI, 방대한 커뮤니티
단점
- 번들 크기: 큰 번들 크기 (tree-shaking으로 완화 가능)
- Material Design 의존: 디자인이 Material Design처럼 보임
- 학습 곡선: 테마 시스템, sx prop, styled API 모두 배워야 함
- 커스터마이징 복잡성: 깊은 커스터마이징은 오버라이드 구조 이해 필요
- Emotion 런타임: 런타임 CSS-in-JS 오버헤드 존재
결론
MUI는 빠른 개발 속도와 일관된 디자인이 필요한 대시보드, 어드민 패널, B2B SaaS 앱에 최적입니다. 브랜드 아이덴티티가 강한 마케팅 사이트나 퍼블릭 웹사이트에서는 Material Design의 시각적 언어가 제약이 될 수 있습니다. 그런 경우에는 shadcn/ui나 Tailwind 기반 접근이 더 자유롭습니다.
댓글