REPO DEEP DIVE · 2026-07-29 · MICROSOFT/FLUENTUI · 마이크로소프트 디자인 시스템의 3세대 동거

microsoft/fluentui 딥다이브
"낡은 본관과 새 별관이 함께 있는" 마이크로소프트 UI 캠퍼스

Fluent UI는 마이크로소프트가 Teams·Office·Windows 등 자사 제품 전체에 공통으로 쓰는 Fluent Design System을 웹에서 구현한 공식 컴포넌트 라이브러리다. 이 저장소 하나에 서로 다른 세대의 React 라이브러리 두 개(v8·v9)와 별도의 웹 컴포넌트 프로젝트가 함께 산다 — 옛날식 office-ui-fabric-react의 후신인 v8(@fluentui/react)은 유지보수만 하고, 처음부터 다시 설계된 v9(@fluentui/react-components)가 현재 신규 개발의 중심이다.

v9의 핵심은 Griffel이라는 원자적(atomic) CSS-in-JS 엔진과, 상태·스타일·렌더링을 3개 훅으로 쪼갠 컴포넌트 패턴이다. 색상·간격·글꼴을 직접 쓰지 않고 반드시 tokens라는 디자인 토큰을 거치게 강제해, 라이트/다크/고대비 테마 전환과 접근성을 기본값으로 만든다.

(저장소 microsoft/fluentui · 라이선스 MIT · v9 @fluentui/react-components 9.74.4 / v8 @fluentui/react 8.125.7 / @fluentui/web-components 3.0.1 · Yarn 4 워크스페이스 + Nx 빌드 · packages/ 아래 최상위 46개, 그중 react-components/ 하나에만 v9 서브패키지 87개 · v9 소스 .ts/.tsx 약 3,372개 · React 피어 지원 16.14~19 · 최신 커밋 2026-07-29)
목차
  1. 한 줄 정체
  2. 왜 지금 뜨는가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계·동작 원리
  6. 기술 스택·의존성
  7. 설치·요구사항
  8. 실제 사용 흐름
  9. 함정·오해 방지
  10. 대안 비교
  11. 정리 — 누가 쓰면 좋은가

1한 줄 정체

"마이크로소프트 전 제품이 쓰는 디자인 시스템"의 웹 구현체 — 그것도 3세대가 한 지붕 아래

Fluent UI는 하나의 프로젝트가 아니라 세 개의 프로젝트가 든 저장소다. README가 스스로 밝히듯 "이 레포는 오늘날 3개의 별도 프로젝트를 담고 있다"(v9 React Components, v8 React, Web Components). 원래 이름은 office-ui-fabric-react였고, 지금의 v8이 그 직계 후신이다. v9는 v8의 문제(런타임 스타일 계산 비용, 일관되지 않은 API)를 고치기 위해 처음부터 다시 설계된 별도 라인업이며, @fluentui/react-components로 배포된다.

실무에서 이 저장소를 접하는 경로는 대개 하나다: yarn add @fluentui/react-components @fluentui/react-icons 후 최상위에 <FluentProvider theme={webLightTheme}>를 감싸고 그 안에서 <Button>, <Dialog> 같은 컴포넌트를 쓰는 것. Teams 애드인이나 Microsoft 365 스타일이 필요한 사내 도구를 만들 때 가장 먼저 검토되는 라이브러리다.

한 줄 비유

할아버지 세대(v0)·부모 세대(v8)·자녀 세대(v9)가 함께 사는 3대 가족

가장 오래된 Northstar(v0)는 이미 은퇴했다(2025년 7월 EOL). v8은 여전히 Office 안에서 매일 일하는 "부모 세대"라 함부로 리모델링할 수 없어 유지보수만 한다. v9는 Microsoft 365가 밀어주는 "자녀 세대"로, 집안의 새 규칙(디자인 토큰, Griffel, 슬롯 시스템)을 따르며 계속 자라는 중이다.

세 세대가 한 저장소(집)에 사는 이유는 간단하다 — 회사(Microsoft)가 수천 개 제품을 하루아침에 이사시킬 수 없어서다. 그래서 v8과 v9를 한 페이지 안에서 점진적으로 섞어 쓰는(gradual migration) 것도 공식적으로 허용된다.

