React
Material UI (MUI)를 통해 공통 컴포넌트를 커스텀마이징 하기 feat. MUI, MUI X Data Grid
2025년 8월 10일
1. 왜 MUI를 선택했는가?
- 신규 B2B 웹 포털 서비스 개발에서 디자이너 리소스가 전혀 없는 상황
- UI 가이드나 컴포넌트 스펙이 문서화되지 않아, 개발자가 직접 UI 구현 표준과 디자인 체계를 수립해야 하는 환경
- 별도의 디자인 시스템 문서나 Storybook 같은 컴포넌트 사양 관리 도구를 운영할 필요성이 없었음
- 대신, 개발 단계에서 React Router 기반의 페이지 환경에 공통으로 적용 가능한 커스텀 UI 컴포넌트를 제작하고, 이를 각 페이지에서 그대로 가져다 쓰는 방식으로 일관성 유지
- 예: 툴팁 컴포넌트를 직접 만들어 서비스 전역에 적용 → 각 서비스 모듈에서 동일한 UI·동일한 동작 보장
- 컴포넌트 커스터마이징 난이도가 낮고, 공식 문서와 커뮤니티 자료가 풍부
- MUI X Data Grid와의 조합 시
- 기본적으로 Row Virtualization(가상 스크롤) 지원 → 대규모 데이터 처리에 유리
- 컬럼/셀 스타일링, 툴바·UI 확장 등 커스터마이징 용이
- 커뮤니티 버전에서도 엑셀 내보내기, 고정 컬럼 등 핵심 기능을 직접 구현 가능 → 라이선스 제약 없이 확장 가능
2. 그럼 어떻게 커스텀마이징을 해볼까? — 실전 도전기
(1) 방향 먼저 잡기 (원칙 3가지)
- 전역은 theme로: 브랜드/일관성/반복되는 기본값은 MUI에서 제공하는
theme.components(variants, defaultProps, styleOverrides) 로 공통화 시킨다.- MUI 가이드/철학 링크 : https://mui.com/material-ui/customization/theme-components
- 재사용되는 공통 컴포넌트는 styled로: 여러 화면에서 반복되는 UI는
styled()로 래핑 시켜 프로덕트의 일관성을 맞춘다. - 일회성은 디자인 스타일은 sx로: 페이지 한정 보정·실험은
sx로 처리합니다.- MUI 2번, 3번 가이드/철학 링크: https://mui.com/material-ui/customization/how-to-customize
(2) 전역 테마 뼈대 만들기
-
브랜드 일관성과 사용성을 동시에 바라보기 때문에, 처음부터 기준을 맞춰두지 않으면 페이지마다 제각각 다른 팔레트, 여백, 폰트가 튀어나오게 됩니다 그렇기 때문에 Theme 설계 원칙을 세웠고 아래 네 가지 축을 중심으로 통일화를 진행 했습니다.
-
덕분에 이후 작업에서는
- "이 색상 뭐였죠?" →
theme.palette.primary.main - "이 타이틀 헤더 폰트?" →
variant="subtitle0" - "테블릿 브레이크 포인트는?" →
breakpoints.up('tablet')
처럼 공통 키워드만으로 즉시 대응 가능해졌습니다.
- "이 색상 뭐였죠?" →
-
개발자, 기획자, PM, 외주 개발자등 샘플 내역을 확인하여 공통 사용이 가능하도록 함

