머티리얼 UI(MUI)는 Google의 머티리얼 디자인을 구현하는 오픈 소스 React 구성요소 라이브러리입니다. 미리 만들어진 버튼, 텍스트 필드, 대화 상자, 목록 및 테마 시스템을 모두 React 구성 요소로 제공합니다.
마지막 부분은 단일 HTML 파일에 중요합니다. 머티리얼 UI는 링크한 스타일시트가 아닙니다. 페이지에는 React와 구성 요소를 렌더링하는 스크립트도 필요합니다. 작동하는 가장 작은 버전은 다음과 같습니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Material UI in one HTML file</title>
<!-- Roboto, the font Material UI uses by default -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<!-- The import map must come before the module script.
The last four entries send every copy of React to one version. -->
<script type="importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@19.2.8/+esm",
"react-dom/client": "https://cdn.jsdelivr.net/npm/react-dom@19.2.8/client/+esm",
"@mui/material": "https://cdn.jsdelivr.net/npm/@mui/material@9.4.0/+esm",
"https://cdn.jsdelivr.net/npm/react@18.2.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.1.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@%5E17.0.0%20%7C%7C%20%5E18.0.0%20%7C%7C%20%5E19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/"
}
}
</script>
<style>
body { margin: 0; font-family: Roboto, system-ui, sans-serif; }
</style>
</head>
<body>
<div id="root"><p style="padding: 20px">Loading Material UI…</p></div>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Box, Button, Stack, Typography } from '@mui/material';
const h = React.createElement; // plain JavaScript instead of JSX: no build step
function App() {
const [count, setCount] = React.useState(0);
return h(Box, { sx: { p: 3 } },
h(Typography, { variant: 'h5', gutterBottom: true }, 'Hello, Material UI'),
h(Typography, { color: 'text.secondary', sx: { mb: 2 } },
'Real MUI components, loaded from jsDelivr.'),
h(Stack, { direction: 'row', spacing: 1.5 },
h(Button, { id: 'add', variant: 'contained', onClick: () => setCount(count + 1) },
'Clicked ' + count + (count === 1 ? ' time' : ' times')),
h(Button, { id: 'reset', variant: 'outlined', onClick: () => setCount(0) }, 'Reset')
)
);
}
createRoot(document.getElementById('root')).render(h(App));
</script>
</body>
</html>
npm도 없고 번들러도 없고 서버도 없습니다. 파일은 브라우저에서 그대로 열립니다.
파일에 필요한 것
Material UI 문서는 npm을 사용하여 설치하고 React 및 React DOM을 피어 종속성으로 나열합니다. 단일 파일에서 해당 세 가지 패키지는 대신 CDN에서 제공됩니다.
| 조각 | 기능 | 여기서 나오는 내용 |
|---|---|---|
| 로보토 | Material UI가 기본적으로 사용하는 글꼴 | 구글 폰트 링크 |
| 반응과 반응 DOM | 구성 요소를 실행하고 그립니다. | jsDelivr |
@mui/material |
구성요소와 테마 | jsDelivr |
| 귀하의 스크립트 | 구성 요소에서 페이지를 빌드합니다. | 파일의 모듈 스크립트 |
문서에서는 CDN 옵션도 설명하고 이를 "빠른 프로토타입 제작을 위한 훌륭한 옵션"이라고 부릅니다. 공식 CDN 예제는 esm.sh에서 패키지를 로드하고 브라우저에서 Babel을 사용하여 JSX를 컴파일합니다. 이 가이드에서는 대신 cdn.jsdelivr.net 및 일반 JavaScript를 사용합니다.
가장 작은 작업 페이지
이 페이지의 모든 예시는 다음 순서대로 동일한 5개 부분으로 구성됩니다.
- Roboto 링크가 머리에 있습니다.
- 가져오기 맵(역시 헤드에 있음) 고정된 jsDelivr URL에
react및@mui/material와 같은 짧은 이름을 제공합니다. - 빈
<div id="root">. React는 그 안에 페이지를 그립니다. - React,
createRoot및 구성요소를 가져오는 모듈 스크립트입니다. - 1개의 렌더링 호출:
createRoot(root).render(h(App)).