2왜 지금 뜨는가

v9 "최종 안정" 릴리스 도달 · v0 은퇴로 정리된 지도 · 빌드타임 CSS-in-JS라는 시대적 유행

① v9가 드디어 "완성형"으로 자리를 잡았다

README 상단에는 "Fluent UI React가 v9 최종 안정 릴리스를 출시하는 중"이라는 문구가 그대로 남아 있다. 즉 이 저장소는 오랫동안 "v8에서 v9로 넘어가는 과도기"였고, 그 이행이 실질적으로 마무리 단계에 들어서면서 v9를 실무에 도입하는 팀이 늘고 있다. react-migration-v8-v9라는 전용 패키지까지 별도로 유지될 만큼 이행 지원이 진지하다.

② v0(Northstar) 은퇴로 지형이 단순해졌다

Fluent UI React Northstar(v0)는 2025년 7월 공식 EOL을 맞았다. 한때 "v0·v8·v9"라는 3파전이었던 구도가 "v9로 수렴 + v8은 레거시 유지"라는 훨씬 단순한 그림으로 정리됐다. 신규 학습자가 "어떤 버전을 배워야 하나"라는 혼란 없이 v9에 집중할 수 있게 된 것이 최근의 관심 증가와 맞물린다.

③ "빌드타임에 CSS를 뽑아내는" 방식이 업계 표준처럼 자리잡는 중

v9의 스타일 엔진 Griffelstyled-components류의 런타임 CSS-in-JS가 안고 있던 성능 문제(매 렌더마다 스타일 계산)를, 원자적 클래스 생성 + 가능하면 빌드타임 추출로 해결한다. Vanilla Extract·Panda CSS 등과 같은 흐름 위에 있고, 마이크로소프트 규모의 실사용(수백 개 Teams 화면)으로 검증된 사례라는 점이 매력 포인트다.

경쟁 지형에서의 위치
"엔터프라이즈 + 접근성" 축의 대표주자

MUI가 범용성, Chakra/Mantine이 개발자 경험(DX), Ant Design이 관리자 대시보드 쪽으로 강하다면, Fluent UI는 WCAG 접근성 기본값·고대비 모드·다중 테마(라이트/다크/Teams 테마)가 처음부터 요구사항으로 박혀 있다는 점에서 차별화된다. 이는 "장난감 프로젝트"가 아니라 마이크로소프트 내부의 수백 개 프로덕트 팀이 매일 쓰는 실전 인프라이기 때문이다.

3무엇을 해결하나

"거대 조직 전체가 하나의 디자인 언어로 말하게" 하는 문제

Fluent UI가 푸는 근본 문제는 단순한 "예쁜 버튼 컴포넌트 모음"이 아니라, 수천 개의 제품 팀이 저마다 다른 스타일로 UI를 만들지 않도록 하는 조직 규모의 일관성 문제다. 이를 위해 세 가지 축을 해결한다.

① 디자인 토큰으로 "하드코딩된 값"을 없앤다

#0078d4 같은 색상 값을 직접 쓰면 다크모드·고대비 모드·브랜드 테마 전환이 불가능해진다. Fluent UI는 tokens.colorBrandBackground처럼 의미 기반 토큰만 쓰도록 강제해, 테마 하나만 바꾸면 앱 전체의 색·간격·타이포그래피가 일괄 전환되게 한다.

② 3계층 훅 패턴으로 "상태·스타일·마크업"을 분리한다

모든 v9 컴포넌트는 useX_unstable(상태 계산) → useXStyles_unstable(스타일 클래스 부여) → renderX_unstable(순수 JSX 렌더)로 쪼개져 있다. 덕분에 "동작은 그대로 두고 스타일만 바꾸기", "렌더링만 바꾸고 상태 로직은 재사용하기" 같은 부분 교체가 가능해지고, 버그를 고칠 때도 "이 버그는 세 파일 중 어디에 있는지"가 명확해진다.

③ 슬롯(slot) 시스템으로 "컴포넌트 내부를 안전하게 바꿔치기"할 수 있게 한다

