본문으로 건너뛰기
5분 AI.
REPO DEEP DIVE · 2026-07-29 · MUI/MATERIAL-UI · React 컴포넌트 라이브러리의 대명사

mui/material-ui 딥다이브
— "버튼 하나 안 만들고" 앱을 완성하게 해주는 React UI 표준

UI 라이브러리

빠른 판단

무엇
Material Design 기반 React 컴포넌트와 테마·스타일 조정 기능을 제공하는 UI 라이브러리
이럴 때
버튼·폼·다이얼로그 등 공통 화면 요소를 조립하고 색·서체·간격을 브랜드에 맞게 조정할 때
Material UI(MUI)는 구글의 Material Design을 React로 독자 구현한 컴포넌트 라이브러리다. 버튼·다이얼로그·테이블·폼 같은 UI 부품을 미리 만들어 제공해, 개발자가 "디자인 시스템"부터 새로 짜지 않고 바로 제품을 조립할 수 있게 한다.

이 저장소는 실제로는 "Material UI 하나"가 아니라 여러 패키지의 모노레포다. 스타일 엔진을 감추는 @mui/styled-engine, CSS 유틸리티 @mui/system, 공용 헬퍼 @mui/utils 등이 뼈대를 이루고, 그 위에 @mui/material(컴포넌트 본체)이 얹힌다. 예전에 이 레포 안에 있던 Base UI·Joy UI·Pigment CSS는 최근 별도 저장소로 완전히 분리됐다 — "MUI 생태계 지도"가 계속 바뀌고 있다는 뜻이라 함정 섹션에서 자세히 다룬다.

(저장소 mui/material-ui · 라이선스 MIT(핵심 패키지) · 현재 버전 @mui/material 9.2.0 · pnpm 워크스페이스 + Lerna(independent) + Nx 캐싱 · Node ≥22.23.1 · 최신 커밋 2026-07-29, 릴리스 9.2.0은 2026-07-03 · MUI X(유료/일부 상용 라이선스)는 별도 저장소)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 무엇을 해결하나
  4. 전체 구조 — 패키지 지도
  5. 핵심 설계·동작 원리
  6. 기술 스택 · 의존성
  7. 설치 · 요구사항
  8. 실제 사용 흐름
  9. 함정 · 오해 방지
  10. 대안 비교
  11. 정리 · 누가 쓰면 좋은가
  12. 직접 해볼 수 있는 실습 과제
  13. 관련 기술 심화 학습 로드맵
  14. 핵심 키워드 사전

1프로젝트 한 줄 요약

한 문장으로: "구글 Material Design을 React 컴포넌트로 미리 만들어 둔, 세계에서 가장 많이 쓰이는 UI 라이브러리"

Material UI는 "이미 완성된 UI 부품 상자"다. 버튼·체크박스·다이얼로그·앱바·테이블·폼 필드처럼 웹 앱에 필요한 화면 요소를 <Button>, <Dialog>, <TextField> 같은 React 컴포넌트로 이미 만들어 배포한다. 개발자는 이걸 가져다 조립하고, 필요하면 테마(색·글꼴·간격)만 바꿔 자기 브랜드에 맞춘다.

npm install @mui/material @emotion/react @emotion/styled 한 줄이면 시작할 수 있다. 흔히 "MUI"라고 줄여 부르며, 회사 이름도 MUI(예전 material-ui.com)다. 이 저장소 하나에 @mui/material(컴포넌트), @mui/system(스타일 유틸), @mui/icons-material(구글 아이콘 2,000개+), @mui/lab(실험적 컴포넌트) 등 10여 개 패키지가 함께 산다.

한 줄 비유

"레고 랜드 정식 부품 세트"를 여는 일

레고로 집을 지을 때, 벽돌 하나하나를 처음부터 사출 성형하는 사람은 없다. 이미 규격화된 블록을 사서 조립하고, 원하면 색만 바꿔 칠한다.

Material UI는 웹 화면의 "정식 규격 블록 세트"다. 버튼·모달·테이블을 처음부터 CSS로 깎는 대신, 이미 접근성·반응형·다국어까지 검증된 블록을 가져다 조립하고, 팔레트(테마)만 바꿔 내 서비스 색으로 물들인다.

2왜 지금 주목받는가

10년 넘게 표준 자리 유지 · shadcn/ui식 "복사형" 대안의 부상 속 여전한 기본값 · CSS 변수·다크모드·제로런타임으로 계속 진화

① "React UI 라이브러리"라는 카테고리 자체를 만든 원조

Material UI는 2014년부터 이어져 온, React UI 컴포넌트 생태계의 사실상 표준이다. React 프로젝트에서 "UI 라이브러리 뭐 쓸까"라는 질문에 가장 먼저 나오는 이름 중 하나이고, 채용 공고·튜토리얼·부트캠프 커리큘럼에 광범위하게 등장한다. "오래돼서 유행이 지났다"는 인식과 달리, npm 주간 다운로드 기준으로도 여전히 최상위권을 지킨다.

② shadcn/ui식 "코드 복사" 흐름 속에서도 흔들리지 않는 이유

최근 shadcn/ui처럼 "라이브러리를 설치하는 대신 소스 코드를 복사해 온다"는 접근이 뜨면서 전통적 컴포넌트 라이브러리가 낡아 보인다는 말이 나온다. 하지만 Material UI는 "완성된 디자인 시스템 + 방대한 접근성·국제화(i18n) 테스트"가 이미 끝나 있다는 점에서 여전히 다른 선택지다. 처음부터 커스텀 디자인을 원하면 shadcn류가 맞고, 빠르게 완성도 있는 화면이 목표면 MUI가 여전히 강하다.

③ 계속되는 내부 진화 — CSS 변수 테마, 제로런타임 실험

겉보기엔 "오래된 라이브러리"지만 내부는 계속 바뀐다. CSS 변수 기반 테마(cssVariables: true)로 서버 사이드 렌더링에서도 깜빡임 없는 다크모드를 지원하게 됐고, Pigment CSS라는 빌드타임 제로런타임 스타일링 실험이 진행 중이다(현재는 별도 저장소로 독립). Base UI·Joy UI를 별도 프로젝트로 떼어내면서 이 레포 자체는 "@mui/material에 집중"하는 쪽으로 재편되기도 했다.

