REPO DEEP DIVE · 2026-07-29 · MANTINEDEV/MANTINE · Emotion 없이 CSS 변수로 테마를 짜는 리액트 UI 라이브러리

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

MantineReact 애플리케이션을 만들 때 필요한 거의 모든 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. 정리 · 누가 쓰면 좋은가

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.jsonworkspaces: ["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 같은 "확장 패키지"는 모두 corehookspeerDependency로 요구한다. 즉 @mantine/core가 사실상 프레임워크의 심장이고, 나머지는 그 위에 얹힌 플러그인이다.

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

Mantine은 lernachangesets 같은 독립 버전 관리 도구를 쓰지 않는다. 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.tsxfactory() 함수는 평범한 함수형 컴포넌트를 받아 .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/stylestheme.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.cssbackground: 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) {}

unstyledheadless — "스타일을 아예 끄는" 탈출구

각 컴포넌트 unstyled prop이나 MantineProviderheadless 모드를 켜면 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/datesdayjspeerDependency로만 요구(직접 번들 안 함) · @mantine/chartsrecharts >=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-mantinepostcss.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 라이브러리의 스타일 아키텍처를 상당 부분 이해할 수 있다.