Button의 아이콘, Dialog의 제목 영역처럼 컴포넌트를 이루는 각 부분을 슬롯으로 노출해, 사용자가 그 부분만 다른 엘리먼트나 컴포넌트로 갈아끼울 수 있게 한다. "컴포넌트를 통째로 다시 만들지 않고도 세부만 커스터마이징"할 수 있는 확장성이 여기서 나온다.

용어
디자인 시스템 (Design System)
색상·간격·타이포그래피 같은 스타일 규칙과, 버튼·대화상자 같은 컴포넌트를 한 벌로 묶어 여러 제품이 같은 "말투"로 UI를 그리게 하는 체계. Fluent Design System은 마이크로소프트의 디자인 시스템 이름이고, Fluent UI는 그것의 코드 구현체다.
용어
디자인 토큰 (Design Token)
"파랑"이 아니라 "브랜드 배경색"처럼 의미로 이름 붙인 값. 실제 색상 값은 테마가 바뀔 때마다 달라지지만, 토큰 이름(colorBrandBackground)은 코드에서 그대로 유지된다.

4전체 구조

Yarn 워크스페이스 + Nx 빌드 오케스트레이션 · "3개 프로젝트, 4개 의존성 티어"

저장소는 Yarn 4 워크스페이스(packageManager: "yarn@4.12.0")로 묶인 모노레포이고, 실제 빌드·테스트·캐싱은 Nx가 담당한다(nx.jsonbuild·test·type-check 등 타깃별 캐시 규칙이 정의돼 있다). packages/ 아래에 최상위 패키지 46개가 있고, 그중 packages/react-components/ 폴더 하나가 다시 v9 컴포넌트별 서브패키지 87개를 품고 있다 — react-button, react-dialog, react-theme처럼 컴포넌트 하나당 패키지 하나가 원칙이다.

┌─────────────────────────────────────────────────────────────────┐ │ FLUENTUI 모노레포 — 3개 프로젝트 │ └─────────────────────────────────────────────────────────────────┘ packages/ ├── react/ # v8 — @fluentui/react (구 office-ui-fabric-react)# 유지보수 전용, 런타임 mergeStyles 사용 ├── react-components/ # v9 — 서브패키지 87개, 신규 개발 중심 │ ├── react-theme/ # @fluentui/tokens를 재노출(thin wrapper) │ ├── react-utilities/ # slot·훅 등 공통 유틸 │ ├── react-button/ react-dialog/ react-menu/ ... # 컴포넌트별 패키지 │ └── react-components/ # ★ 배럴 패키지 — 전체를 재노출 └── web-components/ # FAST 기반 — @fluentui/web-components ┌─────────────────────────────────────────────────────────────────┐ │ v9 패키지 의존성 티어 (아래 → 위로만 의존 가능) │ └─────────────────────────────────────────────────────────────────┘ Tier 4 @fluentui/react-components # 모든 컴포넌트를 모은 배럴 Tier 3 react-button · react-dialog · ... # 서로 의존 금지(수평 의존 금지) Tier 2 react-utilities · react-theme · react-shared-contexts react-tabster · react-positioning · react-portal Tier 1 @griffel/react · @fluentui/tokens · react-jsx-runtime

이 계층 규칙은 실제로 docs/architecture/layers.md에 문서화돼 있으며, "react-buttonreact-menu에 의존해서는 안 된다"처럼 같은 티어(Tier 3) 안에서 수평 의존을 금지한다. 공유가 필요하면 반드시 Tier 2(유틸리티·컨텍스트)를 거치게 해, 컴포넌트 하나만 설치해도 다른 컴포넌트를 줄줄이 끌고 오지 않게 막는다.

용어
모노레포 (monorepo) · Nx
여러 패키지를 한 저장소에 두고 함께 버전 관리하는 방식. Nx는 그 안에서 "무엇이 바뀌었는지"를 추적해 영향받은 패키지만 다시 빌드·테스트하는 빌드 오케스트레이션 도구다.

5핵심 설계·동작 원리

Griffel(원자적 CSS-in-JS) · 3층 훅 패턴 · 슬롯 · FluentProvider의 테마 주입 — 실제 Button 소스로 추적

① 3층 훅 패턴 — 실제 Button.tsx로 보기