경쟁 지형
"완성형" vs "복사형" vs "헤드리스형"

Ant Design·Chakra UI·Mantine 같은 완성형 라이브러리와 경쟁하는 동시에, shadcn/ui 같은 복사형(소스를 내 코드로 가져오는) 접근, Radix·Base UI 같은 헤드리스(스타일 없는 동작만 제공) 접근과도 비교된다. Material UI는 "스타일까지 다 되어 있는 완성형"의 대표 주자 자리를 지키고 있다.

3무엇을 해결하나

"버튼 하나도 접근성·상태·반응형까지 신경 써야 하는" 문제를 없앤다

겉보기엔 간단해 보이는 버튼 하나에도 실은 포커스 링·키보드 조작·스크린리더 라벨·눌림 효과(ripple)·비활성 상태·로딩 상태·반응형 크기까지 신경 쓸 게 많다. 체크박스·드롭다운·모달은 더하다. 이걸 매 프로젝트 처음부터 만들면 시간이 많이 들고, 접근성 실수가 나기 쉽다.

Material UI는 이런 "당연히 필요하지만 반복적이고 까다로운" UI 로직을 미리 검증된 형태로 제공한다. 회사가 새 디자인 시스템 없이도 일관된 톤·간격·색상 체계로 화면을 빠르게 완성할 수 있고, 테마 하나로 전체 앱의 색·타이포그래피를 한 번에 바꿀 수 있다. 즉 "디자인 시스템 구축 비용"과 "접근성 준수 비용"을 동시에 줄여준다.

용어
디자인 시스템 (Design System)
색상·타이포그래피·간격·컴포넌트 규칙을 정리해 여러 화면에서 일관되게 쓰도록 만든 규약 모음. Material UI는 구글의 Material Design을 규약으로 삼아 이를 컴포넌트로 미리 구현해 둔 것이다.
용어
접근성 (a11y)
시각·운동 장애가 있는 사용자도 키보드·스크린리더로 이용할 수 있게 만드는 것. 버튼의 포커스 표시, 다이얼로그의 ARIA 속성 같은 것들이 예다. MUI 컴포넌트는 이런 처리가 기본 내장돼 있다.

4전체 구조 — 패키지 지도

pnpm 워크스페이스 안 16개 공개/내부 패키지 — "스타일 엔진 감추기 → CSS 유틸 → 컴포넌트" 3층

실제로 클론해 packages/를 열어 보면, "Material UI"라는 하나의 패키지가 아니라 여러 패키지가 계층을 이루는 모노레포임을 바로 확인할 수 있다. 핵심만 추리면 다음과 같다.

┌───────────────────────────────────────────────────────────────┐ │ MUI/MATERIAL-UI 패키지 계층 │ └───────────────────────────────────────────────────────────────┘ [3] @mui/material # 컴포넌트 본체 (Button, Dialog, TextField…) └ @mui/material-nextjs # Next.js App Router용 캐시 프로바이더 └ @mui/material-pigment-css # 제로런타임(Pigment CSS) 어댑터 │ depends on ▼ [2] @mui/system # styled(), sx prop, createTheme, 반응형 유틸 └ @mui/private-theming # 테마 컨텍스트 공유(레거시 @mui/styles 호환용) │ depends on ▼ [1] @mui/styled-engine # Emotion 래퍼 (기본) @mui/styled-engine-sc # styled-components 래퍼 (교체용, 동일 인터페이스) │ [0] @mui/utils · @mui/types # 공용 훅·헬퍼(useSlotProps 등)·공용 타입 곁가지: @mui/icons-material # 구글 Material 아이콘 2,000개+ (용량이 가장 큰 패키지, 130MB+) @mui/lab # 실험적/불안정 컴포넌트(정식 승격 전 대기실) @mui/codemod # 메이저 업그레이드 시 코드 자동 변환 CLI @mui/envinfo # 이슈 리포트용 환경 정보 수집 @mui/stylis-plugin-rtl # 오른쪽에서 왼쪽 언어(RTL) 지원 플러그인

이 레포에서 "빠진" 것들 — Base UI · Joy UI · Pigment CSS

예전 자료를 보면 @mui/base, @mui/joy가 이 모노레포 안에 있었다고 나온다. 실제로 클론해 확인하면 지금은 둘 다 존재하지 않는다. Base UI는 완전히 새로 설계돼 mui/base-ui 저장소(패키지명도 @base-ui/react·@base-ui-components/react)로 독립했고, Joy UI 관련 코드도 최근 커밋에서 "leftover Joy UI references"를 지우며 정리됐다. packages/pigment-css-react·packages/pigment-react 폴더를 열어보면 실제 코드 대신 "The package has moved to mui/pigment-css"라는 안내 README만 남아 있다 — 이는 문서와 실제 코드가 어긋나는 대표적 함정이라 9장에서 다시 다룬다.

패키지역할
@mui/materialMaterial Design 컴포넌트 본체. 이 저장소의 핵심 산출물
@mui/systemstyled(), sx prop, createTheme, 반응형 브레이크포인트 등 CSS 유틸 — Joy UI 같은 다른 컴포넌트 라이브러리도 이 위에 지어졌었음
@mui/styled-engine(-sc)Emotion↔styled-components를 교체 가능하게 감싸는 얇은 어댑터 계층
@mui/utilsuseSlotProps·useForkRef 등 컴포넌트 제작에 쓰이는 공용 훅/헬퍼
@mui/icons-material구글 Material 아이콘을 컴포넌트화한 별도 패키지(용량 최대)
Base UI (외부 저장소)스타일 없는 "헤드리스" 컴포넌트 — mui/base-ui로 독립, 이 레포엔 없음
Joy UI (레포에서 제거)Material Design과 다른 자체 디자인의 대안 라이브러리였으나 이 레포에서 정리됨
Pigment CSS (외부 저장소)빌드타임 제로런타임 CSS-in-JS — mui/pigment-css로 독립, @mui/material-pigment-css가 어댑터로만 남음

5핵심 설계·동작 원리

styled-engine 추상화 · createTheme/ThemeProvider · CSS 변수 다크모드 · slots/slotProps · Pigment CSS · MUI X 경계

