sehyun.dev
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가지)

  1. 전역은 theme로: 브랜드/일관성/반복되는 기본값은 MUI에서 제공하는 theme.components (variants, defaultProps, styleOverrides) 로 공통화 시킨다.
    1. MUI 가이드/철학 링크 : https://mui.com/material-ui/customization/theme-components
  2. 재사용되는 공통 컴포넌트는 styled로: 여러 화면에서 반복되는 UI는 styled()로 래핑 시켜 프로덕트의 일관성을 맞춘다.
  3. 일회성은 디자인 스타일은 sx로: 페이지 한정 보정·실험은 sx로 처리합니다.
    1. MUI 2번, 3번 가이드/철학 링크: https://mui.com/material-ui/customization/how-to-customize

(2) 전역 테마 뼈대 만들기

  • 브랜드 일관성과 사용성을 동시에 바라보기 때문에, 처음부터 기준을 맞춰두지 않으면 페이지마다 제각각 다른 팔레트, 여백, 폰트가 튀어나오게 됩니다 그렇기 때문에 Theme 설계 원칙을 세웠고 아래 네 가지 축을 중심으로 통일화를 진행 했습니다.

  • 덕분에 이후 작업에서는

    • "이 색상 뭐였죠?" → theme.palette.primary.main
    • "이 타이틀 헤더 폰트?" → variant="subtitle0"
    • "테블릿 브레이크 포인트는?" → breakpoints.up('tablet')

    처럼 공통 키워드만으로 즉시 대응 가능해졌습니다.

  • 개발자, 기획자, PM, 외주 개발자등 샘플 내역을 확인하여 공통 사용이 가능하도록 함

image.png

  1. 팔레트 확장

    1. 목표: 브랜드 정체성과 상태 색상을 코드 레벨에서 강제
    2. 브랜드 메인 컬러 primary, 부정 액션(negative), 두번째 색상 secondary , 에러 (error)등 업무 문맥 전용 컬러를 라이트 모드와 다크 모드를 palette에 추가했습니다.
    3. 컬러 시스템의 네이밍 룰은 https://m3.material.io/ 를 따르고 있습니다.
  2. 브레이크 포인트 확장

    1. 목표: 반응형 기준을 전사 통일
    2. 기존 xs/sm/md/lg/xl 외에 mobile/tablet/desktop을 명확히 선언하여 실제 지원하는 브레이크 포인트를 사용하도록 유도 합니다.
  3. 컴포넌트 확장

    1. 목표: 자주 쓰는 변형은 전역 variants로, 기본값은 defaultProps로 고정

    2. 대략적인 두 가지 예제를 가져와 봤습니다.

      1. 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)',
                    },
                  },
                  ... 코드 중략 ....
                },
              ],
            },
      2. 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',
              }),
            },
          },
  4. 타이포그라피 확장

    1. 목표: 텍스트 스타일을 공통화
    2. 코드
    typography: {
        h1: {
          fontFamily: ['XXXGDisplay'],
        },
        h2: {
          fontFamily: ['XXXGDisplay'],
        },
        h3: {
          fontFamily: ['XXXGDisplay'],
        },
        ... 코드 중략...
        subtitle0: {
          fontSize: '16px',
          fontFamily: ['XXXGDisplay'],
        },
        ... 코드 중략...

(3) 재사용 패턴 — styled vs sx, 그리고 theme로의 승격 기준

  1. 여러 화면에서 반복이 된다면 styled()공통 컴포넌트화를 합니다.
  2. 브랜드/전역 기본값을 사용하는 것들이라면 theme.componentsdefaultProps/variants/styleOverrides전역화의 theme 를 사용합니다.
  3. 한 페이지 한정 혹은 레이아웃/간격/정렬 같은 국소적인 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 라이선스로 무료 정책을 사용하고 있습니다.
  1. 우리가 택한 전략
    1. Community 정책만으로도 많은 기능을 제공하고 있기 때문에 사용에는 적합하였습니다.
    2. Pro/Premium에 해당하는 일부 요구(예: 고정 컬럼, Excel 내보내기)로 오픈소스를 참고해서 커스텀 구현이 필요했습니다.
      1. 예를 들어 엑셀 내보내기와 같은 경우 exceljs 라이브러리를 조합하여 만들어야하며, 그리드의 모든 전처리 이벤트 바인딩 또한 공통 훅을 통해 제공 되고 있는 점을 오픈소스를 통해 확인 할 수 있었고 또한 레퍼런스를 참고해서 분석하여 개발 할 수 밖에 없었습니다.
  2. 참고 레퍼런스
  • 아래의 레퍼런스를 그대로 사용하는 경우 문제의 소지가 있음을 알려드리며 소스 분석과 비지니스의 흐름으로 참고 해주시면 좋을 것 같습니다.

(5) Data Grid 래핑 구조: 확장 가능한 컴포넌트 아키텍처

  1. Data Grid 래핑 구조: 확장 가능한 컴포넌트 아키텍처
GridWrapper (공개 Wrapper)
 ├─ GridContextProvider (컨텍스트/상태 주입)
 ├─ GridRoot (루트)
 │   ├─ GridHeader (헤더)
 │   ├─ GridBody (바디)
 │   └─ GridFooterPlaceholder (푸터)
 └─ privateApiRef (커스텀 훅/프리프로세서가 물리는 내부 API) -- 핵심 !
  1. 최소 래핑 코드 예시
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 라이브러리 선택과 커스터마이징은 단순한 '디자인 변경'이 아니라, 서비스 품질과 팀 생산성을 좌우하는 전략적 선택임을 다시 확인할 수 있었습니다.

앞으로도 기술을 선택할 때는

"왜 필요한지, 선택한 기술이 요구사항을 충족할 수 있는지, 정책과의 충돌은 없는지"

를 철저히 검토하고, 부족한 기능은 직접 구현하되 유지보수가 가능한 구조로 남기는 원칙을 지켜갈 것입니다.