packages/react-components/react-button/library/src/components/Button/ 아래를 열면 아래 세 함수가 그대로 보인다. Button.tsx는 이 셋을 순서대로 호출할 뿐, 로직을 직접 갖지 않는다.

export const Button = React.forwardRef((props, ref) => {
  // ① 상태: props를 정규화하고 슬롯을 만든다
  const state = useButton_unstable(props, ref);

  // ② 스타일: state.root.className 을 Griffel 클래스로 채운다
  useButtonStyles_unstable(state);

  // ③ 렌더: state 를 그대로 JSX로 바꾼다(로직 없음)
  return renderButton_unstable(state);
});

useButton_unstableappearance(secondary 기본), shape(rounded 기본), size 같은 props에 기본값을 채우고, slot.optional(icon, …)로 아이콘을 슬롯 객체로 바꾼 뒤 { appearance, shape, size, icon, root, iconOnly, … } 형태의 state 객체를 만든다. renderButton_unstable은 그 state를 받아 <state.root>{state.icon && <state.icon />}{state.root.children}</state.root>만 반환하는 순수 함수다 — 조건 분기 외에 다른 로직이 전혀 없다.

비유

레스토랑 주방을 떠올리면 쉽다. useX_unstable은 주문서를 받아 "이 손님은 무엇을 원하는지" 정리하는 홀 서버고, useXStyles_unstable은 그 주문에 맞게 플레이팅(그릇·소스 배치)만 담당하는 사람이며, renderX_unstable은 완성된 접시를 그대로 손님 테이블에 놓는 서빙 담당이다. 세 역할이 나뉘어 있어 "소스 맛이 이상하면" 플레이팅 담당만, "주문이 잘못 전달됐으면" 홀 서버만 바꾸면 된다.

② Griffel — 빌드타임을 지향하는 원자적 CSS-in-JS

useButtonStyles.styles.ts@griffel/reactmakeStyles·makeResetStyles로 스타일을 정의한다. 실제 소스를 보면 appearance별(outline·primary·subtle·transparent), size별(small·medium·large), 심지어 강제 색상 모드(forced-colors: active, 즉 Windows 고대비 모드)까지 별도 스타일 객체로 나뉘어 있고, 이들을 mergeClasses()로 조건부 병합한다.

const useRootStyles = makeStyles({
  primary: { backgroundColor: tokens.colorBrandBackground, /* ... */ },
  subtle:  { backgroundColor: tokens.colorSubtleBackground, /* ... */ },
  small:   { minWidth: '64px', fontSize: tokens.fontSizeBase200 },
  // ...
});

// 사용부 — 규칙: 사용자가 넘긴 className은 항상 "맨 마지막"
state.root.className = mergeClasses(
  buttonClassNames.root,
  rootBaseClassName,
  appearance && rootStyles[appearance],
  rootStyles[size],
  state.root.className, // ← 항상 마지막
);

makeStyles는 하나의 규칙(예: { color: 'red' })마다 단 하나의 원자적(atomic) CSS 클래스를 만들어 여러 컴포넌트가 재사용하게 하고, 가능하면 빌드타임(babel/swc 플러그인)에 CSS를 미리 뽑아내 런타임 계산을 최소화한다. makeResetStyles는 컴포넌트당 한 번만 계산되는 "리셋용 기본 클래스"를 만들어 인라인 스타일 재계산 비용을 더 줄인다.

③ 디자인 토큰의 "진짜 소스"는 @fluentui/tokens

컴포넌트 코드는 @fluentui/react-theme에서 tokens를 가져오지만, 실제로 packages/react-components/react-theme/library/src/index.ts를 열어보면 그 안에서 하는 일은 @fluentui/tokens의 값을 그대로 재수출(re-export)하는 것뿐이다(tokens, webLightTheme, createLightTheme 등). 즉 토큰의 진실원(source of truth)은 react-theme가 아니라 tokens 패키지다.

④ FluentProvider — 테마를 실제 CSS 변수로 바꿔 주입하는 지점