① styled()·sx prop — Emotion을 "갈아 끼울 수 있게" 감춘다

Material UI의 모든 컴포넌트는 CSS-in-JS 라이브러리 Emotion 위에서 스타일을 만든다. 그런데 실제 소스(packages/mui-styled-engine/src/index.ts)를 보면 컴포넌트들이 Emotion을 직접 부르지 않는다. 대신 @mui/styled-engine이라는 얇은 재수출(re-export) 계층을 거친다.

// packages/mui-styled-engine/src/index.ts (발췌, 실제 소스)
import emStyled from '@emotion/styled';

function styled(tag, options) {
  const stylesFactory = emStyled(tag, options);
  // 개발 모드에서 styled(Comp)() 처럼 인자 없이 호출하면 경고를 띄운다
  return stylesFactory;
}
export default styled;
export { ThemeContext, keyframes, css } from '@emotion/react';

@mui/styled-engine-sc는 같은 인터페이스로 Emotion 대신 styled-components를 감싼다. 그래서 프로젝트 번들러 설정에서 @mui/styled-engine을 @mui/styled-engine-sc로 별칭(alias) 처리하면, MUI 컴포넌트 코드는 한 줄도 안 건드리고 내부 CSS-in-JS 엔진을 바꿀 수 있다. 이게 "styled-engine 추상화"의 정체다.

그 위에 @mui/system의 createStyled(packages/mui-system/src/createStyled/createStyled.js)가 한 겹 더 쌓인다. 여기서 모든 스타일드 컴포넌트에 sx prop을 자동으로 붙여준다 — attachTheme으로 테마를 주입하고, styleFunctionSx로 sx={{ p: 2, color: 'primary.main' }} 같은 객체를 실제 CSS로 변환한 뒤 기존 스타일 뒤에 이어붙인다. 즉 "Emotion 스타일 + sx 인라인 스타일"을 한 컴포넌트가 동시에 받는 구조다.

비유

전기 콘센트(styled-engine)와 가전제품(MUI 컴포넌트)의 관계다. 컴포넌트는 "표준 규격 콘센트"에만 꽂혀 있을 뿐, 그 뒤에 어떤 발전소(Emotion 또는 styled-components)가 있는지 신경 쓰지 않는다. 발전소를 바꿔도 콘센트 모양(인터페이스)만 같으면 가전제품은 그대로 쓸 수 있다.

용어
sx prop
MUI 컴포넌트 어디에나 붙일 수 있는 스타일 지름길. sx={{ mt: 2, display:'flex' }}처럼 테마 값(간격 단위·팔레트 색 이름)을 그대로 CSS로 바꿔준다. 매번 styled()로 새 컴포넌트를 안 만들어도 되는 "빠른 오버라이드" 용도다.

② createTheme·ThemeProvider — 테마 스케일과 팔레트 생성

@mui/system의 기본 createTheme(packages/mui-system/src/createTheme/createTheme.js)는 breakpoints·spacing·shape만 만드는 저수준 버전이다. 실제로 앱에서 쓰는 건 @mui/material/styles의 createTheme(packages/mui-material/src/styles/createTheme.ts)인데, 이게 팔레트 생성(createPalette)·타이포그래피 스케일·트랜지션·컴포넌트별 기본값까지 얹은 "완전판"이다.

import { createTheme, ThemeProvider } from '@mui/material/styles';

const theme = createTheme({
  palette: { primary: { main: '#1565c0' } },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button>눌러보세요</Button>
    </ThemeProvider>
  );
}

primary.main 색 하나만 주면, 내부적으로 createPalette가 light/dark 보조색을 자동 생성하고, 컴포넌트들은 ThemeProvider의 React Context를 통해 이 값을 읽어 자기 스타일을 결정한다. "버튼 색을 하나하나 바꾸는 대신 테마 하나로 앱 전체 톤을 바꾼다"는 MUI의 핵심 사용 방식이 여기서 나온다.

③ CSS 변수 기반 테마(cssVariables)와 다크모드 — 실제 분기 코드

v5까지는 테마 값이 JS 객체로만 존재해, 다크모드 전환 시 리액트가 전체 트리를 다시 렌더링해야 했다. 최근 버전은 cssVariables: true 옵션으로 테마 값을 실제 CSS 커스텀 프로퍼티(--mui-palette-primary-main 등)로 뽑아내는 방식을 지원한다. createTheme.ts의 실제 분기 로직을 그대로 옮기면:

// packages/mui-material/src/styles/createTheme.ts (발췌)
if (cssVariables === false) {
  // v5와 완전히 동일하게 동작 — JS 객체 테마
  return createThemeNoVars(options, ...args);
}
// cssVariables: true 인 경우 — CSS 변수 + colorSchemes 기반 테마
return createThemeWithVars({
  ...other,
  colorSchemes: colorSchemesInput,
  defaultColorScheme: defaultColorSchemeInput,
}, ...args);

cssVariables: true로 만들면 colorSchemes: { light: {...}, dark: {...} }를 함께 정의할 수 있고, 실제 화면 전환은 CSS의 data-mui-color-scheme 어트리뷰트만 바꿔치기하면 된다 — React 리렌더링 없이 브라우저가 CSS 변수 값을 즉시 다시 계산해준다. useColorScheme() 훅으로 모드를 읽고 바꾸며, 서버 사이드 렌더링에서 "처음 깜빡이는(FOUC)" 문제도 InitColorSchemeScript가 막아준다.

실전 팁

신규 프로젝트는 cssVariables: true를 켜는 것이 이제 사실상 권장 경로다. 다크모드가 리렌더링 없이 즉시 전환되고, SSR 깜빡임도 없다. 다만 커스텀 CSS에서 테마 값을 theme.palette.primary.main처럼 JS로 직접 읽던 코드가 있다면, CSS 변수 문자열(var(--mui-palette-primary-main))로 바뀌는 부분을 마이그레이션 가이드로 확인해야 한다.

④ slots·slotProps와 useSlotProps — 내부 구조를 열어주는 커스터마이징 API

