본문으로 건너뛰기
5분 AI.
REPO DEEP DIVE · 2026-07-29 · MICROSOFT/FLUENTUI · 마이크로소프트 디자인 시스템의 3세대 동거

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

UI 라이브러리

빠른 판단

무엇
Microsoft Fluent 디자인의 React 컴포넌트·웹 컴포넌트·테마를 제공하는 UI 패키지 모음
이럴 때
Teams·Office 계열과 어울리는 웹 화면을 만들고 프로젝트에 맞는 컴포넌트 패키지와 테마를 고를 때
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. 정리 — 누가 쓰면 좋은가
  12. 직접 해볼 수 있는 실습 과제
  13. 관련 기술 심화 학습 로드맵
  14. 핵심 키워드 사전

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의 스타일 엔진 Griffel은 styled-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.json에 build·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-button이 react-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_unstable은 appearance(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/react의 makeStyles·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-utilities의 slot.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.0 — corepack 또는 저장소에 커밋된 .yarn/releases/yarn-4.12.0.cjs로 고정 실행됨(nodeLinker: node-modules)
초기 설치yarn → postinstall이 husky 훅 설치까지 자동 처리
단일 프로젝트 빌드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에 webLightTheme ↔ webDarkTheme만 바꿔 끼우면 되고, 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 패키지를 활용한 점진적 이행을 권한다.

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

Button 추적 → 슬롯 조립 → 중첩 테마 → 접근성 회귀 테스트로 넓혀 가는 네 가지 과제

아래 경로와 명령은 저장소 루트 기준이다. 코드 변경과 실행 과제는 의존성이 준비된 별도 실습용 사본에서 진행한다. 실행 환경은 루트 package.json의 engines·packageManager와 .yarnrc.yml을 확인한다.

과제 ① · 입문

Button 하나의 처리 경로 복원하기

목표: 화면의 버튼을 입력 속성, 상태, 스타일, 렌더링으로 나누어 설명한다.

  1. packages/react-components/react-button/library/src/components/Button/Button.tsx를 읽고 호출 순서를 적는다. 같은 폴더의 useButton.ts, useButtonStyles.styles.ts, renderButton.tsx로 이동해 각 함수가 받거나 바꾸는 값을 표시한다.
  2. 다음 명령으로 기본값과 슬롯 생성 지점을 찾는다. size는 전달된 속성뿐 아니라 컨텍스트의 값도 참고한다는 점을 확인한다.
    rg -n 'appearance|shape|size|slot[.]|iconOnly' packages/react-components/react-button/library/src/components/Button/useButton.ts
  3. <Button>저장</Button>과 아이콘을 넣고 iconPosition="after"를 지정한 경우를 비교한다. state.icon, state.iconOnly, state.root.children이 렌더링 분기에 어떻게 쓰이는지 추적한다.

완료 기준: 입력 → 상태와 슬롯 → 클래스 결합 → JSX의 흐름도를 만들고, 아이콘이 없는 경우와 글자 뒤에 붙는 경우를 실제 함수 이름으로 설명할 수 있다.

과제 ② · 초중급

같은 아이콘을 두 가지 슬롯 입력으로 조립하기

목표: JSX 숏핸드와 속성 객체가 같은 슬롯으로 정규화되는 과정을 확인한다.

  1. packages/react-components/react-button/stories/src/Button/ButtonIcon.stories.tsx를 연다. 기존 Icon 스토리 안에 아래 두 버튼을 나란히 배치한다. 이 파일에는 Button과 CalendarMonthRegular의 가져오기가 이미 있다.
    <Button icon={<CalendarMonthRegular />}>일정</Button>
    <Button icon={{ children: <CalendarMonthRegular /> }}>
      일정
    </Button>
  2. 한 버튼에 iconPosition="after"를 지정했다가 icon={undefined}로 바꾸며 결과를 비교한다. 글자를 없애 아이콘만 남기는 경우에는 aria-label="일정"을 지정한다.
  3. 아래 명령으로 Storybook을 열고 Button의 아이콘 예제를 확인한다. 개발자 도구에서 아이콘과 글자의 순서를 보고, packages/react-components/react-utilities/src/compose/slot.ts의 resolveShorthand와 optional에서 입력 처리 근거를 찾는다.
    yarn nx run react-button-stories:start

완료 기준: 같은 위치 설정에서 두 입력 방식이 같은 모양으로 표시되고, 뒤 배치에서는 순서가 바뀌며, 아이콘을 빼면 아이콘 슬롯이 렌더되지 않는다. 아이콘만 있는 버튼도 보조 기술이 읽을 이름을 갖는다.

과제 ③ · 중급

중첩 FluentProvider의 부분 테마 관찰하기

목표: 토큰을 사용하는 스타일을 유지하면서 특정 영역의 테마만 바꾼다.

  1. packages/react-components/react-provider/stories/src/Provider/FluentProviderNested.stories.tsx를 읽는다. 예제의 배경·글자·테두리가 각각 어떤 토큰을 사용하는지 찾고, packages/tokens/src/tokens.ts에서 해당 토큰이 참조하는 CSS 변수 이름을 확인한다.
  2. 기존 가져오기에 webDarkTheme를 추가한다. 바깥쪽 webLightTheme는 유지하고 안쪽 FluentProvider의 theme 속성을 다음처럼 바꾼다.
    theme={{
      colorBrandBackground2: webDarkTheme.colorBrandBackground2,
      colorBrandForeground2: webDarkTheme.colorBrandForeground2,
      colorBrandStroke1: webDarkTheme.colorBrandStroke1,
    }}
  3. 아래 명령으로 중첩 예제를 확인한다. 개발자 도구에서 안팎의 세 토큰 값과, 별도로 지정하지 않은 --fontSizeBase300의 값을 비교한다.
    yarn nx run react-provider-stories:start
    yarn nx run react-provider-stories:type-check

완료 기준: 바깥 영역은 유지되고 안쪽의 지정 토큰만 바뀌며, 미지정 토큰은 부모 테마 값을 유지한다. packages/react-components/react-provider/library/src/components/FluentProvider/useFluentProvider.ts의 shallowMerge(parentTheme, theme)로 그 이유를 설명한다.

과제 ④ · 심화

비활성 상태로 바뀌어도 포커스를 유지하는지 검증하기

목표: disabledFocusable의 포커스 유지와 활성화 차단을 상태 전환 테스트로 묶는다.

  1. packages/react-components/react-button/library/src/components/Button/Button.test.tsx에서 포커스·클릭 테스트를 읽는다. 이어서 packages/react-components/react-aria/library/src/button/useARIAButtonProps.ts의 기본 버튼 처리에서 disabled, aria-disabled, 이벤트 처리기가 어떻게 정해지는지 확인한다.
  2. 같은 테스트 파일에 상태 전환 사례를 추가한다. render로 이름이 있는 버튼과 jest.fn() 클릭 처리기를 만들고, userEvent.tab()으로 포커스를 준다. 반환된 rerender로 같은 버튼에 disabledFocusable를 적용한다.
  3. 포커스가 같은 버튼에 남고 aria-disabled="true"가 적용되는지 확인한다. 클릭·Enter·Space 입력 뒤 처리기 호출이 없는지 검증한 다음, disabledFocusable를 해제하고 클릭하면 한 번 호출되는지도 확인한다.
  4. 컴포넌트 구현은 유지한 채 패키지 테스트를 실행한다.
    yarn nx run react-button:test --runInBand

완료 기준: 새 상태 전환 테스트와 기존 테스트가 함께 통과한다. 결과물에는 활성 상태 → 포커스 가능한 비활성 상태 → 활성 상태의 관찰값과, 단순히 회색으로 표시하는 것만으로는 충분하지 않은 이유를 적는다.

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

여섯 주 동안 컴포넌트 내부에서 테마·접근성·모노레포 운영까지 연결하기

매주 읽은 코드에서 근거를 찾고 작은 결과물을 남긴다. 앞의 실습 과제를 함께 진행하면 구조 설명과 실제 동작을 서로 대조할 수 있다.

주차배울 개념이 레포에서 읽을 곳결과물
1주차 React 속성·상태, TypeScript 타입, 참조 전달 packages/react-components/react-button/library/src/components/Button/의 Button.types.ts와 Button.tsx ButtonProps·ButtonState·ButtonSlots의 역할표와 버튼 처리 흐름도
2주차 슬롯, 숏핸드 정규화, 조건부 렌더링 packages/react-components/react-utilities/src/compose/slot.ts와 assertSlots.ts, Button 폴더의 useButton.ts와 renderButton.tsx 아이콘 없음·앞 배치·뒤 배치·아이콘만 있는 경우의 입력과 렌더 결과 비교표
3주차 Griffel 클래스 결합, 의미 기반 토큰, CSS 변수 Button 폴더의 useButtonStyles.styles.ts, packages/tokens/src/tokens.ts, packages/react-components/react-theme/library/src/index.ts 버튼의 배경·글자·포커스 스타일에서 토큰과 CSS 변수까지 이어지는 추적표
4주차 중첩 테마, 컨텍스트, 서버 렌더링과 하이드레이션 packages/react-components/react-provider/library/src/components/FluentProvider/의 useFluentProvider.ts, useFluentProviderThemeStyleTag.ts, FluentProvider-hydrate.test.tsx 부분 테마 실습 결과와 서버에서 만든 테마 스타일을 브라우저에서 연결하는 순서도
5주차 ARIA 상태, 키보드 조작, 포커스 표시와 회귀 테스트 packages/react-components/react-aria/library/src/button/useARIAButtonProps.ts, packages/react-components/react-tabster/src/focus/createCustomFocusIndicatorStyle.ts, Button 폴더의 Button.test.tsx 기본·비활성·포커스 가능한 비활성 상태의 조작 비교표와 상태 전환 테스트
6주차 Nx 타깃·캐시·의존 작업, 패키지별 검증과 릴리스 기록 nx.json, packages/react-components/react-button/library/project.json, tools/workspace-plugin/src/plugins/workspace-plugin.ts, docs/workflows/contributing.md Button의 빌드·테스트·타입 검사와 스토리 실행 명령표, 배포 코드 변경 시 필요한 검증·변경 파일 설명서

14핵심 키워드 사전

경로를 찾고 코드를 읽을 때 다시 확인할 열네 가지 개념
용어
모노레포와 워크스페이스
여러 패키지를 한 저장소에서 함께 관리하고, 패키지 매니저가 그 관계를 연결하는 구성이다. 이 레포는 루트 package.json의 workspaces로 앱·컴포넌트·도구 패키지를 묶는다.
용어
Nx 타깃
특정 프로젝트에 대해 실행할 빌드·테스트 같은 작업의 이름이다. react-button:test의 앞부분은 프로젝트, 뒷부분은 작업이며, 이 레포에서는 project.json과 워크스페이스 플러그인·nx.json을 함께 읽어 실제 구성을 파악한다.
용어
Storybook 스토리
컴포넌트의 특정 사용 상태를 따로 띄워 확인하는 예제다. ButtonIcon.stories.tsx처럼 상태별 파일을 작성하고 index.stories.tsx에서 내보내는 구성을 이 레포에서 볼 수 있다.
용어
ForwardRefComponent와 참조 전달
부모가 전달한 참조를 내부 요소까지 연결하면 부모 코드가 해당 요소에 포커스를 주는 등의 작업을 할 수 있다. Button.tsx는 ForwardRefComponent 타입과 React.forwardRef를 사용하고, 받은 참조를 상태 생성 함수에 전달한다.
용어
상태·스타일·렌더링 분리
입력 정리, 클래스 적용, 화면 구조 생성을 서로 다른 함수에 맡기는 패턴이다. Button의 useButton_unstable·useButtonStyles_unstable·renderButton_unstable을 따라가면 문제의 책임 위치를 좁힐 수 있다.
용어
슬롯
컴포넌트를 이루는 부분마다 속성과 렌더링 방식을 지정할 수 있도록 만든 확장 지점이다. Button에서는 바깥 요소인 root와 선택적으로 표시하는 icon이 슬롯이며, 각각 slot.always와 slot.optional로 구성한다.
용어
숏핸드
슬롯 속성 객체를 전부 쓰는 대신 JSX나 문자열 같은 내용을 바로 전달하는 간단한 입력 방식이다. slot.ts의 resolveShorthand는 이런 입력을 children을 가진 속성 객체로 정리한다.
용어
Griffel과 원자적 CSS
스타일을 작은 CSS 규칙과 클래스로 나누어 재사용하는 스타일링 도구다. 이 레포의 Button은 외부 패키지인 @griffel/react의 makeStyles·makeResetStyles로 스타일을 정의하고 mergeClasses로 상태에 맞게 결합한다.
용어
디자인 토큰
색상이나 간격을 용도에 맞는 이름으로 참조하게 하는 값이다. 이 레포의 실제 정의는 packages/tokens/src/에 있으며, react-theme가 다시 내보내는 tokens.colorBrandBackground 같은 항목은 해당 CSS 변수를 참조한다.
용어
FluentProvider
하위 영역이 사용할 테마와 표시 방향 같은 공통 설정을 제공하는 컴포넌트다. 이 레포에서는 부모 테마와 전달받은 테마를 합치고, 테마를 CSS 변수 규칙으로 만들어 하위 컴포넌트의 스타일에 연결한다.
용어
ARIA
화면 요소의 역할과 상태를 보조 기술에 전달하기 위한 속성 체계다. useARIAButtonProps는 렌더할 요소와 속성에 맞춰 역할·비활성 상태·키보드 처리 등을 구성하며, 속성 표시와 실제 조작 동작을 함께 다룬다.
용어
disabledFocusable
동작은 막으면서 포커스를 받을 수 있게 두는 Button 속성이다. 이 레포에서는 aria-disabled와 이벤트 처리를 조정하며, Button.test.tsx가 포커스 가능 여부와 클릭 처리 차단을 검증한다.
용어
서버 렌더링과 하이드레이션
서버 렌더링은 HTML을 서버에서 먼저 만들고, 하이드레이션은 브라우저에서 그 HTML에 React 동작을 연결하는 과정이다. FluentProvider-hydrate.test.tsx와 테마 스타일 훅을 함께 읽으면 이 과정에서 테마 스타일을 연결하고 기존 스타일 요소를 재사용하는 흐름을 배울 수 있다.
용어
beachball 변경 파일
어느 패키지가 왜 어떤 종류의 버전 변경을 필요로 하는지 기록하는 작은 파일이다. 이 레포의 기여 절차는 배포 패키지를 바꾸는 경우 변경 파일을 요구하며, 릴리스 시 버전과 변경 이력을 정리하는 근거로 사용한다.