<FluentProvider theme={...}>는 단순히 React 컨텍스트만 채우는 게 아니라, createCSSRuleFromTheme()로 테마 객체의 모든 키를 --colorNeutralForeground1: #242424; 같은 CSS 커스텀 프로퍼티 문자열로 직렬화한 뒤, useInsertionEffect(또는 SSR-세이프 폴백)로 <style> 태그를 만들어 document.head에 삽입한다. 이때 테마 값에 </>가 섞여 있으면 <style> 태그를 조기 종료시켜 HTML을 주입할 수 있으므로, 실제 코드는 이를 CSS 이스케이프 시퀀스로 치환하는 방어 로직까지 갖고 있다.

// createCSSRuleFromTheme.ts — 실제 로직을 요약하면
function createCSSRuleFromTheme(selector, theme) {
  const cssVars = Object.keys(theme).reduce((acc, key) =>
    acc + `--${key}: ${theme[key]}; `, '');
  return `${selector} { ${escapeForStyleTag(cssVars)} }`; // </> 이스케이프 필수
}

makeStyles로 만든 클래스들은 결국 color: var(--colorNeutralForeground1)처럼 이 CSS 변수를 참조하므로, 테마 객체 하나만 바꿔 끼우면 이미 그려진 컴포넌트들의 색이 재계산 없이 즉시 바뀐다 — 이것이 "런타임에 CSS 전체를 다시 만들지 않고도 테마를 바꾸는" v9식 접근이다.

⑤ 슬롯 시스템 — slot.always / slot.optional / assertSlots

@fluentui/react-utilitiesslot.ts는 "숏핸드(단순 값) 또는 완전한 props 객체"를 받아 항상 같은 모양의 슬롯 객체로 정규화한다. always는 반드시 렌더되는 슬롯(Button의 root), optional은 값이 없으면 undefined가 되어 아예 렌더되지 않는 슬롯(Button의 icon)을 만든다. renderX_unstable 안에서 쓰는 assertSlots<ButtonSlots>(state)는 타입스크립트가 "state의 각 필드가 진짜 슬롯 형태인지"를 컴파일 타임에 보증해 준다.

⑥ Tabster — 키보드 포커스만 전담하는 별도 패키지

방향키로 메뉴 이동, 모달 안에 포커스 가두기(focus trap), 루트에서 벗어나지 않는 탭 순환 같은 키보드 접근성 로직은 컴포넌트 코드에 흩어져 있지 않고 @fluentui/react-tabster라는 전용 패키지(내부적으로 마이크로소프트의 tabster 라이브러리를 감쌈)로 분리돼 있다. Button의 포커스 표시 스타일도 이 패키지의 createCustomFocusIndicatorStyle을 그대로 가져다 쓴다.

경계해야 할 착각
"_unstable" 접미사는 실제로 불안정하다는 뜻이 아니다

거의 모든 v9 함수 이름이 useButton_unstable처럼 _unstable로 끝난다. 이름만 보면 "베타 기능인가?" 싶지만, 실제로는 이미 안정 배포된 정식 API다. "내부 구현 세부사항을 감싸는 함수라 시그니처가 언제든 바뀔 수 있다"는 아키텍처적 경고 표시일 뿐, 프로덕션에서 못 쓰는 게 아니다. 이 네이밍만 보고 도입을 주저하는 것은 흔한 오해다.

6기술 스택·의존성