Autocomplete·DataGrid류의 복합 컴포넌트는 내부에 여러 하위 요소(팝업, 리스트박스, 페이퍼 등)를 갖는다. 이를 개별적으로 바꿔치기·prop 추가할 수 있게 하는 것이 slots(컴포넌트 교체)와 slotProps(그 컴포넌트에 넘길 prop)다. 실제 구현은 @mui/utils의 useSlotProps(packages/mui-utils/src/useSlotProps/useSlotProps.ts)가 담당한다.

// 사용 예 — Select의 팝업 부분(paper)만 커스텀 스타일 주기
<Select
  slotProps={{
    paper: { sx: { borderRadius: 2 } },
  }}
>
  ...
</Select>

useSlotProps 내부는 외부에서 온 slotProps(함수 또는 객체 모두 허용)를 컴포넌트 내부 기본 props와 병합(mergeSlotProps)하고, ownerState(현재 상태값)를 필요하면 함께 넘겨준다. "라이브러리가 미리 정한 마크업 안에서, 내가 원하는 하위 요소만 정밀 타격으로 바꾼다"는 게 이 API의 핵심 아이디어이며, 최근 data-* 어트리뷰트도 slotProps에 직접 얹을 수 있도록 확장됐다(9.2.0 변경 로그).

⑤ Pigment CSS — 제로런타임 시도의 현황

MUI는 런타임에 스타일을 계산하는 Emotion 방식의 성능 한계(초기 렌더링·SSR 비용)를 넘기 위해 빌드타임에 CSS를 미리 뽑아내는 Pigment CSS를 실험해 왔다. 하지만 이 저장소 안의 packages/pigment-css-react·packages/pigment-react 폴더는 현재 빈 껍데기고, 실제 구현은 mui/pigment-css라는 별도 저장소로 옮겨졌다. 이 레포에 남아 있는 @mui/material-pigment-css는 "Pigment CSS 위에 Material UI와 같은 styled/테마 API를 얹어주는 얇은 어댑터"일 뿐이다.

⑥ MUI X와의 경계 — "복잡한 유료 컴포넌트는 여기 없다"

중요한 경계
DataGrid·DatePicker의 고급 기능은 이 레포에 없다

README에 명시된 대로, MUI X(mui/mui-x 별도 저장소)가 DataGrid·Date/Time Pickers·Charts·Tree View 같은 복합 컴포넌트를 담당한다. 이 중 상당수는 Community(MIT) 버전과 Pro/Premium(상용 라이선스) 버전이 나뉘어 있다. material-ui 레포 자체는 순수 MIT지만, "MUI 생태계 전체가 MIT다"라고 오해하면 안 된다.

6기술 스택 · 의존성

pnpm 워크스페이스 + Lerna(independent) + Nx 캐싱 · TypeScript 전면 전환 진행 중 · 버전 진실원은 각 package.json
영역내용
모노레포 관리pnpm 워크스페이스(pnpm-workspace.yaml) + Lerna(version: independent — 패키지별 독립 버전) + Nx(빌드 캐시·의존 그래프)
CSS-in-JS기본 Emotion(@emotion/react, @emotion/styled) — @mui/styled-engine-sc로 styled-components로 교체 가능
언어TypeScript 전면 전환이 진행형(레거시 .js + .d.ts 쌍을 .ts로 옮기는 작업이 최근 커밋 로그에도 계속 등장)
테스트Vitest(단위 테스트, cross-env TZ=UTC vitest) + Playwright(e2e·시각 회귀). 과거 Karma+Mocha 체계에서 이관 완료
문서 사이트docs/ 하위의 Next.js 기반 mui.com 문서 — 데모 코드가 곧 통합 테스트 역할도 겸함
CIGitHub Actions 워크플로 17개(ci.yml, CodeQL 보안 스캔, OpenSSF Scorecards, 릴리스 자동화 등)
코드 변환@mui/codemod — 메이저 버전 업그레이드 시 import 경로·prop 이름을 자동으로 고쳐주는 CLI

버전의 진실원(source of truth)은 각 패키지 폴더의 package.json이다. 저장소 루트 package.json의 version(예: 9.2.0)은 모노레포 관리용 사설(private) 버전이고, 실제로 npm에 배포되는 @mui/material·@mui/system·@mui/utils 등은 Lerna의 independent 모드 덕에 패키지마다 다른 속도로 버전이 오를 수 있다(실제로 @mui/styled-engine은 9.1.1인데 @mui/material은 9.2.0인 시점이 있다). "루트 버전 = 전체 버전"이라고 단정하면 안 된다.

용어
모노레포 (monorepo)
서로 관련된 여러 패키지를 하나의 저장소에 두고 함께 빌드·테스트·버전 관리하는 방식. 패키지 간 의존을 workspace 프로토콜(workspace:^)로 연결해 로컬 개발 중에도 항상 최신 코드끼리 맞물리게 한다.
용어
Lerna independent 버전
모노레포 안 모든 패키지가 같은 버전 번호를 쓰는 "fixed" 모드와 달리, 각 패키지가 자기 변경 이력에 따라 서로 다른 버전 번호를 갖게 하는 모드. MUI는 이 방식이라 @mui/material과 @mui/utils의 버전이 항상 같지 않다.

7설치 · 요구사항

사용자 입장: npm install 한 줄 · 기여자 입장: pnpm + Node 22+ 필요

일반 사용자(라이브러리로 쓰는 경우)는 매우 가볍다.

npm install @mui/material @emotion/react @emotion/styled
# 또는 styled-components 를 쓰고 싶다면:
npm install @mui/material @mui/styled-engine-sc styled-components

Roboto 폰트와 아이콘까지 쓰려면 @fontsource/roboto, @mui/icons-material을 추가로 설치한다. Next.js App Router라면 @mui/material-nextjs로 서버 컴포넌트 캐시 처리를 얹는 게 공식 권장이다.

대상요구사항
라이브러리 사용자React 18+ (React 19까지 지원), Node는 프레임워크(Vite·Next.js 등) 요구사항을 따름
레포 기여자(빌드)Node ≥22.23.1, pnpm 11.17.0(engineStrict로 강제). pnpm install → pnpm build
동시 지원 스타일 엔진Emotion(기본) 또는 styled-components — 필수 peerDependency는 둘 중 하나
제로런타임 실험Pigment CSS를 쓰려면 별도 저장소(mui/pigment-css)의 번들러 플러그인을 추가로 설정해야 함
주의
레포 클론해서 빌드하는 건 별개 난이도