가져오기 맵은 모듈 스크립트 앞에 와야 합니다. MDN에서는 선언된 이름을 사용하여 모듈을 가져오는 스크립트보다 먼저 가져오기 맵을 처리해야 한다고 말합니다.
<script type="importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@19.2.8/+esm",
"react-dom/client": "https://cdn.jsdelivr.net/npm/react-dom@19.2.8/client/+esm",
"@mui/material": "https://cdn.jsdelivr.net/npm/@mui/material@9.4.0/+esm"
}
}
</script>
<div id="root"></div>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Button } from '@mui/material';
const h = React.createElement;
createRoot(document.getElementById('root'))
.render(h(Button, { variant: 'contained' }, 'Hello'));
</script>
jsDelivr npm URL에 /+esm를 추가하면 패키지가 ES 모듈로 반환됩니다. 페이지가 변경되지 않도록 모든 URL에 정확한 버전을 고정하세요.
가져오기 맵에 추가 선이 4개 있는 이유
위의 짧은 지도만으로는 충분하지 않습니다. 실행하면 페이지가 공백으로 유지되고 콘솔에 다음 오류가 표시됩니다.
Cannot read properties of null (reading 'useContext')
원인은 React의 두 복사본입니다. Material UI는 기본 스타일 엔진인 Emotion과 같은 도우미 패키지를 가져옵니다. 2026-10-01을 확인했을 때 jsDelivr의 일부 헬퍼 빌드는 19.2.8 대신 19.0.0과 같은 다른 React 버전을 가져왔습니다.

React 문서에서는 이에 대해 설명합니다. 후크가 작동하려면 코드에서 가져오는 react가 react-dom가 사용하는 모듈과 동일해야 합니다.
가져오기 맵 키는 전체 URL일 수 있으며 /로 끝나는 키는 이 키로 시작하는 모든 주소를 다시 매핑합니다. 따라서 다음 줄은 이탈된 버전을 하나의 복사본으로 보냅니다.
"https://cdn.jsdelivr.net/npm/react@18.2.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.1.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@%5E17.0.0%20%7C%7C%20%5E18.0.0%20%7C%7C%20%5E19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/"
이 줄은 여기에 고정된 버전과 일치합니다. 버전을 변경하는 경우 DevTools에서 네트워크 패널을 열고 react@로 필터링한 다음 자신의 버전이 아닌 다른 React 버전에 대한 줄을 추가하세요.
JSX 또는 일반 createElement
머티리얼 UI 예제는 일반적으로 <Button variant="contained">와 같은 JSX로 작성됩니다. React 문서에서는 JSX를 JavaScript용 구문 확장이라고 부르며 일반 모듈 스크립트에서는 이를 실행할 수 없습니다. 브라우저는 Unexpected token '<'가 있는 첫 번째 <에서 중지됩니다.
여기의 예에서는 h로 이름이 변경된 React.createElement를 사용합니다. React 문서에는 JSX의 대안으로 나열되어 있습니다. 각 JSX 태그는 하나의 호출이 됩니다.
// JSX (needs a compile step)
<Button variant="contained" onClick={save}>Save</Button>
// The same thing, runs as is
h(Button, { variant: 'contained', onClick: save }, 'Save')
JSX를 선호하는 경우 Babel 독립 실행형은 브라우저에서 <script type="text/babel"> 태그를 컴파일합니다. Babel 문서에서는 프로덕션에서는 이를 권장하지 않으며 미리 컴파일할 것을 권장합니다.
관련된 실수는 구성 요소를 HTML에 직접 작성하는 것입니다. 브라우저는 이를 일반 태그로 읽습니다.