React 16~19 지원 · Griffel(별도 저장소) · TypeScript · Nx + Yarn 4 · beachball 버전 관리
영역내용
프레임워크React. v9 peerDependencies 기준 react >=16.14.0 <20.0.0 — React 16부터 19까지 폭넓게 지원(특정 최신 버전 강제 아님)
스타일 엔진Griffel(@griffel/react) — fluentui와는 다른 별도 저장소(microsoft/griffel)에서 개발되는 원자적 CSS-in-JS 라이브러리. v9는 이를 의존성으로 가져다 쓸 뿐, 이 레포에 스타일 엔진 자체 소스는 없다
디자인 토큰@fluentui/tokens(진짜 소스) → @fluentui/react-theme(재노출) → 컴포넌트의 tokens.xxx
접근성/포커스@fluentui/react-tabster(키보드 포커스 관리), @fluentui/react-aria(ARIA 속성 계산)
포지셔닝@fluentui/react-positioning — Popover·Tooltip·Menu 같은 플로팅 UI의 위치 계산
아이콘@fluentui/react-icons — 트리셰이킹 가능한 형태로 개별 아이콘만 임포트
빌드/모노레포Nx(빌드 오케스트레이션·캐시) + Yarn 4(packageManager: "yarn@4.12.0", workspace) — 2026년 안에 Yarn 4로 마이그레이션이 이뤄졌고 최신 커밋에도 그 여진(문서 복구)이 남아 있음
버전 관리beachball — 각 패키지가 독립 버전을 갖는 모노레포에서 "변경 파일(change file)"을 커밋과 함께 남겨 릴리스 시점에 자동으로 버전을 올리고 CHANGELOG를 생성하는 도구
웹 컴포넌트@fluentui/web-components — Microsoft의 FAST(@microsoft/fast-element) 위에 구축된 프레임워크 독립적 커스텀 엘리먼트 구현
CI/인프라Azure Pipelines(1ES 파이프라인 템플릿) — Windows 풀에서 CodeQL 컴플라이언스, Ubuntu 풀에서 빌드/테스트/린트, 별도 Job으로 E2E 배포
용어
Griffel
"규칙 하나 = 클래스 하나"로 CSS를 쪼개(atomic CSS) 여러 컴포넌트가 같은 클래스를 재사용하게 하고, 가능한 한 빌드 시점에 CSS를 미리 뽑아내려는 CSS-in-JS 라이브러리. Fluent UI 팀이 만들었지만 별도 저장소로 독립돼 있다.
용어
beachball
"이 커밋으로 어떤 패키지가 patch/minor/major로 올라가야 하는지"를 작은 변경 파일(change file)로 남기게 하는 버전 관리 도구. 리뷰어가 리뷰할 때 변경 의도를 파악하기 쉽고, 릴리스 시 자동으로 버전과 CHANGELOG를 계산한다.

7설치·요구사항

라이브러리 사용은 npm install 한 줄 · 저장소 자체를 빌드하려면 Node 22/24 + Yarn 4 필요

이 라이브러리를 "쓰기만" 할 때

yarn add @fluentui/react-components @fluentui/react-icons
import { FluentProvider, webLightTheme, Button } from '@fluentui/react-components';

function App() {
  return (
    <FluentProvider theme={webLightTheme}>
      <Button appearance="primary">안녕하세요</Button>
    </FluentProvider>
  );
}

peer dependency는 React 16.14~19대를 모두 허용하므로 최신 프로젝트든 다소 오래된 React 16/17 프로젝트든 도입에 큰 제약이 없다. Next.js 등 SSR 환경에서 쓸 때는 FluentProvider가 서버에서 만든 <style> 태그를 하이드레이션 시 재사용하도록 설계돼 있어(테스트 파일 FluentProvider-hydrate.test.tsx로 검증됨) 별도 설정 없이도 대체로 안전하다.

이 저장소 자체를 클론해 빌드/기여할 때

항목요구사항
Node.js^22.0.0 || ^24.0.0(package.json engines)
패키지 매니저Yarn 4.12.0corepack 또는 저장소에 커밋된 .yarn/releases/yarn-4.12.0.cjs로 고정 실행됨(nodeLinker: node-modules)
초기 설치yarnpostinstallhusky 훅 설치까지 자동 처리
단일 프로젝트 빌드yarn nx run <project>:build / yarn nx run <project>:test
컴포넌트 뜯어보기Storybook: yarn nx run <project>:start로 해당 컴포넌트의 스토리를 로컬에서 띄울 수 있음
주의
저장소 전체를 한 번에 빌드하려 하지 말 것

최상위 46개 패키지 + v9 서브패키지 87개를 합치면 저장소 규모가 매우 크다. 레포 자체의 에이전트 가이드(CLAUDE.md)조차 "react-components/는 74개 이상의 패키지가 있으니 특정 컴포넌트 이름으로 검색하되, 폴더 전체를 읽지 말라"고 명시한다. 처음 접한다면 yarn nx run <특정 프로젝트>:build처럼 범위를 좁혀서 접근하는 것이 안전하다.

8실제 사용 흐름