이 저장소 자체를 빌드/기여하려면 pnpm install 시점부터 전체 워크스페이스(패키지 16개 + 문서 사이트)를 함께 설치하게 되어 상당히 무겁다. "라이브러리로 쓰기"와 "레포를 클론해 내부 코드를 고치기"는 완전히 다른 난이도의 작업임을 구분해야 한다.

8실제 사용 흐름

설치 → ThemeProvider로 감싸기 → 컴포넌트 조립 → sx/슬롯으로 미세 조정 → 다크모드

실제 화면 하나를 만드는 전형적인 흐름을 코드로 옮기면 다음과 같다.

import { createTheme, ThemeProvider, CssBaseline } from '@mui/material';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';

// 1) 앱 전역 테마 정의 — 색·다크모드 옵션까지 한 번에
const theme = createTheme({
  cssVariables: true,
  colorSchemes: { light: true, dark: true },
  palette: { primary: { main: '#1565c0' } },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      {/* 브라우저 기본 스타일 초기화 */}
      <CssBaseline />

      {/* 2) 컴포넌트 조립 — 이미 접근성·상태 처리 완료 */}
      <TextField label="이메일" fullWidth sx={{ mb: 2 }} />
      <Button variant="contained" sx={{ mt: 1 }}>
        가입하기
      </Button>
    </ThemeProvider>
  );
}

더 깊이 커스텀하고 싶으면 slots/slotProps로 내부 하위 요소를 겨냥하고, 아예 새 스타일드 컴포넌트가 필요하면 styled(Button)(...)로 확장한다. 다크모드 토글은 useColorScheme() 훅 하나로 끝난다 — 이 모든 게 위 5장에서 본 내부 설계(styled-engine 추상화·CSS 변수 테마·슬롯 API)가 실제 사용자에게 노출되는 표면이다.

9함정 · 오해 방지

Base UI·Joy UI는 이 레포에 없다 · MUI X는 부분 유료 · 아이콘 패키지 통짜 임포트 금지 · 루트 버전 ≠ 전체 버전
함정 ①
"Base UI로 커스텀 디자인 만들어보자" → 이 레포에서 코드가 안 보인다

블로그·튜토리얼에서 "MUI의 헤드리스 버전 Base UI"를 이 저장소 안에서 찾으려 하면 실패한다. Base UI는 완전히 새로 설계돼 mui/base-ui라는 별도 저장소로 독립했고, 패키지명도 @base-ui/react 계열로 바뀌었다. 이 레포의 docs/package.json에는 데모용으로만 @base-ui/react: "catalog:docs"가 들어 있을 뿐, 소스 자체는 없다.

함정 ②
Joy UI·Pigment CSS도 최근 정리됨

Joy UI(Material Design과 다른 자체 톤의 대안 라이브러리)는 이 저장소에서 관련 코드가 제거됐고(최신 변경 로그에 "Remove leftover Joy UI references" 항목이 실제로 있다), Pigment CSS(제로런타임 CSS-in-JS)도 mui/pigment-css로 독립했다. packages/pigment-css-react 폴더를 열어보면 실제 코드가 아니라 "이동했다"는 안내 README만 있다. "material-ui 레포 = MUI 생태계 전부"라는 가정은 더 이상 맞지 않는다.

함정 ③
"MUI는 전부 MIT다" → MUI X 상당 부분은 상용 라이선스

이 레포(@mui/material 등)는 확실히 MIT다. 하지만 DataGrid의 고급 기능, DatePicker의 일부, Charts, Tree View 같은 복합 컴포넌트를 담당하는 MUI X는 별도 저장소이며 Community(무료)와 Pro/Premium(유료 상용 라이선스)이 나뉜다. "MUI라서 전부 무료겠지"라고 넘겨짚고 프로덕션에 Pro 전용 기능을 쓰면 라이선스 위반이 될 수 있다.

함정 ④
@mui/icons-material를 통째로 import하면 번들이 폭발한다

이 패키지 하나만 로컬 디스크에서 130MB가 넘는다(아이콘 2,000개+ 각각을 개별 컴포넌트 파일로 내보내기 때문). import { Delete } from '@mui/icons-material'처럼 배럴(barrel) 임포트를 하면 번들러 설정에 따라 전체가 딸려 들어올 수 있으니, import DeleteIcon from '@mui/icons-material/Delete'처럼 개별 경로 임포트가 안전하다.

함정 ⑤
styled-engine을 styled-components로 바꾸는 건 설치만으로 안 끝난다

@mui/styled-engine-sc를 깔았다고 자동으로 엔진이 바뀌지 않는다. 번들러(Webpack·Vite 등) 설정에서 @mui/styled-engine 임포트를 @mui/styled-engine-sc로 별칭(alias) 처리해야 실제로 적용된다. 이 설정을 빼먹으면 두 엔진이 동시에 번들에 들어가 크기만 커진다.

참고 — 루트 버전 숫자에 낚이지 말 것

저장소 루트 package.json의 버전(예: 9.2.0)은 모노레포 관리용 사설 버전이다. @mui/material·@mui/system·@mui/styled-engine 등은 Lerna의 independent 모드로 패키지마다 버전이 따로 오른다. 실제로 쓰는 패키지의 package.json을 직접 확인하는 습관이 안전하다.

10대안 비교

