본문으로 건너뛰기
5분 AI.
REPO DEEP DIVE · 2026-07-29 · MANTINEDEV/MANTINE · Emotion 없이 CSS 변수로 테마를 짜는 리액트 UI 라이브러리

mantinedev/mantine 딥다이브
— "CSS-in-JS를 버린" 100개+ 컴포넌트 리액트 UI 킷

UI 라이브러리

빠른 판단

무엇
CSS 변수 테마와 UI 컴포넌트·훅·폼·날짜·알림 등을 제공하는 React UI 라이브러리
이럴 때
폼과 대시보드 같은 앱 화면을 미리 구현된 부품으로 조립하고 다크모드·테마를 함께 구성할 때
Mantine은 React 애플리케이션을 만들 때 필요한 거의 모든 UI 조각 — 버튼, 폼, 모달, 날짜 선택기, 차트, 알림 — 을 한 번에 제공하는 컴포넌트 라이브러리이자 프레임워크급 도구 모음이다. 국내외에서 MUI·Chakra·Ant Design과 함께 가장 많이 거론되는 리액트 UI 킷 중 하나로 자리잡았다.

가장 큰 특징은 런타임에 스타일시트를 새로 만들어 끼워 넣는 "CSS-in-JS"(Emotion) 방식을 완전히 버리고, 빌드 시점에 고정되는 CSS Modules + 런타임엔 오직 CSS 커스텀 프로퍼티(변수)만 갈아끼우는 방식으로 갈아탔다는 점이다. 색을 바꾸거나 다크모드를 켜도 새 스타일시트를 만들지 않고, 이미 있는 --button-bg 같은 변수 값만 인라인 스타일로 바꿔 끼운다.