FluentProvider로 감싸기 → 컴포넌트 배치 → 커스텀 테마 만들기 → (기여 시) beachball change 남기기
흐름 ①

앱 최상단에 FluentProvider 배치

모든 v9 컴포넌트는 FluentProvider 안에 있어야 토큰이 CSS 변수로 주입된다. 앱을 라이트/다크로 전환하고 싶다면 theme prop에 webLightThemewebDarkTheme만 바꿔 끼우면 되고, Teams 스타일이 필요하면 teamsLightTheme류를 쓴다.

흐름 ②

컴포넌트는 슬롯 숏핸드로 조립

<Button icon={<CalendarIcon />} iconPosition="after">일정</Button>처럼 슬롯에 JSX를 그대로 넘기는 "숏핸드" 문법이 기본이고, 더 세밀한 제어가 필요하면 icon={{ children: <CalendarIcon />, className: '...' }}처럼 객체로 넘길 수도 있다. 두 형태 모두 내부적으로 slot.optional()이 같은 모양으로 정규화한다.

흐름 ③

브랜드 컬러로 커스텀 테마 만들기

createLightTheme(brandVariants)/createDarkTheme(brandVariants)에 16단계 브랜드 색상 램프를 넘기면, 그 브랜드에 맞춘 완전한 라이트/다크 테마 객체가 만들어진다. 사내 색상 가이드를 그대로 웹 앱에 반영하고 싶을 때 쓰는 공식 경로다.

흐름 ④ — 저장소에 기여할 때

변경 → beachball change → PR

컴포넌트 코드를 고쳤다면 반드시 yarn beachball change --type patch --message "..."로 변경 파일을 함께 커밋해야 한다. 이 파일이 없으면 CI의 check:change 단계가 실패한다 — "코드는 고쳤는데 왜 CI가 막히지"라는 초심자 함정의 대표 사례다.

9함정·오해 방지

v8·v9 혼동 · 문서와 코드의 실제 괴리 · 이름만 보고 판단하면 안 되는 것들
함정 ①
"@fluentui/react"와 "@fluentui/react-components"는 완전히 다른 스타일링 방식

v8(@fluentui/react)은 mergeStyles·mergeStyleSets 기반의 런타임 CSS-in-JS를 쓰고, v9(@fluentui/react-components)는 Griffel 기반이다. 이 저장소의 자체 에이전트 가이드조차 "v8 패키지의 패턴을 새 코드에 그대로 베끼지 말라. 검증 없이 기존 코드를 참고하지 말라"고 못 박아 둘 만큼, 두 세대의 스타일링 철학이 근본적으로 다르다. 검색 결과나 예제 코드가 어느 버전인지 반드시 import 경로로 먼저 확인해야 한다.

함정 ②
문서가 "진실"이라 명시하지만, 그 문서 자체가 비어 있는 경우가 있다

레포의 CLAUDE.md는 "이 파일의 지침이 기존 코드보다 우선하는 소스오브트루스"라고 선언하고, docs/quality-grades.md·docs/tech-debt-tracker.md 같은 문서로 품질/기술부채를 추적하겠다고 안내한다. 그런데 실제로 두 파일을 열어보면 표 본문이 _To be populated by first docs audit run_(첫 감사 실행 후 채워질 예정) 상태로 비어 있다. "문서가 있다"와 "문서에 실제 내용이 채워져 있다"는 다른 이야기라는, 아주 실전적인 함정이다.

함정 ③
웹 컴포넌트의 "정식 버전"과 README 배지가 서로 다른 말을 한다

packages/web-components/package.json의 실제 버전은 3.0.1(정식 릴리스 태그)이지만, 저장소 루트 README의 npm 배지는 @fluentui/web-components/beta 태그를 가리키며 3.0.0-beta.15 링크를 걸어 둔다. 어느 쪽을 신뢰해 도입 여부를 결정할지, 실제 npm 태그(latest vs beta)를 직접 확인하지 않으면 헷갈리기 쉬운 지점이다.

함정 ④
"react-theme"를 뜯어보면 진짜 토큰 정의는 없다