Ant Design · Chakra UI · Mantine · shadcn/ui — "완성형" 안에서도, "복사형"과 비교해도
라이브러리포지션 · 차이점
Material UI구글 Material Design 기반 "완성형" 컴포넌트. 방대한 컴포넌트 수·성숙한 접근성·긴 실전 검증 기간이 강점. 디자인이 "구글스럽다"는 인상을 벗기려면 테마 커스터마이징 공수가 든다
Ant Design중국 기업(엔터프라이즈) 화면에 강한 "완성형" 라이브러리. 테이블·폼 등 데이터 중심 대시보드 컴포넌트가 촘촘함. 번들 크기와 커스터마이징 자유도에서 MUI와 종종 비교됨
Chakra UI유틸리티 스타일 prop과 접근성을 앞세운 "완성형". Material Design 같은 고정 스타일 언어가 없어 처음부터 자유로운 톤을 원할 때 선호됨. 최근 버전은 스타일 엔진을 자체 시스템(Panda CSS 계열)으로 전환하는 중
Mantine비교적 최근에 뜬 "완성형" 라이브러리. 훅(hooks) 모음이 강력하고 다크모드·폼 처리가 기본기가 좋다는 평. 커뮤니티·생태계 크기는 MUI보다 아직 작음
shadcn/ui"복사형" — 라이브러리를 설치하는 대신 Radix/Base UI 기반 소스 코드를 프로젝트에 직접 복사해 넣는다. 처음부터 완전히 다른 커스텀 디자인을 원하면 유리하지만, MUI처럼 "설치 즉시 완성된 스타일"은 제공하지 않는다
용어
완성형 vs 헤드리스 vs 복사형
완성형은 스타일까지 포함된 컴포넌트를 설치해 바로 쓰는 방식(MUI·Ant Design). 헤드리스는 동작·접근성만 제공하고 스타일은 직접 입히는 방식(Radix·Base UI). 복사형은 헤드리스 컴포넌트에 스타일을 입힌 소스 코드를 프로젝트로 복사해 오는 방식(shadcn/ui)이다.

11정리 · 누가 쓰면 좋은가

"디자인 시스템 없이 빠르게 완성도 있는 화면"이 목표라면 여전한 정답

Material UI는 "React 위에서 완성된 UI 부품 세트를 원하는 사람"에게 여전히 강력한 선택이다. 스타트업 MVP, 사내 어드민, 접근성이 중요한 공공·금융 서비스, "디자이너 없이도 봐줄 만한 화면"이 필요한 개인 프로젝트까지 폭넓게 맞는다. 반대로 처음부터 완전히 고유한 브랜드 룩을 원한다면 shadcn/ui 같은 복사형이나 Chakra·Mantine처럼 스타일 언어가 덜 고정된 쪽이 나을 수 있다.

레포 자체를 공부 대상으로 본다면, ① styled-engine 같은 "구현체를 갈아 끼울 수 있게 감추는" 어댑터 패턴, ② createTheme의 팔레트/스케일 자동 생성, ③ CSS 변수로 리렌더링 없이 다크모드를 전환하는 기법, ④ slots/slotProps로 "정해진 마크업 안에서 정밀하게 커스터마이징을 열어주는" API 설계, ⑤ 모노레포 안에서 패키지 경계·라이선스 경계(MUI X)를 어떻게 긋는지가 좋은 학습 포인트다.

이런 사람에게 추천

디자인팀 없이 빠르게 견고한 화면이 필요한 개발자, React 상태·테마 아키텍처를 공부하려는 학습자

버튼 하나까지 픽셀 단위로 새로 디자인하고 싶은 프로젝트보다는, "이미 검증된 UI 부품으로 빠르게 완성하고 테마로 톤만 맞추면 되는" 프로젝트에 잘 맞는다. 동시에 대규모 오픈소스 모노레포가 스타일 엔진 추상화·테마 시스템·커스터마이징 API를 어떻게 설계하는지 보고 싶은 사람에게도 좋은 교재다.

12직접 해볼 수 있는 실습 과제

예제 화면 조립 → 테마와 반응형 → 슬롯과 접근성 → 동작을 지키는 회귀 테스트

아래 과제는 의존성이 준비된 별도 실습용 사본에서 수행한다. 명령과 경로는 모두 Material UI 저장소 루트를 기준으로 한다. Node와 pnpm 요구사항은 그 사본의 package.json에서 확인한다.

실행 위치 확인

examples/material-ui-vite-ts/는 pnpm-workspace.yaml의 워크스페이스 목록에 포함되지 않으므로 pnpm --dir로 실행한다. 예제는 자체 package.json에 선언된 Material UI 의존성을 사용하며, packages/ 안의 소스를 바꾼 결과는 뒤의 단위 테스트에서 확인한다.

과제 ① · 입문

입력과 저장 상태가 연결된 설정 패널

목표: 완성된 컴포넌트를 조립하면서 React 상태와 컴포넌트 속성의 관계를 익힌다.

  1. examples/material-ui-vite-ts/package.json의 dev·build 스크립트와 src/main.tsx가 src/App.tsx를 렌더링하는 흐름을 읽는다.
  2. examples/material-ui-vite-ts/src/App.tsx에 TextField, Button, Typography로 닉네임 설정 패널을 만든다. 컴포넌트는 @mui/material/TextField처럼 개별 경로에서 가져온다.
  3. useState로 입력값과 저장된 값을 따로 관리한다. 입력란에 label="닉네임"을 붙이고, value·onChange를 연결한다. 공백을 제거한 입력이 비어 있으면 저장 버튼을 비활성화하고, 클릭하면 저장된 이름을 화면에 표시한다.
  4. 개발 서버를 실행해 동작을 확인한다. 서버를 유지한 채 다른 터미널에서 빌드 명령으로 타입 검사와 번들 생성을 확인한다.
pnpm --dir examples/material-ui-vite-ts dev
pnpm --dir examples/material-ui-vite-ts build

완료 기준: 빈 입력에서는 저장할 수 없고, 입력한 뒤 저장하면 이름이 표시된다. 입력을 다시 바꿔도 저장된 이름은 다음 저장 전까지 유지되며, 빌드가 통과한다.

과제 ② · 중급

테마 하나로 색·간격·화면 폭에 대응하기

목표: 테마의 공통 값이 sx와 CSS 변수로 이어지는 과정을 확인한다.

  1. packages/mui-material/src/styles/createTheme.ts와 packages/mui-system/src/createTheme/createSpacing.ts를 읽어 테마 생성의 분기와 간격 변환을 찾는다.
  2. examples/material-ui-vite-ts/src/main.tsx에서 @mui/material/styles의 createTheme·ThemeProvider를 가져온다. 아래 테마를 컴포넌트 바깥에 만들고, 기존 App을 프로바이더로 감싼 뒤 그 안에 CssBaseline을 추가한다.
  3. App.tsx에서 useColorScheme()을 호출하는 하위 컴포넌트를 만든다. 초기 mode가 없으면 표시를 보류하고, 두 버튼에서 각각 setMode('light')·setMode('dark')를 호출한다.
  4. pnpm --dir examples/material-ui-vite-ts dev로 실행한 뒤, 패널의 Box에 sx={{ p: 2, display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 2 }}를 적용한다. 개발자 도구에서 계산된 여백, 화면 폭별 배치, 색상 모드에 따른 CSS 변수 값을 확인한다.