(저장소 mantinedev/mantine · 제작 Vitaly Rtishchev · 라이선스 MIT · 버전 9.5.0(전 패키지 동일 버전, 이른바 lockstep) · React 19.2+ 전용 · Yarn 4 + 커스텀 스크립트 기반 모노레포, packages/@mantine/* 아래 15개 패키지 · @mantine/core 컴포넌트 116개 · @mantine/hooks 훅 81개 · 최신 커밋 2026-07-28)
목차
  1. 한 줄 정체
  2. 왜 지금 뜨는가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계 · 동작 원리
  6. 기술 스택 · 의존성
  7. 설치 · 요구사항
  8. 실제 사용 흐름
  9. 함정 · 오해 방지
  10. 대안 비교
  11. 정리 · 누가 쓰면 좋은가
  12. 직접 해볼 수 있는 실습 과제
  13. 관련 기술 심화 학습 로드맵
  14. 핵심 키워드 사전

1한 줄 정체

한 문장으로: "리액트로 화면을 짤 때 필요한 부품 116종 + 훅 81종을, 다크모드·테마·접근성까지 갖춰 미리 만들어 둔 상자"

Mantine은 리액트 개발자를 위한 "완제품 UI 부품 상자"다. 버튼·인풋·모달·테이블처럼 화면 어디에나 들어가는 기본 부품(@mantine/core)부터, 폼 상태 관리(@mantine/form), 날짜 선택기(@mantine/dates), 알림 토스트(@mantine/notifications), 차트(@mantine/charts), Ctrl+K 커맨드 팔레트(@mantine/spotlight)까지 — 실무에서 필요한 UI를 웬만하면 다 갖추고 있다.

더 중요한 건 스타일을 만드는 방식이다. 예전 버전(v6 이하)은 Emotion이라는 CSS-in-JS 라이브러리로 컴포넌트가 그려질 때마다 자바스크립트가 CSS 문자열을 계산해 <style> 태그로 밀어 넣었다. 지금(v7 이후, 현재 9.x)은 이 방식을 완전히 버리고, 색상·간격 같은 값은 빌드 시점에 고정된 .module.css 파일에 넣고, 인스턴스마다 달라지는 값(버튼 색상 등)만 CSS 변수를 담은 인라인 style로 갈아끼운다.

한 줄 비유

"매번 새 옷을 재봉하는 것"에서 "이미 만든 옷에 다이얼만 돌리는 것"으로

Emotion 방식은 버튼 색을 바꿀 때마다 재봉틀을 돌려 새 옷(스타일시트)을 한 벌씩 다시 만드는 것과 비슷했다. 화면이 복잡해질수록 재봉 작업(런타임 CSS 계산)이 쌓여 느려진다.

지금의 Mantine은 옷(CSS 파일)을 미리 딱 한 벌만 만들어 두고, 색상 다이얼(CSS 변수)만 손으로 돌려 끼운다. 옷 자체는 다시 만들지 않으니 훨씬 가볍고 예측 가능하다.

2왜 지금 뜨는가

CSS-in-JS 탈출 러시 · "배터리 포함" 생산성 · shadcn 대비 완제품 지향

2020년대 중반 리액트 생태계에서 벌어진 가장 큰 지각변동 중 하나가 "CSS-in-JS 이탈"이다. Emotion·styled-components처럼 런타임에 스타일을 계산하는 방식은 서버 컴포넌트(RSC)와 궁합이 나쁘고 번들·런타임 비용이 있다는 지적이 쌓였다. Mantine은 이 흐름을 가장 적극적으로, 그것도 대규모 컴포넌트 라이브러리 스케일에서 실제로 완주한 몇 안 되는 사례다.

① "Emotion 없이도 테마 가능"을 증명한 레퍼런스

많은 라이브러리가 "CSS-in-JS를 빼자"고 말은 하지만, Mantine처럼 Styles API(classNames/styles/vars/unstyled)를 그대로 유지하면서 내부 구현만 CSS Modules + CSS 변수로 완전히 갈아치운 사례는 드물다. "이렇게 하면 된다"는 실전 설계도로 자주 인용된다.

② 배터리 포함(batteries-included) 생산성

shadcn/ui류가 "복사해서 내 코드로 만드는" 방식이라면, Mantine은 npm install 한 번으로 폼 검증·날짜·차트·알림·커맨드 팔레트까지 끝난다. 스타트업이나 사이드 프로젝트에서 "UI 결정 피로"를 줄이고 싶을 때 여전히 강한 선택지다.

③ AI 코딩 시대에 맞춘 도구 확장 — @mantine/mcp-server

저장소 안에 AI 에이전트(Claude, Cursor 등)가 Mantine 문서를 직접 조회할 수 있는 MCP(Model Context Protocol) 서버 패키지가 정식으로 포함돼 있다. "AI가 UI 라이브러리를 얼마나 잘 아느냐"가 채택 기준이 되는 시대에, 공식 MCP 서버를 자체 제공하는 것은 눈에 띄는 선제 대응이다.

경쟁 지형에서의 위치
MUI·Chakra·shadcn 사이의 "중간 지대"

Google Material 느낌의 MUI, 헤드리스+접근성 중심의 Chakra/Radix, 복사-붙여넣기형 shadcn/ui 사이에서 Mantine은 "디자인도 갖췄고, 완제품이고, 그러면서 커스터마이징도 CSS 변수 하나로 된다"는 절충점을 판다. 자세한 비교는 10장 참고.

3무엇을 해결하나

"매번 처음부터 만드는 UI"의 반복 노동과 "CSS-in-JS 런타임 비용" 둘 다 해결

Mantine이 실제로 없애주는 고통은 두 갈래다.

① "버튼부터 다시 만들기" 문제

새 프로젝트를 시작할 때마다 버튼·모달·드롭다운·폼 검증을 처음부터 짜는 건 시간 낭비다. Mantine은 116개 컴포넌트와 81개 유틸리티 훅(use-disclosure, use-debounced-value, use-hotkeys 등)으로 이 반복을 없앤다. 접근성(ARIA, 키보드 내비게이션)과 다크모드도 기본 내장이라 따로 신경 쓸 게 적다.

② "런타임 스타일 계산" 문제

Emotion 같은 CSS-in-JS는 컴포넌트가 리렌더될 때마다 스타일 객체를 해시하고 <style> 태그에 새로 주입하는 계산 비용이 든다. 서버 컴포넌트 환경에서는 이 계산이 애초에 서버에서 도는 게 애매해지는 문제도 있다. Mantine은 스타일 결정을 대부분 빌드 시점으로 옮기고, 런타임에는 "이미 있는 CSS 변수 값만 바꿔치기"하는 값싼 연산만 남겼다.

용어
CSS-in-JS
자바스크립트 코드 안에서 CSS를 작성하고, 실행 중(런타임)에 그 CSS를 실제 스타일시트로 변환해 페이지에 주입하는 기법. Emotion, styled-components가 대표적. 유연하지만 런타임 계산 비용이 든다.
용어
CSS Modules
Button.module.css처럼 파일마다 클래스 이름을 자동으로 고유하게 만들어주는 CSS 작성 방식. 빌드 시점에 완전히 확정되며, 런타임 계산이 없다.
용어
CSS 커스텀 프로퍼티 (CSS 변수)
--button-bg: blue;처럼 이름 붙은 값을 CSS 안에 저장해두고 var(--button-bg)로 재사용하는 표준 CSS 기능. 자바스크립트 없이도 인라인 style로 값만 바꿔 테마를 바꿀 수 있다.

4전체 구조

Yarn 워크스페이스 모노레포 · packages/@mantine/* 15개 · 전 패키지 버전 락스텝

저장소는 Yarn 4 워크스페이스 기반 모노레포(package.json의 workspaces: ["packages/**/*", "apps/*"])이며, 실제 배포 패키지는 packages/@mantine/ 아래 15개, 테스트 전용 유틸은 packages/@mantine-tests/, 문서 사이트는 apps/mantine.dev·apps/help.mantine.dev에 있다.

┌────────────────────────────────────────────────────────────────┐ │ MANTINE 모노레포 전체 지도 │ └────────────────────────────────────────────────────────────────┘ packages/@mantine/ ├── hooks/ # 81개 훅 — core가 의존하는 최하위 기반 ├── core/ # ★ 116개 컴포넌트, Styles API, 테마 엔진 │ └── src/core/ # factory · styles-api · MantineProvider ├── form/ # useForm — 폼 상태·검증(zod/yup/joi 붙임) ├── dates/ # 달력·날짜입력 (dayjs는 사용자가 설치) ├── notifications/ # 토스트 알림 시스템 ├── charts/ # recharts 위에 얹은 차트 프리셋 ├── spotlight/ # Ctrl+K 커맨드 팔레트 ├── modals/ # 중앙집중형 모달 매니저 ├── code-highlight/ # highlight.js 코드 하이라이팅 ├── tiptap/ # Tiptap 리치 텍스트 에디터 바인딩 ├── dropzone/ # 드래그앤드롭 파일 업로드 ├── carousel/ # 캐러셀 컴포넌트 ├── nprogress/ # 상단 진행바 ├── schedule/ # 일정표 컴포넌트 ├── emotion/ # ⚠ v6 마이그레이션용 Emotion 호환 레이어(옵션) └── mcp-server/ # AI 에이전트용 문서 조회 MCP 서버 packages/@mantine-tests/ # core·dates 등의 공유 테스트 유틸(비공개) apps/mantine.dev # 공식 문서 사이트(Next.js) apps/help.mantine.dev # 커뮤니티 Q&A 사이트 scripts/build # rolldown 기반 자체 빌드 파이프라인 scripts/release # 전 패키지 버전을 한 번에 맞추는 락스텝 릴리스

의존 방향은 단방향이다. @mantine/hooks는 아무것도 의존하지 않는 최하위 레이어, @mantine/core는 hooks에만 의존, 그 위의 form·dates·notifications·charts·spotlight·modals 같은 "확장 패키지"는 모두 core와 hooks를 peerDependency로 요구한다. 즉 @mantine/core가 사실상 프레임워크의 심장이고, 나머지는 그 위에 얹힌 플러그인이다.

구조상 주의
버전은 "락스텝(lockstep)" — 패키지마다 따로 올라가지 않는다

Mantine은 lerna나 changesets 같은 독립 버전 관리 도구를 쓰지 않는다. scripts/release가 루트 package.json의 버전을 한 번 올린 뒤, 모든 @mantine/* 패키지의 version과 서로 간의 peerDependencies 버전을 전부 그 숫자로 덮어쓴다(scripts/release/set-mantine-packages-version.ts). 그래서 @mantine/core와 @mantine/dates는 항상 완전히 같은 버전 번호(예: 둘 다 9.5.0)로 나온다. "버전의 진실 원천은 루트 package.json 하나"라는 뜻이다.

5핵심 설계 · 동작 원리

factory + useProps + useStyles = Styles API의 삼각대 · 정적 전역 변수 vs 동적 인스턴스 변수

이 장이 Mantine 이해의 핵심이다. "classNames·styles·unstyled로 커스터마이징 가능한 컴포넌트를, Emotion 없이 어떻게 만드는가"를 실제 소스로 뜯어본다.

① 모든 컴포넌트를 찍어내는 틀 — factory()

packages/@mantine/core/src/core/factory/factory.tsx의 factory() 함수는 평범한 함수형 컴포넌트를 받아 .extend()와 .withProps()라는 두 정적 메서드를 붙여 돌려준다. .extend()는 테마에서 이 컴포넌트의 기본값을 재정의할 때, .withProps()는 특정 props가 고정된 파생 컴포넌트를 만들 때 쓴다. Mantine의 모든 컴포넌트(Button, TextInput …)는 예외 없이 이 factory 또는 다형성 버전인 polymorphicFactory로 만들어진다.

// factory.tsx (실제 소스 요약)
export function factory<Payload>(ui) {
  const Component = ui;
  Component.extend = identity;         // 테마에서 defaultProps/classNames 재정의
  Component.withProps = (fixedProps) => {
    const Extended = (props) => <Component {...fixedProps} {...props} />;
    return Extended;
  };
  return Component;
}

② props의 3단 우선순위 — useProps()

컴포넌트 내부에서는 가장 먼저 useProps('Button', null, _props)를 호출한다. 이 훅(core/MantineProvider/use-props/use-props.ts)은 "실제로 넘어온 props" > "MantineProvider 테마의 theme.components.Button.defaultProps" > "컴포넌트 자체 기본값" 순서로 병합한다. 그래서 <MantineProvider theme={{ components: { Button: Button.extend({ defaultProps: { radius: 'xl' } }) } }}>처럼 앱 전체의 모든 버튼 기본 반경을 한 곳에서 바꿀 수 있다.

③ 클래스와 인라인 스타일을 동시에 계산 — useStyles()

Button 컴포넌트 내부의 실제 코드다: const getStyles = useStyles({ name: 'Button', props, classes, classNames, styles, unstyled, vars, varsResolver }). 이 getStyles('root', {...}) 호출이 리턴하는 { className, style }를 그대로 DOM에 스프레드한다 — <UnstyledButton {...getStyles('root', ...)}>. useStyles(core/styles-api/use-styles/use-styles.ts) 내부는 4겹을 합성한다.

  1. 정적 클래스 — 빌드된 Button.module.css의 .root 클래스 (항상 붙는다)
  2. 테마 단위 classNames/styles — theme.components.Button.classNames
  3. 인스턴스 단위 classNames/styles prop — 이 버튼 하나에만 준 값
  4. 동적 CSS 변수 — varsResolver(theme, props, ctx)가 계산한 { '--button-bg': ... }를 인라인 style로 병합

마지막 4번이 핵심이다. Button의 varsResolver는 이렇게 생겼다(실제 소스, Button.tsx):

const varsResolver = createVarsResolver<ButtonFactory>(
  (theme, { radius, color, gradient, variant, size }) => {
    const colors = theme.variantColorResolver({ color, theme, gradient, variant });
    return {
      root: {
        '--button-height': getSize(size, 'button-height'),
        '--button-bg': colors.background,
        '--button-color': colors.color,
        // ...
      },
    };
  }
);

이 결과가 DOM에 인라인 style="--button-bg: #1c5fd6; --button-height: 42px"로 찍히고, Button.module.css는 background: var(--button-bg, var(--mantine-primary-color-filled))처럼 그 변수를 참조만 한다. 즉 CSS는 빌드 때 이미 완성돼 있고, React는 매 렌더마다 "변수 값 몇 개"만 계산해 인라인으로 얹는다 — Emotion처럼 스타일시트 자체를 새로 만들지 않는다.

비유

배전반(CSS 변수)과 배선도(CSS Modules)로 생각하면 쉽다. 전기 배선(어떤 속성이 어떤 변수를 쓰는지)은 건물을 지을 때(빌드 시점) 이미 다 깔아놨다. 리모컨(React 렌더)이 하는 일은 배전반의 다이얼(CSS 변수 값) 몇 개를 돌리는 것뿐이다. 배선을 다시 까는(스타일시트 재생성) 공사는 일어나지 않는다.

④ 전역 값은 "빌드 타임에 굳힌다" — default-css-variables.css

버튼 배경색처럼 인스턴스마다 달라지는 값은 위 방식으로 런타임에 넣지만, --mantine-spacing-md·--mantine-color-blue-6처럼 테마 전체에서 한 벌만 있으면 되는 값은 아예 빌드 스크립트(scripts/codegen/generate-default-css-variables.ts)가 DEFAULT_THEME을 읽어 정적 CSS 파일(default-css-variables.css, 527줄)로 미리 생성해 둔다. 앱이 <MantineProvider>로 테마를 바꾸면 이 파일 대신 MantineCssVariables 컴포넌트가 루트 <style> 하나만 다시 계산해 갈아끼운다 — 컴포넌트마다 스타일시트를 다시 만드는 게 아니라 "이 앱 전체에 딱 하나 있는 변수 사전"만 바꾸는 것이다.

⑤ 다크모드 — 깜빡임 없는 ColorSchemeScript

다크모드는 새 스타일시트가 아니라 <html data-mantine-color-scheme="dark"> 속성 하나로 전환된다. 모든 색상 변수가 [data-mantine-color-scheme="dark"] 셀렉터 아래 다시 정의돼 있을 뿐이다. 문제는 서버 렌더링(SSR) 시 "깜빡임(FOUC)"인데, Mantine은 <head>에 심는 <ColorSchemeScript>가 브라우저가 리액트를 실행하기도 전에 localStorage를 읽어 이 속성을 즉시 세팅하는 인라인 <script>를 실제로 렌더한다:

// ColorSchemeScript.tsx가 만드는 스크립트(실제 소스)
try {
  var _colorScheme = window.localStorage.getItem("mantine-color-scheme-value");
  var colorScheme = (_colorScheme === "light" || _colorScheme === "dark" || _colorScheme === "auto")
    ? _colorScheme : "light";
  var computed = colorScheme !== "auto" ? colorScheme
    : (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
  document.documentElement.setAttribute("data-mantine-color-scheme", computed);
} catch (e) {}

⑥ unstyled와 headless — "스타일을 아예 끄는" 탈출구

각 컴포넌트 unstyled prop이나 MantineProvider의 headless 모드를 켜면 useStyles가 Mantine의 기본 클래스·변수 주입을 전부 건너뛴다. Tailwind나 다른 디자인 시스템 위에 Mantine의 "동작(behavior)"만 얹고 싶은 팀을 위한 탈출구다.

용어
Styles API
Mantine 컴포넌트를 커스터마이징하는 4종 세트 — classNames(내부 요소별 클래스 추가), styles(내부 요소별 인라인 스타일), vars(CSS 변수 재정의), unstyled(기본 스타일 끄기). 모든 컴포넌트가 동일한 규약을 따른다.
용어
varsResolver
현재 테마와 props를 받아 "이 컴포넌트 인스턴스에 필요한 CSS 변수 값들"을 계산해 반환하는 함수. 결과는 인라인 style로 DOM에 붙는다. Mantine 스타일 시스템의 실질적인 심장부다.

6기술 스택 · 의존성

React 19.2+ 전용 · @mantine/core 의존성은 5개뿐 · 빌드는 rolldown · 테스트는 Jest
영역실제 사용 기술
프레임워크React 19.2+만 지원(peerDependency ^19.2.0). React 18 지원은 최신 버전 기준 이미 끊겼다
@mantine/core 의존성@floating-ui/react(팝오버·툴팁 위치), clsx(클래스 합성), react-number-format, react-remove-scroll, type-fest — 이 5개뿐. Emotion·styled-components 등 CSS-in-JS 라이브러리는 전혀 없다
스타일 빌드rolldown(Rust 기반 Rollup 후계) + rollup-plugin-postcss(CSS Modules 추출) + hash-css-selector(클래스명 해싱)
모노레포 도구Yarn 4 워크스페이스. lerna·changesets 없이 자체 scripts/release·scripts/build로 직접 구현
테스트Jest 30 + jest-environment-jsdom + jest-axe(접근성 자동 검사) + Testing Library. 소스 전반에 645개 이상의 *.test.ts(x) 파일
문서·스토리북Storybook(포트 2356, 컴포넌트별 *.story.tsx) + Next.js 기반 apps/mantine.dev 문서 사이트(문서 코드 자동 생성 스크립트 docs:docgen)
CIGitHub Actions(npm_test.yml) — PR마다 yarn install → docgen → build all → npm test(포맷·syncpack·typecheck·lint·jest 전부 포함)
확장 패키지 의존성@mantine/dates는 dayjs를 peerDependency로만 요구(직접 번들 안 함) · @mantine/charts는 recharts >=3.2.1 위에 얹힌 래퍼 · @mantine/tiptap은 Tiptap 에디터 바인딩
"의존성이 적다"는 것의 의미

UI 라이브러리치고 @mantine/core 자체 의존성이 5개뿐이라는 건 이례적으로 가볍다. MUI가 Emotion·Popper 등 스타일링 런타임을 필수로 끌고 오는 것과 대비된다. 대신 그 대가로 CSS Modules를 지원하는 번들러(Vite, Next.js, webpack 등 대부분 기본 지원)가 필요하다.

7설치 · 요구사항

npm install 한 줄 + PostCSS 프리셋 등록이 사실상 전부
항목내용
최소 설치npm install @mantine/core @mantine/hooks — 대부분의 기능이 이 둘로 시작된다
번들러CSS Modules를 지원하는 도구(Vite, Next.js, CRA, webpack 등 사실상 전부 기본 지원). Mantine 전용 postcss-preset-mantine을 postcss.config.cjs에 등록해야 light-dark()·rem() 같은 Mantine mixin이 동작
필수 초기화루트에 import '@mantine/core/styles.css' 한 번 + 앱을 <MantineProvider>로 감싸기. SSR에서 깜빡임을 없애려면 <head>에 <ColorSchemeScript /> 추가
Node/React 버전모노레포 개발 기준 Node ≥20.9 (.nvmrc), 사용 앱 기준 React 19.2+. React 17/18 프로젝트는 구버전 Mantine(v7)에 머물러야 함
확장 패키지필요한 것만 추가 설치 — 예: npm install @mantine/form, npm install @mantine/dates dayjs(dayjs는 직접 설치해야 함), npm install @mantine/notifications
템플릿공식 CLI로 Vite/Next.js 템플릿을 바로 생성 가능(npx create-mantine-app 명령 계열 형태로 문서에 안내됨)
"npm install만 하면 끝"이 아니다

CSS 파일 import(@mantine/core/styles.css)를 빼먹거나 PostCSS 프리셋을 등록하지 않으면 컴포넌트는 렌더되는데 스타일이 하나도 안 먹는 초보자 함정에 자주 빠진다. Emotion 시절에는 스타일이 JS 안에서 자동 주입됐지만, 지금은 CSS 파일을 명시적으로 불러와야 한다는 점이 v6→v7 이관자들이 가장 자주 놓치는 부분이다.

8실제 사용 흐름

Provider로 감싸기 → 컴포넌트 쓰기 → 필요하면 classNames/vars로 파고들기
STEP 1

앱 전체를 MantineProvider로 감싼다

import { MantineProvider, ColorSchemeScript } from '@mantine/core';
import '@mantine/core/styles.css';

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <head><ColorSchemeScript /></head>
      <body>
        <MantineProvider theme={{ primaryColor: 'blue' }}>
          {children}
        </MantineProvider>
      </body>
    </html>
  );
}
STEP 2

컴포넌트를 그대로 쓴다 — 기본값만으로 완성도가 높다

import { Button, TextInput, Group } from '@mantine/core';

function LoginForm() {
  return (
    <Group>
      <TextInput label="이메일" placeholder="you@example.com" />
      <Button color="blue" radius="md">로그인</Button>
    </Group>
  );
}
STEP 3

폼·날짜·알림 등 확장 패키지를 필요할 때만 얹는다

import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';

const form = useForm({
  initialValues: { email: '' },
  validate: { email: (v) => (/^\S+@\S+$/.test(v) ? null : '이메일 형식 오류') },
});

function onSubmit(values) {
  notifications.show({ message: '저장 완료', color: 'green' });
}
STEP 4

필요할 때만 Styles API로 파고든다

// 인스턴스 하나만 커스터마이징
<Button
  classNames={{ root: styles.myButton }}
  styles={{ root: { textTransform: 'uppercase' } }}
>확인</Button>

// 앱 전체 버튼 기본값을 한 곳에서 바꾸기
<MantineProvider theme={{
  components: {
    Button: Button.extend({ defaultProps: { radius: 'xl' } }),
  },
}}>

대부분의 실무는 STEP 1~3에서 끝난다. STEP 4는 "디자인 시스템을 세밀하게 통일하고 싶을 때"만 필요하다.

9함정 · 오해 방지

"아직도 Emotion 쓴다"는 낡은 정보 · 빌드 도구 이름 착각 · React 버전 함정
함정 ① "Mantine은 Emotion 기반이다" — 옛날 정보

v6 이하 시절 자료(블로그·강의)를 보고 "Mantine = sx prop + createStyles + Emotion"이라고 기억하는 사람이 여전히 많다. v7부터는 core가 CSS Modules + CSS 변수로 완전히 갈아탔고, Emotion은 @mantine/emotion이라는 완전히 별도의 옵션 패키지로만 남아 있다(실제로 core의 dependencies엔 Emotion이 전혀 없다). "Emotion 쓰려면 이 패키지를 따로 깔아야" 하는 처지가 됐다는 걸 모르면 오래된 튜토리얼 코드가 그대로 작동하지 않아 당황한다.

함정 ② 빌드 도구 이름 — "Rollup"이라 쓰여 있지만 실제로는 rolldown

빌드 스크립트 폴더 이름이 scripts/build/rollup/이라 Rollup을 쓴다고 오해하기 쉽지만, 실제 설정 파일(create-package-config.ts)은 RolldownOptions 타입을 쓰고 로그도 "Compiling ... with rolldown..."이라 찍는다. 즉 실제 번들러는 Rollup이 아니라 Rust로 다시 짠 후계자 rolldown이다. 폴더 이름만 보고 문서를 짐작하면 틀린다.

함정 ③ React 18은 이제 지원 밖 — 다운그레이드가 답이 아니다

현재 @mantine/core의 peerDependency는 react: ^19.2.0이다. React 18 프로젝트에 최신 Mantine을 설치하면 타입 오류나 런타임 경고가 날 수 있다. "최신 버전을 깔았는데 안 된다"면 프로젝트의 React 버전부터 확인해야 한다. 반대로 React 18에 머물러야 하는 프로젝트는 Mantine의 v7 계열을 의도적으로 유지하는 게 맞다.

함정 ④ 패키지 버전이 다 같다고 "묶여서 나온다"는 뜻은 아니다

모든 @mantine/*가 같은 버전 번호를 갖는 건 락스텝 릴리스 정책 때문이지, 코드가 한 덩어리로 묶여 있다는 뜻이 아니다. 각 패키지는 여전히 독립적으로 npm install해야 하고, @mantine/dates 하나만 깔았다고 @mantine/charts가 딸려오지 않는다.

참고 — README의 패키지 목록이 실제보다 짧다

공식 README.md의 "Packages" 목록에는 @mantine/dates·@mantine/emotion·@mantine/mcp-server가 빠져 있다(실제 저장소엔 15개 패키지가 있음). 문서 사이트나 packages/@mantine/ 디렉터리를 직접 보는 편이 항상 더 정확하다.

10대안 비교

MUI · Chakra UI · shadcn/ui · Ant Design 대비 Mantine의 위치
라이브러리스타일링 방식Mantine과의 차이
MUI (Material UI)Emotion 기반 CSS-in-JS(v5/v6 기준), Material Design 강제MUI는 여전히 런타임 CSS-in-JS 색이 짙고 Material 디자인 언어에 갇히기 쉽다. Mantine은 디자인 강제가 약하고, 스타일 계층이 빌드 타임에 고정돼 더 가볍다
Chakra UIv3부터 Panda CSS(정적 CSS 추출)로 전환, 스타일 프롭 중심철학적으로 가장 비슷한 방향(런타임 CSS-in-JS 탈출)이지만, Chakra는 유틸리티 스타일 프롭 중심이고 Mantine은 완성된 컴포넌트 + Styles API 중심 — "레고 블록"과 "완제품 가구"의 차이에 가깝다
shadcn/uiTailwind CSS + 컴포넌트 코드를 프로젝트에 직접 복사shadcn은 라이브러리가 아니라 코드 생성기에 가깝다 — 복사한 코드는 내 것이 되고 자유롭게 고치지만, 업데이트도 내가 직접 병합해야 한다. Mantine은 npm install로 계속 업데이트를 받는 전통적 라이브러리다
Ant DesignCSS-in-JS(v5, @ant-design/cssinjs) + 엔터프라이즈 지향 디자인Ant Design은 복잡한 데이터 테이블·엔터프라이즈 백오피스에 강하다. Mantine은 범용 디자인에 가깝고 커스터마이징(테마 오버라이드)이 CSS 변수 수준으로 더 단순하다
비유로 정리

MUI는 "이미 정해진 브랜드 유니폼"(Material 디자인), Ant Design은 "회사 정장"(엔터프라이즈 지향), shadcn/ui는 "내가 직접 마름질하는 맞춤 원단"(코드 복사), Chakra는 "부품 상자에서 골라 조립"(유틸리티 프롭)에 가깝다. Mantine은 "이미 만들어졌지만 다이얼(CSS 변수) 몇 개로 색과 크기를 바로 바꿀 수 있는 기성복"에 가장 가깝다.

11정리 · 누가 쓰면 좋은가

"완제품 UI + CSS 변수 테마"가 필요한 팀에게 여전히 강력한 선택

Mantine은 "CSS-in-JS 없이도 강력하게 커스터마이징 가능한 완제품 컴포넌트 라이브러리"를 대규모로 증명한 프로젝트다. factory로 컴포넌트를 찍어내고, useProps로 테마 기본값을 주입하고, useStyles가 정적 CSS Modules와 동적 CSS 변수를 인라인 스타일로 합성하는 삼각 구조가 116개 컴포넌트 전체에 일관되게 적용돼 있다.

이런 사람 · 팀에게 잘 맞는다

· 리액트로 관리자 페이지·SaaS 대시보드를 빠르게 완성해야 하는 1~3인 스타트업 팀
· 디자인 리소스 없이도 "기본으로도 봐줄 만한" UI가 필요한 사이드 프로젝트
· CSS-in-JS의 런타임 비용·RSC 호환성 문제를 피하면서도 테마 커스터마이징의 유연함은 포기하고 싶지 않은 팀
· 폼·날짜·차트·알림까지 한 생태계 안에서 해결하고 싶은 경우

다시 생각해볼 상황

· 이미 Tailwind + shadcn/ui 워크플로에 익숙하고 컴포넌트를 코드 레벨로 완전히 소유하고 싶은 팀
· React 18 이하를 벗어날 수 없는 레거시 프로젝트(최신 Mantine 사용 불가)
· Material Design 가이드라인을 그대로 따라야 하는 조직(MUI가 더 적합)

코드를 배우는 관점에서는, "CSS-in-JS 없이 다크모드·테마·컴포넌트 커스터마이징을 어떻게 설계하는가"를 실제 프로덕션 규모로 보여주는 교재로서 가치가 크다. packages/@mantine/core/src/core 폴더 하나만 정독해도 현대 리액트 UI 라이브러리의 스타일 아키텍처를 상당 부분 이해할 수 있다.

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

버튼의 기본값부터 폼 상태와 CSS 변수 우선순위까지, 작은 실험으로 내부 설계를 확인한다

아래 실습은 의존성이 이미 준비된 개인 학습용 사본에서 진행한다. 경로와 명령의 기준은 Mantine 저장소 루트다. 실행 환경은 해당 사본의 .nvmrc와 루트 package.json에서 확인한다. Storybook의 예제 선택 방식은 .storybook/main.ts, 공통 테마 설정은 .storybook/preview.tsx에 있다.

과제 1

버튼 두 개로 테마의 우선순위 확인하기 입문

목표: 앱 전체 기본값과 버튼 하나에 직접 준 속성값이 어떻게 함께 적용되는지 이해한다.

  1. packages/@mantine/core/src/components/Button/Button.story.tsx를 열고, 이미 가져온 MantineProvider와 Button을 이용해 아래 예제를 추가한다.
  2. 다음 명령으로 버튼 관련 예제만 띄운 뒤, 추가한 ThemePractice를 선택한다.
COMPONENT=Button npm run storybook
export function ThemePractice() {
  return (
    <MantineProvider
      theme={{
        components: {
          Button: Button.extend({ defaultProps: { radius: 'xl' } }),
        },
      }}
    >
      <Button>테마 기본값</Button>
      <Button radius="sm">직접 지정한 값</Button>
    </MantineProvider>
  );
}
  1. 브라우저 개발자 도구에서 두 버튼의 --button-radius와 최종 border-radius를 비교한다.
  2. packages/@mantine/core/src/core/MantineProvider/use-props/use-props.ts의 병합 순서와 packages/@mantine/core/src/components/Button/Button.module.css의 변수 참조를 따라간다.

완료 기준: 첫 버튼에는 테마의 xl, 둘째 버튼에는 직접 지정한 sm이 적용된다. “직접 준 값이 테마 기본값보다 우선한다”는 결과를 화면과 소스 양쪽에서 설명할 수 있다.

과제 2

새로고침해도 남는 학습 진도 만들기 초급

목표: React 상태와 브라우저 저장소를 연결하고, 값 변경과 삭제를 구분한다.

  1. packages/@mantine/hooks/src/use-local-storage/UseLocalStorage.story.tsx에 아래 예제를 추가한다. useLocalStorage는 이 파일에서 이미 가져오고 있다.
  2. 다음 명령으로 실행하고 ReadingProgress를 선택한다.
COMPONENT=UseLocalStorage npm run storybook
export function ReadingProgress() {
  const [count, setCount, removeCount] = useLocalStorage<number>({
    key: 'mantine-study-progress',
    defaultValue: 0,
  });

  return (
    <div>
      <p>완료한 과제: {count}</p>
      <button type="button" onClick={() => setCount((value) => value + 1)}>
        하나 완료
      </button>
      <button type="button" onClick={removeCount}>초기화</button>
    </div>
  );
}
  1. 값을 올리고 새로고침한다. 개발자 도구의 저장소 화면에서 mantine-study-progress 값도 확인한 뒤 초기화한다.
  2. packages/@mantine/hooks/src/use-local-storage/create-storage.ts에서 JSON.stringify, storage 이벤트, mantine-local-storage 이벤트가 쓰이는 위치를 찾는다. 다른 탭 사이의 동기화와 같은 탭 안의 동기화가 왜 다른 이벤트를 쓰는지 정리한다.
  3. 아래 명령으로 기존 훅 테스트를 확인하고, 읽은 테스트 중 값 저장·삭제에 해당하는 항목을 실험 결과와 짝짓는다.
npm run jest -- --runInBand packages/@mantine/hooks/src/use-local-storage/use-local-storage.test.ts

완료 기준: 새로고침 후 진도가 복원되고, 초기화하면 화면이 0으로 돌아간다. 훅이 돌려주는 현재 값·변경 함수·삭제 함수의 역할을 각각 설명할 수 있다.

과제 3

제어 모드와 비제어 모드의 폼 비교하기 중급

목표: 같은 입력 화면에서 value와 defaultValue, 최신 값 조회와 화면 갱신의 차이를 확인한다.

  1. packages/@docs/demos/src/demos/form/Form.demo.controlled.tsx와 packages/@docs/demos/src/demos/form/Form.demo.uncontrolled.tsx를 나란히 읽는다. mode와 비제어 예제의 key={form.key('name')}를 비교한다.
  2. 다음 명령으로 실행하고 Form 아래의 controlled·uncontrolled 데모를 번갈아 연다. 이름을 채우고, 잘못된 이메일과 올바른 이메일로 각각 제출해 본다. 제출 콜백은 예제의 setSubmittedValues를 그대로 사용해 화면에 값만 표시한다.
COMPONENT=Form npm run storybook
  1. 비제어 예제의 실제 Demo 함수 안에 아래 버튼을 추가한다. 파일 상단의 code 문자열은 표시용 예제이므로, 동작을 바꾸려면 실제 함수를 고쳐야 한다.
<Button type="button" onClick={() => form.setFieldValue('name', '연습생')}>
  이름 채우기
</Button>
<Button type="button" onClick={() => form.reset()}>초기화</Button>
  1. packages/@mantine/form/src/use-form.ts에서 getInputProps, key, onSubmit을 찾는다. 비제어 모드가 defaultValue를 넘기는 부분과, form.getValues()로 최신 값을 읽는 흐름을 확인한다.
  2. 다음 테스트에서 두 모드의 입력 속성과 값 변경을 어떻게 검증하는지 읽는다.
npm run jest -- --runInBand packages/@mantine/form/src/tests/use-form/getInputProps.test.ts

완료 기준: 잘못된 이메일은 검증 오류를 보이고, 올바른 입력은 제출 값에 반영된다. 비제어 모드에서도 이름 채우기와 초기화가 화면에 반영되며, form.values 표시가 입력할 때마다 갱신된다고 가정하면 안 되는 이유를 설명할 수 있다.

과제 4

CSS 변수의 덮어쓰기 규칙을 테스트로 고정하기 심화

목표: 기본 변수·테마 변수·인스턴스 변수가 충돌할 때의 결과를 작은 회귀 테스트로 검증한다.

  1. packages/@mantine/core/src/core/styles-api/use-styles/get-style/resolve-vars/resolve-vars.ts와 같은 폴더의 merge-vars.ts를 읽는다. 기본 varsResolver, 테마의 vars, 인스턴스의 vars가 합쳐지는 순서를 적는다.
  2. 같은 폴더의 resolve-vars.test.ts에 충돌 사례를 추가한다. themeName: ['Test']와 selector: 'root'를 사용하고, 기본 해석기는 --button-bg: red와 --button-height: 40px, 테마의 components.Test.vars는 배경을 green, 인스턴스의 vars는 배경을 blue로 반환하게 한다.
  3. 세 값이 모두 있을 때 배경은 blue이고 높이는 40px인지 검증한다. 인스턴스의 vars를 빼면 green, 테마 변수도 빼면 red인지 추가로 확인한다.
  4. 다음 명령으로 해당 파일만 실행한다. 이어서 packages/@mantine/core/src/core/styles-api/use-styles/use-styles.ts도 읽고, 실제 훅에서 같은 mergeVars 순서가 쓰이는지 비교한다.
npm run jest -- --runInBand packages/@mantine/core/src/core/styles-api/use-styles/get-style/resolve-vars/resolve-vars.test.ts

완료 기준: 배경의 세 단계 우선순위와 충돌하지 않는 높이 값의 보존을 모두 검증한다. 이는 CSS 변수 객체의 병합 테스트이며, 브라우저의 실제 색상·레이아웃 확인은 Storybook에서 별도로 해야 한다는 범위도 설명할 수 있다.

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

6주 동안 사용법에서 내부 구현으로 이동하고, 매주 설명 가능한 결과물을 하나씩 남긴다

한 번에 모든 컴포넌트를 읽기보다 버튼 하나의 속성값이 화면에 도착하는 경로를 먼저 따라간다. 이후 상태 관리·폼·초기 색상 설정으로 넓히면 같은 설계가 어디에서 반복되는지 보인다.

주차 · 배울 개념이 레포에서 읽을 파일·폴더남길 결과물
1주차
React 속성값과
TypeScript 다형성
packages/@mantine/core/src/components/Button/Button.tsx
packages/@mantine/core/src/core/factory/polymorphic-factory.tsx
.storybook/main.ts
과제 1의 버튼 예제. component="a"처럼 렌더링할 요소를 바꿀 때 필요한 속성과 기본 버튼의 차이를 설명한 메모.
2주차
CSS Modules와
테마의 우선순위
packages/@mantine/core/src/components/Button/Button.module.css
packages/@mantine/core/src/core/MantineProvider/use-props/use-props.ts
packages/@mantine/core/src/core/styles-api/use-styles/
postcss.config.cjs
버튼의 radius가 --button-radius와 border-radius로 이어지는 흐름도. 과제 4의 변수 충돌 테스트.
3주차
사용자 정의 훅과
브라우저 저장소
packages/@mantine/hooks/src/use-local-storage/
packages/@mantine/hooks/src/use-uncontrolled/use-uncontrolled.ts
과제 2의 진도 저장 예제. 같은 탭의 사용자 정의 이벤트와 다른 탭의 storage 이벤트를 구분한 그림.
4주차
폼의 상태 보관과
입력 검증
packages/@docs/demos/src/demos/form/
packages/@mantine/form/src/use-form.ts
packages/@mantine/form/src/tests/use-form/getInputProps.test.ts
과제 3의 비교 기록. getInputProps·form.key·getValues·onSubmit이 각각 필요한 이유를 적은 설명.
5주차
서버 렌더링과
초기 색상 설정
packages/@mantine/core/src/core/MantineProvider/ColorSchemeScript/ColorSchemeScript.tsx
packages/@mantine/core/src/core/MantineProvider/color-scheme-managers/local-storage-manager.ts
packages/@mantine/core/src/core/MantineProvider/MantineProvider.tsx
저장된 설정·기본값·시스템 색상 중 무엇을 고르는지 정리한 결정표. 초기 스크립트와 Provider의 설정이 맞아야 하는 이유를 설명한 메모.
6주차
자동 검증과
라이브러리 배포 구조
jest.config.ts
packages/@mantine/core/src/components/Button/Button.test.tsx
scripts/build/build-package.ts
scripts/build/rollup/create-package-config.ts
packages/@mantine/core/package.json
Jest의 소스 경로 연결과 CSS 대체 설정을 설명한 메모. 소스가 esm/·cjs/·lib/와 CSS 파일로 나뉘어 배포되는 지도.

매주 결과물에는 “어떤 값을 바꾸었는가 → 어느 함수가 처리했는가 → 무엇으로 확인했는가”를 남긴다. 화면 확인과 객체·속성 검증을 구분하면, 테스트가 통과했을 때 어디까지 믿을 수 있는지도 함께 배울 수 있다.

14핵심 키워드 사전

실습과 소스 읽기에 반복해서 등장하는 말들을 Mantine의 실제 역할과 연결한다
워크스페이스 · workspaces
여러 패키지를 한 저장소에서 함께 개발하도록 묶는 작업 단위다. 이 레포는 루트 package.json에서 packages/**/*와 apps/*를 묶어 라이브러리와 문서 앱을 관리한다.
사용 환경의 의존성 조건 · peerDependencies
라이브러리가 함께 사용할 패키지와 호환 버전을 사용자 환경에 알리는 약속이다. @mantine/core의 package.json에서는 React와 @mantine/hooks 등의 조건을 확인할 수 있다.
컴포넌트에 넘기는 속성값 · props
같은 부품의 색·크기·동작을 고르는 입력값으로, 기기의 조절 손잡이에 가깝다. Mantine의 useProps는 컴포넌트 기본값과 테마 기본값을 합친 뒤 직접 전달한 값을 우선한다.
테마 공급자 · MantineProvider
하위 컴포넌트가 공통 색상·간격·기본 속성을 읽도록 설정을 공급하는 React 컴포넌트다. 이 레포에서는 theme.components.Button처럼 부품별 기본값도 한곳에서 지정한다.
파일 단위 스타일 · CSS Modules
파일마다 클래스 이름의 범위를 나누어 다른 컴포넌트와 이름이 충돌하지 않게 만드는 방식이다. Mantine의 Button.module.css는 버튼의 스타일 규칙을 담고, 빌드 설정은 클래스 이름을 변환하고 CSS를 추출한다.
CSS 커스텀 프로퍼티 · CSS 변수
CSS 규칙 안에서 이름을 붙여 재사용하는 값으로, 배선은 그대로 두고 조절값만 바꾸는 다이얼과 비슷하다. Mantine 버튼은 --button-radius 같은 값을 받아 미리 작성된 CSS 규칙에 적용한다.
스타일 조정 규약 · Styles API
컴포넌트의 어느 내부 요소를 어떻게 꾸밀지 지정하는 공통 사용법이다. Mantine에서는 root·label 같은 요소 이름에 맞춰 classNames·styles·vars를 지정하고, unstyled로 기본 스타일 적용을 조절한다.
변수 해석기 · varsResolver
테마와 속성값을 받아 내부 요소별 CSS 변수 객체를 만드는 함수다. Button.tsx의 해석기가 크기·색상·반경을 변수로 바꾸고, useStyles가 테마와 인스턴스의 변수 설정을 병합한다.
컴포넌트 제작 틀 · factory
반복해서 필요한 기능과 타입 규약을 컴포넌트에 붙이는 공통 틀이다. Mantine의 factory는 테마 설정용 .extend()와 속성값을 미리 지정하는 .withProps()를 제공한다.
다형성 컴포넌트 · polymorphicFactory
같은 사용법을 유지하면서 실제로 그리는 요소의 종류를 바꿀 수 있는 컴포넌트다. Mantine의 Button은 component="a"로 링크 요소를 그릴 수 있고, 다형성 타입은 그 요소에 맞는 속성을 연결한다.
제어 모드와 비제어 모드
입력값을 React 상태로 계속 전달하면 제어 모드이고, 입력 요소가 값을 보관하도록 초기값을 주면 비제어 모드다. useForm은 모드에 따라 value 또는 defaultValue를 넘기며, 비제어 모드의 최신 값은 getValues()로 읽는다.
폼 상태 관리 훅 · useForm
입력값·오류·검증·제출 처리를 연결하는 도구다. @mantine/form의 getInputProps는 입력 요소에 필요한 속성을 만들고, onSubmit은 검증 결과에 따라 제출 콜백을 실행한다.
브라우저의 로컬 저장소 · localStorage
같은 출처의 브라우저 화면에서 공유하며 새로고침 뒤에도 남는 문자열 저장소다. useLocalStorage는 값의 직렬화와 상태 동기화를 감싸고, 다른 탭의 storage 이벤트와 같은 탭의 사용자 정의 이벤트를 처리한다.
서버 렌더링과 하이드레이션 · SSR
서버 렌더링은 서버에서 초기 화면의 HTML을 만들고, 하이드레이션은 브라우저의 React가 그 화면에 동작을 연결하는 과정이다. Mantine의 ColorSchemeScript는 React가 동작하기 전에 저장된 색상 설정 등을 읽어 data-mantine-color-scheme을 정하므로 초기 색상 설정을 이해할 때 중요하다.
회귀 테스트
이미 정한 동작이 수정 뒤에도 유지되는지 반복 확인하는 테스트다. 이 레포의 Jest 테스트는 변수 병합·입력 속성·버튼 상태 등을 검증하며, jest.config.ts에서는 CSS를 대체하므로 실제 시각 결과는 브라우저에서도 확인해야 한다.