토큰 값(색상표, 브랜드 램프 계산식 등)을 찾으려고 @fluentui/react-theme 소스를 파고들면 허탕을 친다. 실제 정의는 @fluentui/tokens 패키지에 있고, react-theme는 이를 그대로 재수출하는 얇은 겹일 뿐이다. 패키지 이름만 보고 "테마"라는 단어가 들어간 곳에서 실물을 찾으려 하면 시간을 허비한다.

그나마 다행인 점
"_unstable" 이름과 별개로 실사용 검증은 확실하다

이름의 불안정함과 달리, v9는 Microsoft 365·Teams의 실제 프로덕션 화면에서 쓰이고 있어 API가 하루아침에 파괴적으로 바뀔 위험은 낮다. 다만 컴포넌트 내부 훅 함수(use~_unstable) 자체를 직접 import해 재사용하는 건 피하고, 공식 진입점(컴포넌트, props)만 쓰는 것이 안전하다.

10대안 비교

MUI · Ant Design · Adobe Spectrum · IBM Carbon · Chakra/Mantine과 나란히 놓고 보면
라이브러리강점 축Fluent UI와 다른 점
MUI (Material UI)범용성·생태계 규모·문서량구글 Material 언어 기반. Fluent UI는 마이크로소프트 자체 화면(Teams·Office)에 최적화된 결이 더 강함
Ant Design관리자·대시보드용 컴포넌트 밀도중국 엔터프라이즈 백오피스 쪽에 특화. Fluent UI는 고대비·다중 테마 같은 접근성 요구가 더 촘촘함
Adobe Spectrum(React Spectrum)헤드리스+접근성 분리 아키텍처철학이 비슷한 "선두주자"끼리 서로 참고 관계. Fluent UI는 슬롯+토큰, Spectrum은 훅(useX) 중심으로 결이 다름
IBM Carbon정부·금융권 규정 준수형 접근성둘 다 "엔터프라이즈+접근성"을 표방하지만, Carbon은 IBM 제품군, Fluent UI는 Microsoft 365 생태계에 맞춰짐
Chakra UI / Mantine가볍고 빠른 개발자 경험(DX)Fluent UI는 토큰·슬롯·3층 훅 규칙이 훨씬 엄격해 학습 곡선이 더 높은 대신, 대규모 조직 일관성에는 더 유리
용어
헤드리스 컴포넌트 (Headless Component)
동작·접근성 로직만 제공하고 스타일은 전혀 강제하지 않는 컴포넌트 방식(Radix·React Aria 계열). Fluent UI는 완전 헤드리스는 아니지만, Griffel 클래스와 슬롯 시스템 덕분에 스타일 오버라이드가 비교적 유연한 편이다.

11정리 — 누가 쓰면 좋은가

Microsoft 365/Teams 생태계 · 접근성·다중 테마가 필수 요구사항인 엔터프라이즈 팀에게
이럴 때 적합
Teams 애드인, Microsoft 365 룩앤필이 필요한 사내 도구, 고대비/다크모드가 계약 요건인 프로젝트

Teams 탭·애드인을 만들거나, 회사 로그인 화면이 Office와 자연스럽게 이어져야 하거나, 공공·금융권처럼 접근성(WCAG, 고대비 모드)이 필수 요건인 엔터프라이즈 웹앱이라면 Fluent UI v9가 가장 직접적인 선택지다. 디자인 토큰과 3층 훅 패턴을 따르는 규율이 처음엔 번거롭지만, 팀이 커질수록 "왜 이 버튼만 색이 다르지" 같은 일관성 붕괴를 막아 준다.

이럴 땐 다른 선택지도 고려
빠른 프로토타입, Microsoft 색채가 필요 없는 일반 소비자 서비스

마이크로소프트 느낌이 꼭 필요하지 않은 랜딩 페이지나 빠르게 검증만 하면 되는 MVP라면, 학습 곡선이 낮은 Chakra/Mantine이나 생태계가 훨씬 큰 MUI가 개발 속도 면에서 더 유리할 수 있다. 또한 이미 v8(@fluentui/react) 기반 대형 코드베이스를 운영 중이라면, 전체를 한 번에 v9로 옮기기보다 react-migration-v8-v9 패키지를 활용한 점진적 이행을 권한다.