const theme = createTheme({
  cssVariables: { colorSchemeSelector: 'class' },
  colorSchemes: {
    light: { palette: { primary: { main: '#1565c0' } } },
    dark: { palette: { primary: { main: '#90caf9' } } },
  },
  spacing: 10,
});

완료 기준: 위 설정에서 p: 2의 계산된 안쪽 여백은 20px이고, 창 폭을 바꾸면 세로·가로 배치가 전환된다. 모드 버튼으로 배경과 기본색이 바뀌며 --mui-palette-primary-main의 값도 달라진다.

과제 ③ · 중급

다이얼로그의 슬롯 속성과 키보드 동작 검증

목표: 내부 부품의 속성을 조정하면서 접근성 동작이 유지되는지 확인한다.

  1. packages/mui-material/src/Dialog/Dialog.d.ts에서 paper 슬롯을 확인하고, Dialog.js의 useSlot('paper', ...) 호출을 찾는다. 이어서 packages/mui-material/src/utils/useSlot.ts와 packages/mui-utils/src/mergeSlotProps/mergeSlotProps.ts에서 외부 속성이 합쳐지는 흐름을 읽는다.
  2. examples/material-ui-vite-ts/src/App.tsx에 open 상태와 열기 버튼을 만든다. 아래 구성에 필요한 Dialog·DialogTitle·DialogContent·Button을 개별 경로에서 가져온다.
  3. pnpm --dir examples/material-ui-vite-ts dev로 실행하고 열기 버튼으로 창을 연다. 개발자 도구로 data-testid와 둥근 모서리가 루트 배경이 아닌 대화상자 표면에 적용됐는지 확인한다.
  4. 마우스 없이 Tab·Shift+Tab으로 이동하고 Escape로 닫는다. 접근성 트리에서 대화상자의 역할과 제목도 확인한다.
<Dialog
  open={open}
  onClose={() => setOpen(false)}
  slotProps={{
    paper: {
      sx: { borderRadius: 3 },
      'data-testid': 'preview-paper',
    },
  }}
>
  <DialogTitle>설정 미리보기</DialogTitle>
  <DialogContent>
    <Button onClick={() => setOpen(false)}>닫기</Button>
  </DialogContent>
</Dialog>

완료 기준: 대화상자의 접근 가능한 이름이 “설정 미리보기”로 표시된다. 열린 동안 키보드 초점이 대화상자 안에 머무르고, 닫으면 열기 버튼으로 돌아온다. 슬롯 속성을 바꿔도 이 동작이 유지된다.

과제 ④ · 심화

로딩 중 중복 실행을 막는 회귀 테스트

목표: 화면의 약속을 테스트로 표현하고, 그 약속을 책임지는 구현까지 추적한다.

  1. packages/mui-material/src/Button/Button.js에서 disabled={disabled || loading}을 찾고, packages/mui-material/src/ButtonBase/ButtonBase.js로 상태가 전달되는 과정을 읽는다.
  2. packages/mui-material/src/Button/Button.test.js의 기존 테스트를 참고해 같은 파일에 테스트를 추가한다. createRenderer()의 render, 반환된 user·setProps, spy()로 클릭 횟수를 관찰한다.
  3. loading={true}인 버튼을 클릭해 호출 횟수가 0인지 확인한다. setProps({ loading: false }) 뒤 다시 클릭하면 1회가 되고, 다시 로딩 상태로 바꾸고 클릭해도 1회로 유지되는지 검증한다.
  4. 아래 명령으로 해당 파일의 jsdom 테스트를 실행한다. 실습용 사본에서만 로딩에 따른 비활성화를 잠시 제거해 새 테스트가 실패하는지 확인하고, 원래 구현을 복구한 뒤 다시 통과시킨다.
pnpm test:unit run packages/mui-material/src/Button/Button.test.js --project 'node:@mui/material'

완료 기준: 정상 구현에서는 테스트가 통과하고, 로딩 중 클릭을 허용하면 새 테스트가 실패한다. jsdom 테스트로 확인한 클릭 동작과 실제 브라우저에서 별도로 확인할 시각적 로딩 표시를 구분해 설명할 수 있다.

13관련 기술 심화 학습 로드맵

6주 동안 사용 코드에서 내부 설계로, 마지막에는 테스트와 서버 렌더링으로 확장한다

매주 “입력 속성 → 내부 처리 → 화면에서 관찰한 결과”를 연결해 기록한다. 실습은 앞의 설정 패널을 이어서 사용하고, 소스 읽기는 아래 경로를 기준으로 한다.

주차배울 개념읽을 파일·폴더결과물
1주차 React 속성·상태·이벤트, 입력값을 상태로 관리하는 방식, 예제와 라이브러리 소스의 경계 examples/material-ui-vite-ts/src/main.tsx
examples/material-ui-vite-ts/src/App.tsx
pnpm-workspace.yaml
입력·저장이 동작하는 설정 패널과 예제 실행 경로를 설명한 메모
2주차 디자인 토큰, 팔레트 생성, React Context를 통한 테마 전달 packages/mui-material/src/styles/createTheme.ts
packages/mui-material/src/styles/createPalette.js
packages/mui-material/src/styles/ThemeProvider.tsx
색·간격을 한곳에서 바꾸는 테마와 컴포넌트까지 값이 전달되는 흐름도
3주차 CSS-in-JS, 스타일 엔진 어댑터, sx 변환과 반응형 규칙 packages/mui-styled-engine/src/index.ts
packages/mui-system/src/createStyled/
packages/mui-system/src/styleFunctionSx/
p·팔레트 경로·반응형 객체가 어떤 CSS가 되는지 비교한 표와 화면
4주차 슬롯의 교체와 속성 병합, ownerState, 대화상자의 이름과 키보드 초점 관리 packages/mui-material/src/Dialog/
packages/mui-material/src/utils/useSlot.ts
packages/mui-utils/src/mergeSlotProps/
packages/mui-material/src/Unstable_TrapFocus/
슬롯을 조정한 대화상자, 열기·이동·닫기·초점 복귀를 기록한 점검표
5주차 사용자 동작 중심 테스트, jsdom과 실제 브라우저의 차이, 구현과 공개 타입의 관계 packages/mui-material/src/Button/Button.test.js
packages/mui-material/src/Button/Button.d.ts
vitest.shared.mts
로딩 중 클릭을 막는 회귀 테스트와 테스트가 지키는 동작을 설명한 짧은 문서
6주차 서버 렌더링과 하이드레이션, Emotion 스타일 수집, 초기 색상 모드와 CSS 선택자의 일치 examples/material-ui-nextjs-ts/src/app/layout.tsx
examples/material-ui-nextjs-ts/src/theme.ts
packages/mui-material-nextjs/src/
AppRouterCacheProvider와 InitColorSchemeScript의 역할을 나눈 렌더링 흐름도
마지막 주의 관찰 포인트