테마: 컬러 및 다크 모드
구성 요소는 테마에서 색상, 모서리 및 글꼴을 가져옵니다. createTheme로 하나를 만들고 ThemeProvider로 페이지를 래핑하면 내부의 모든 구성 요소가 이를 따릅니다. CssBaseline는 페이지 배경과 텍스트 색상도 따라오게 만듭니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Material UI theme: colour and dark mode</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<script type="importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@19.2.8/+esm",
"react-dom/client": "https://cdn.jsdelivr.net/npm/react-dom@19.2.8/client/+esm",
"@mui/material": "https://cdn.jsdelivr.net/npm/@mui/material@9.4.0/+esm",
"https://cdn.jsdelivr.net/npm/react@18.2.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.1.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@%5E17.0.0%20%7C%7C%20%5E18.0.0%20%7C%7C%20%5E19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/"
}
}
</script>
</head>
<body>
<div id="root"><p style="padding: 20px; font-family: sans-serif">Loading Material UI…</p></div>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import {
createTheme, ThemeProvider, CssBaseline,
Box, Stack, Button, TextField, Alert, Switch, FormControlLabel, Typography
} from '@mui/material';
const h = React.createElement;
const colors = { Indigo: '#3f51b5', Teal: '#00897b', Orange: '#e65100' };
function App() {
const [dark, setDark] = React.useState(false);
const [main, setMain] = React.useState(colors.Indigo);
// One theme object styles every component inside ThemeProvider
const theme = React.useMemo(() => createTheme({
palette: { mode: dark ? 'dark' : 'light', primary: { main } },
shape: { borderRadius: 10 },
}), [dark, main]);
return h(ThemeProvider, { theme },
h(CssBaseline), // page background and text colour follow the theme
h(Box, { sx: { p: 2.5, maxWidth: 520 } },
h(Stack, { direction: 'row', spacing: 1, sx: { mb: 2, flexWrap: 'wrap' } },
Object.entries(colors).map(([name, hex]) =>
h(Button, {
key: name, size: 'small', onClick: () => setMain(hex),
variant: hex === main ? 'contained' : 'outlined',
}, name)),
h(FormControlLabel, {
sx: { ml: 1 },
label: 'Dark',
control: h(Switch, { id: 'dark', checked: dark, onChange: (e) => setDark(e.target.checked) }),
})
),
h(Typography, { variant: 'subtitle2', sx: { mb: 1 } }, 'The three Button variants'),
h(Stack, { direction: 'row', spacing: 1, sx: { mb: 2.5 } },
h(Button, { variant: 'text' }, 'Text'),
h(Button, { variant: 'outlined' }, 'Outlined'),
h(Button, { id: 'contained', variant: 'contained' }, 'Contained')
),
h(TextField, { label: 'Your name', size: 'small', fullWidth: true, sx: { mb: 2 } }),
h(Alert, { severity: 'info' }, 'Change the colour or switch to dark: every component follows.')
)
);
}
createRoot(document.getElementById('root')).render(h(App));
</script>
</body>
</html>
다크 모드는 하나의 팔레트 설정입니다. 문서에 따르면 Material UI에는 밝은(기본값)과 어두운 두 가지 팔레트 모드가 있습니다.
const theme = createTheme({
palette: { mode: 'dark', primary: { main: '#00897b' } },
});
Button는 text(기본값), outlined 및 contained의 세 가지 변형으로 제공됩니다. 패딩 및 여백을 위한 sx: { p: 2, mb: 1 }와 같은 sx 소품에서 더 작은 조정이 이루어집니다.
완성된 예: 작업 목록
이것은 양식, 확인란이 있는 목록, 두 개의 아이콘 및 스낵바 메시지를 추가합니다. 양식은 페이지 내부에서 처리되므로 아무데도 전송되지 않습니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Material UI task list</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<script type="importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@19.2.8/+esm",
"react-dom/client": "https://cdn.jsdelivr.net/npm/react-dom@19.2.8/client/+esm",
"@mui/material": "https://cdn.jsdelivr.net/npm/@mui/material@9.4.0/+esm",
"@mui/icons-material/Delete": "https://cdn.jsdelivr.net/npm/@mui/icons-material@9.4.0/Delete/+esm",
"@mui/icons-material/Add": "https://cdn.jsdelivr.net/npm/@mui/icons-material@9.4.0/Add/+esm",
"https://cdn.jsdelivr.net/npm/react@18.2.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@19.1.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/",
"https://cdn.jsdelivr.net/npm/react@%5E17.0.0%20%7C%7C%20%5E18.0.0%20%7C%7C%20%5E19.0.0/": "https://cdn.jsdelivr.net/npm/react@19.2.8/"
}
}
</script>
</head>
<body>
<div id="root"><p style="padding: 20px; font-family: sans-serif">Loading Material UI…</p></div>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import {
createTheme, ThemeProvider, CssBaseline, Paper, Typography, Stack, TextField, Button,
List, ListItem, ListItemIcon, ListItemText, Checkbox, IconButton, Chip, Snackbar
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
const h = React.createElement;
const theme = createTheme({ palette: { primary: { main: '#00796b' }, background: { default: '#eef2f1' } } });
let nextId = 3;
function App() {
const [tasks, setTasks] = React.useState([
{ id: 1, text: 'Pick a theme colour', done: true },
{ id: 2, text: 'Send the share link', done: false },
]);
const [text, setText] = React.useState('');
const [toast, setToast] = React.useState('');
// The form is handled in the page: nothing is sent to a server
function add(e) {
e.preventDefault();
const value = text.trim();
if (!value) return;
setTasks([...tasks, { id: nextId++, text: value, done: false }]);
setText('');
setToast('Added "' + value + '"');
}
const toggle = (id) => setTasks(tasks.map((t) => t.id === id ? { ...t, done: !t.done } : t));
const remove = (id) => setTasks(tasks.filter((t) => t.id !== id));
const left = tasks.filter((t) => !t.done).length;
return h(ThemeProvider, { theme },
h(CssBaseline),
h(Paper, { elevation: 2, sx: { m: 2, p: 2, maxWidth: 480 } },
h(Stack, { direction: 'row', sx: { alignItems: 'center', justifyContent: 'space-between', mb: 1.5 } },
h(Typography, { variant: 'h6' }, 'Tasks'),
h(Chip, { id: 'left', size: 'small', color: 'primary', label: left + ' open' })
),
h('form', { id: 'add-form', onSubmit: add },
h(Stack, { direction: 'row', spacing: 1 },
h(TextField, {
id: 'task', label: 'New task', size: 'small', fullWidth: true,
value: text, onChange: (e) => setText(e.target.value),
}),
h(Button, { type: 'submit', variant: 'contained', startIcon: h(AddIcon) }, 'Add')
)
),
h(List, { id: 'list', dense: true },
tasks.map((t) => h(ListItem, {
key: t.id,
secondaryAction: h(IconButton, { edge: 'end', 'aria-label': 'Delete', onClick: () => remove(t.id) }, h(DeleteIcon)),
},
h(ListItemIcon, null, h(Checkbox, { edge: 'start', checked: t.done, onChange: () => toggle(t.id) })),
h(ListItemText, {
primary: t.text,
sx: { textDecoration: t.done ? 'line-through' : 'none', color: t.done ? 'text.disabled' : 'text.primary' },
})
))
)
),
h(Snackbar, {
open: !!toast, message: toast, autoHideDuration: 2500, onClose: () => setToast(''),
anchorOrigin: { vertical: 'bottom', horizontal: 'center' },
})
);
}
createRoot(document.getElementById('root')).render(h(App));
</script>
</body>
</html>
- 형식:
onSubmit는e.preventDefault()를 호출하고 상태에 작업을 추가합니다. 대신 서버로 보내려면 HTML 양식 제출을 참조하세요. - 아이콘: 문서는
@mui/icons-material에서 각 아이콘을 자체 구성요소로 내보냅니다.@mui/icons-material/Delete와 같이 사용하는 항목만 매핑하세요. - 상태:
React.useState는 목록을 보유하고 있으며 변경 시 Material UI가 다시 그려집니다.
npm으로 전환해야 하는 시기
Material UI 문서는 제작에 CDN 접근 방식을 권장하지 않습니다. 브라우저는 사용하는 구성 요소에 관계없이 전체 라이브러리를 다운로드합니다. 제품의 경우 패키지를 설치하고 다음을 빌드합니다.
npm install @mui/material @emotion/react @emotion/styled
그러면 코드는 CDN URL 대신 설치된 패키지를 가져옵니다. 빌드된 프런트 엔드 호스팅에서는 구축된 앱을 온라인에 올리는 방법을 다룹니다.
작동하지 않을 때
| 당신이 보는 것 | 원인 | 수정 |
|---|---|---|
빈 페이지, 콘솔의 useContext 오류 |
React 사본 2개 | 가져오기 맵에 React 리맵 라인 추가 |
Unexpected token '<' |
일반 모듈 스크립트의 JSX | h(...) 또는 Babel 독립형 사용 |
Failed to resolve module specifier |
가져오기 맵에서 이름이 누락되었거나 맵이 스크립트 뒤에 옵니다. | 이름을 추가하고 스크립트 위에 지도를 유지하세요. |
| 회색 브라우저 버튼, 스타일 없음 | HTML 태그로 작성된 구성 요소 | 모듈 스크립트에서 렌더링 |
| 잘못된 글꼴 | Roboto 링크 없음 | 헤드에 Google Fonts 링크 추가 |
| 새 릴리스 이후 페이지 변경 | 버전이 URL에 고정되지 않았습니다. | 모든 URL에 정확한 버전을 입력하세요. |
기타 스크립트 문제의 경우 HTML JavaScript가 작동하지 않는 이유에서 일반적인 검사를 거칩니다.
링크로 공유하세요
머티리얼 UI 페이지는 설명하는 것보다 보여주는 것이 더 쉽습니다. 스크린샷을 클릭할 수 없으며 .html 첨부 파일이 일반 코드로 열리거나 전화기에서 전혀 열리지 않을 수 있습니다.
작업 버전을 보내려면 페이지를 NOS 문서에 붙여넣고 공유 링크 만들기를 선택하세요. 링크할 HTML이 이를 안내합니다.
페이지는 작성된 대로 렌더링되고, 해당 스크립트가 실행되고, cdn.jsdelivr.net의 모듈이 로드되므로 페이지를 받는 사람은 실제 구성 요소를 직접 클릭할 수 있습니다. 나중에 코드를 변경하면 동일한 링크에 새 버전이 표시됩니다.