-
팔레트 확장
- 목표: 브랜드 정체성과 상태 색상을 코드 레벨에서 강제
- 브랜드 메인 컬러
primary, 부정 액션(negative), 두번째 색상secondary, 에러 (error)등 업무 문맥 전용 컬러를 라이트 모드와 다크 모드를palette에 추가했습니다. - 컬러 시스템의 네이밍 룰은 https://m3.material.io/ 를 따르고 있습니다.
-
브레이크 포인트 확장
- 목표: 반응형 기준을 전사 통일
- 기존
xs/sm/md/lg/xl외에 mobile/tablet/desktop을 명확히 선언하여 실제 지원하는 브레이크 포인트를 사용하도록 유도 합니다.
-
컴포넌트 확장
-
목표: 자주 쓰는 변형은 전역
variants로, 기본값은defaultProps로 고정 -
대략적인 두 가지 예제를 가져와 봤습니다.
-
MuiButton에서 variant만 바꾸면 동일 룩앤필 보장MuiButton: { defaultProps: {variant: 'contained', color: 'secondary'}, variants: [ ... 코드 중략 .... { props: {variant: 'negativeLight'}, style: { textTransform: 'none', backgroundColor: '#969FAD', color: '#ffffff', ':hover': {backgroundColor: '#828C97'}, // mui button disabled 공통 속성 ':disabled': { color: 'rgba(0, 0, 0, 0.26)', boxShadow: 'none', backgroundColor: 'rgba(0, 0, 0, 0.12)', }, }, ... 코드 중략 .... }, ], }, -
MuiChip에서 color 지정 시 자동 스타일 매핑
MuiChip: { styleOverrides: { root: ({theme, ownerState}) => ({ // ChipColor는 개발 시 지정한 색상을 맵핑해서 사용 ...(ChipColor[ownerState.color] && theme.unstable_sx({ px: 2, borderRadius: 2, height: 24, fontSize: '10px', })), }), label: ({theme, ownerState}) => ({ padding: ChipColor[ownerState.color] && 'initial', }), }, }, -
-
-
타이포그라피 확장
- 목표: 텍스트 스타일을 공통화
- 코드
typography: { h1: { fontFamily: ['XXXGDisplay'], }, h2: { fontFamily: ['XXXGDisplay'], }, h3: { fontFamily: ['XXXGDisplay'], }, ... 코드 중략... subtitle0: { fontSize: '16px', fontFamily: ['XXXGDisplay'], }, ... 코드 중략...
(3) 재사용 패턴 — styled vs sx, 그리고 theme로의 승격 기준
- 여러 화면에서 반복이 된다면
styled()로 공통 컴포넌트화를 합니다. - 브랜드/전역 기본값을 사용하는 것들이라면
theme.components의 defaultProps/variants/styleOverrides로 전역화의theme를 사용합니다. - 한 페이지 한정 혹은 레이아웃/간격/정렬 같은 국소적인 CSS 보정이 필요하다면
sx를 활용합니다.
// components/SectionHeader.tsx
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
export const SectionHeader = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1.5),
padding: theme.spacing(1, 0),
'& .caption': { color: theme.palette.text.secondary },
}));
const UserPage = () => {
return (
<>
<SectionHeader>
<Typography variant="subtitle0">유저 페이지</Typography>
</SectionHeader>
<DefaultContent>
<Grid container sx={{mt: 2}}>
... 코드 중략 ...
</Grid>
</DefaultContent>
</>
)
}
const AdminPage = () => {
return (
<SectionHeader>
<Typography variant="subtitle0">어드민 페이지</Typography>
</SectionHeader>
... 코드 중략 ...
)
}(4) MUI X Data Grid 사용 정책 & 라이선스 원칙
@mui/x-data-grid(Community)를 채택를 했고 해당 플랜은 MIT 라이선스로 무료 정책을 사용하고 있습니다.
- 우리가 택한 전략
- Community 정책만으로도 많은 기능을 제공하고 있기 때문에 사용에는 적합하였습니다.
- Pro/Premium에 해당하는 일부 요구(예: 고정 컬럼, Excel 내보내기)로 오픈소스를 참고해서 커스텀 구현이 필요했습니다.
- 예를 들어 엑셀 내보내기와 같은 경우
exceljs라이브러리를 조합하여 만들어야하며, 그리드의 모든 전처리 이벤트 바인딩 또한 공통 훅을 통해 제공 되고 있는 점을 오픈소스를 통해 확인 할 수 있었고 또한 레퍼런스를 참고해서 분석하여 개발 할 수 밖에 없었습니다.
- 예를 들어 엑셀 내보내기와 같은 경우
- 참고 레퍼런스
- 아래의 레퍼런스를 그대로 사용하는 경우 문제의 소지가 있음을 알려드리며 소스 분석과 비지니스의 흐름으로 참고 해주시면 좋을 것 같습니다.
(5) Data Grid 래핑 구조: 확장 가능한 컴포넌트 아키텍처
- Data Grid 래핑 구조: 확장 가능한 컴포넌트 아키텍처
GridWrapper (공개 Wrapper)
├─ GridContextProvider (컨텍스트/상태 주입)
├─ GridRoot (루트)
│ ├─ GridHeader (헤더)
│ ├─ GridBody (바디)
│ └─ GridFooterPlaceholder (푸터)
└─ privateApiRef (커스텀 훅/프리프로세서가 물리는 내부 API) -- 핵심 !- 최소 래핑 코드 예시
import * as React from 'react';
import {
GridRoot, GridHeader, GridBody, GridFooterPlaceholder,
GridContextProvider, useGridSelector, GridValidRowModel
} from '@mui/x-data-grid';
import { useDataGridDSProps } from './hooks/useDataGridDSProps';
import { useDataGridComponent } from './hooks/useDataGridComponent';
import { DSDataGridVirtualScroller } from './components/DSDataGridVirtualScroller';
// 메모라이제이션 작업이 꼭 필요합니다. 마우스 오버 이벤트에 대한 핸들링 시 같은 props에 대한 리렌더링 처리에 동일 데이터 비교가 필요합니다.
export const GridWrapper = React.memo(
React.forwardRef<HTMLDivElement, DataGridDSProps<GridValidRowModel>>(
(inProps, ref) => {
const props = useDataGridDSProps(inProps);
const privateApiRef = useDataGridComponent(props.apiRef, props);
return (
<GridContextProvider privateApiRef={privateApiRef} props={props}>
<GridRoot className={props.className} style={props.style} sx={props.sx} ref={ref} {...props.forwardedProps}>
<GridHeader />
<GridBody
VirtualScrollerComponent={DSDataGridVirtualScroller}
ColumnHeadersProps={{ pinnedColumns }}
/>
<GridFooterPlaceholder />
</GridRoot>
</GridContextProvider>
);
}
)
);
// useDataGridComponent.ts — 전처리/훅 바인딩
import {
useGridInitialization, useGridInitializeState,
// 표준 기능 훅
useGridColumns, useGridRows, useGridSorting, useGridFilter, useGridPagination,
useGridDensity, useGridFocus, useGridEvents, useGridDimensions, useGridVirtualization,
// …
} from '@mui/x-data-grid/internals';
import { useGridColumnPinning, columnPinningStateInitializer } from './columnPinning';
import { useGridExcelExport } from './excel/useGridExcelExport'; // 커스텀 구현...
// 그 외 row pinning, resize, grouping 등 커스텀/프로젝트 훅…
export const useDataGridComponent = (inputApiRef, props) => {
const apiRef = useGridInitialization(inputApiRef, props);
// 1) Pre-processors: 상태 초기화 단계에서 커스텀 상태 주입
useGridInitializeState(columnPinningStateInitializer, apiRef, props);
// ... rowPinningStateInitializer, columnResizeStateInitializer 등
// 2) Hooks: 표준 + 커스텀 기능을 한 번에 바인딩
useGridColumns(apiRef, props);
useGridRows(apiRef, props);
useGridSorting(apiRef, props);
useGridFilter(apiRef, props);
useGridPagination(apiRef, props);
useGridDensity(apiRef, props);
useGridColumnPinning(apiRef, props);
useGridVirtualization(apiRef, props);
useGridExcelExport(apiRef, props); // exceljs 조합
// …나머지 공통 훅 바인딩
return apiRef;
};6. 마치며 — MUI와 MUI X Data Grid 커스터마이징
MUI와 MUI X Data Grid를 선택하고 커스터마이징한 경험은 단순한 UI 구현을 넘어, UI 표준 수립 → 전역 테마화 → 컴포넌트 재사용 → 데이터 처리 성능 최적화로 이어지는 완성형 프론트엔드 아키텍처를 만드는 과정을 살펴 보았습니다.
- MUI의
theme.components기반의 전역 UI 체계 styled/sx승격 기준을 통한 철학- MUI X Data Grid Wrapper 무료 버전의 기능 확장의 요령과 라이선스 제약에 대한 고찰
를 통해 UI 라이브러리 선택과 커스터마이징은 단순한 '디자인 변경'이 아니라, 서비스 품질과 팀 생산성을 좌우하는 전략적 선택임을 다시 확인할 수 있었습니다.
앞으로도 기술을 선택할 때는
"왜 필요한지, 선택한 기술이 요구사항을 충족할 수 있는지, 정책과의 충돌은 없는지"
를 철저히 검토하고, 부족한 기능은 직접 구현하되 유지보수가 가능한 구조로 남기는 원칙을 지켜갈 것입니다.