스타일 수집과 색상 모드 초기화는 서로 다른 일이다. Next.js 예제에서 캐시 프로바이더는 Emotion 스타일을 서버 렌더링 흐름에 연결하고, 초기화 컴포넌트는 본문보다 먼저 색상 모드를 설정한다. 테마의 colorSchemeSelector: 'class'와 초기화 컴포넌트의 attribute="class"가 맞는지도 함께 확인한다.

14핵심 키워드 사전

설정 파일·스타일 처리·컴포넌트 확장·테스트를 읽을 때 다시 찾을 15개 용어
디자인 토큰
색·간격·글꼴처럼 여러 화면에서 공유할 디자인 값을 이름으로 정리한 것이다. 이 레포에서는 테마의 palette·spacing·typography가 대표적이며, createTheme이 이 값을 컴포넌트가 사용할 형태로 구성한다.
워크스페이스
여러 패키지를 한 저장소에서 함께 관리하도록 묶은 작업 단위다. 이 레포의 pnpm-workspace.yaml이 묶을 경로를 정하므로, examples/에 폴더가 있다고 자동으로 워크스페이스 패키지가 되지는 않는다.
피어 의존성 · peerDependencies
라이브러리를 사용하는 앱이 함께 제공해야 하는 패키지와 호환 범위를 선언하는 규칙이다. packages/mui-material/package.json의 peerDependencies와 peerDependenciesMeta를 함께 보면 React와 스타일 엔진 관련 요구사항, 선택 의존성 여부를 구분할 수 있다.
React Context
중간 컴포넌트마다 속성을 전달하지 않고 하위 컴포넌트들이 공통 값을 읽게 하는 React 기능이다. Material UI의 ThemeProvider는 테마를 전달하는 입구이며, useTheme()으로 전달된 테마를 읽는다.
CSS-in-JS
자바스크립트 코드로 스타일을 정의하고 CSS로 적용하는 방식이다. 이 레포의 기본 스타일 엔진은 Emotion이며, packages/mui-styled-engine/src/index.ts가 Emotion의 스타일 생성 기능을 감싼다.
스타일 엔진 어댑터
컴포넌트가 특정 스타일 구현에 직접 얽히지 않도록 공통 사용 창구를 제공하는 계층이다. @mui/system은 @mui/styled-engine을 통해 스타일을 생성하며, 이 경계가 대체 엔진을 연결하는 지점이 된다.
sx 속성
테마 값과 반응형 규칙을 함께 쓸 수 있는 Material UI의 스타일 입력이다. styleFunctionSx가 p: 2 같은 간격 표현과 'primary.main' 같은 색상 경로를 CSS로 해석하므로, 숫자를 모두 픽셀로 읽으면 안 된다.
브레이크포인트
화면 폭에 따라 다른 스타일을 적용하는 기준점이다. 이 레포에서는 sx={{ flexDirection: { xs: 'column', sm: 'row' } }} 같은 입력을 packages/mui-system/src/breakpoints/의 처리와 연결해 읽을 수 있다.
CSS 사용자 정의 속성
--이름으로 값을 선언하고 var(...)로 읽는 브라우저의 CSS 변수 기능이다. Material UI의 CSS 변수 테마는 --mui-palette-primary-main 같은 값을 만들며, 개발자 도구에서 현재 적용된 값을 확인할 수 있다.
색상 모음 · colorSchemes
밝은 화면과 어두운 화면에 사용할 팔레트 등을 이름별로 묶은 테마 설정이다. Material UI에서는 colorSchemes로 값을 준비하고 useColorScheme()으로 현재 모드를 읽거나 바꾸며, 수동 전환에 쓸 선택자도 함께 설정한다.
슬롯 · slots
복합 컴포넌트 내부에서 교체할 수 있도록 이름을 붙인 부품 자리다. Dialog의 paper·backdrop 등이 해당하며, slots는 그 자리에 사용할 컴포넌트를 지정한다.
슬롯 속성 · slotProps
슬롯에 전달할 속성을 이름별로 지정하는 설정이다. Dialog의 slotProps.paper에 스타일이나 데이터 속성을 넣으면 useSlot과 mergeSlotProps를 거쳐 내부 속성과 합쳐진다.
ownerState
컴포넌트가 기본값과 현재 속성 등을 모아 내부 스타일과 슬롯 처리에 전달하는 상태 묶음이다. Dialog.js에서는 fullScreen·scroll 등이 포함되며, 함수 형태의 slotProps도 이 값을 받을 수 있다.
회귀 테스트
코드를 바꾼 뒤에도 이전에 보장하던 동작이 유지되는지 확인하는 테스트다. Button.test.js처럼 클릭·키보드·비활성 상태를 검증하는 테스트가 해당하며, 화면 모양을 비교하는 시각 회귀 테스트와는 확인 대상이 다르다.
하이드레이션
서버가 만든 HTML에 브라우저의 React가 이벤트와 상태 처리를 연결하는 과정이다. 이 레포의 Next.js 예제에서는 초기 색상 모드 설정과 서버에서 생성한 스타일의 전달을 함께 살펴보며 첫 화면과 브라우저 초기 상태가 맞는지 공부할 수 있다.