본문으로 건너뛰기

TanStack Router와 Material-UI(MUI) 통합 방법

이 가이드에서는 올바른 TypeScript 통합과 컴포넌트 조합 패턴을 포함해 Material-UI를 TanStack Router와 함께 설정하는 방법을 다룹니다.

빠른 시작

소요 시간: 45~60분
난이도: 중급
사전 요구 사항: 기존 TanStack Router 프로젝트

수행할 작업

  • TanStack Router와 함께 Material-UI 설치 및 구성
  • 올바른 테마 프로바이더 통합 설정
  • 타입 안전성을 갖춘 라우터 호환 MUI 컴포넌트 생성
  • 활성 상태 표시기가 있는 탐색 구현
  • 일반적인 TypeScript 및 스타일 문제 해결

설치 및 설정

1단계: Material-UI 의존성 설치

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

선택 사항: 날짜 선택기 지원 추가

npm install @mui/x-date-pickers dayjs

2단계: 테마 프로바이더 설정

TanStack Router와 함께 작동하는 테마 프로바이더를 생성합니다.

// src/components/theme-provider.tsx
import { ThemeProvider, createTheme } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import { ReactNode } from 'react'

const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
},
components: {
// Customize components for router integration
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none', // More modern button styling
},
},
},
MuiLink: {
styleOverrides: {
root: {
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
},
},
},
},
})

interface MuiThemeProviderProps {
children: ReactNode
}

export function MuiThemeProvider({ children }: MuiThemeProviderProps) {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
)
}

3단계: 루트 라우트 업데이트

MUI 테마 프로바이더로 애플리케이션을 감쌉니다.

// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { MuiThemeProvider } from '@/components/theme-provider'

export const Route = createRootRoute({
component: () => (
<MuiThemeProvider>
<Outlet />
<TanStackRouterDevtools />
</MuiThemeProvider>
),
})

라우터 호환 MUI 컴포넌트 생성

MUI Link 컴포넌트는 TanStack Router의 타입 시스템을 위해 특별히 처리해야 합니다.

// src/components/ui/mui-router-link.tsx
import { createLink } from '@tanstack/react-router'
import { Link as MuiLink, type LinkProps } from '@mui/material/Link'
import { forwardRef } from 'react'

// Create a router-compatible MUI Link with full type safety
export const RouterLink = createLink(
forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
return <MuiLink ref={ref} {...props} />
}),
)

2단계: 타입이 지정된 MUI Button 컴포넌트 생성

// src/components/ui/mui-router-button.tsx
import { createLink } from '@tanstack/react-router'
import { Button, type ButtonProps } from '@mui/material/Button'
import { forwardRef } from 'react'

// Create a router-compatible MUI Button
export const RouterButton = createLink(
forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
return <Button ref={ref} component="button" {...props} />
}),
)

3단계: 고급 탐색 컴포넌트 생성

// src/components/ui/mui-router-fab.tsx
import { createLink } from '@tanstack/react-router'
import { Fab, type FabProps } from '@mui/material/Fab'
import { forwardRef } from 'react'

// Router-compatible Floating Action Button
export const RouterFab = createLink(
forwardRef<HTMLButtonElement, FabProps>((props, ref) => {
return <Fab ref={ref} {...props} />
}),
)

활성 상태를 사용하는 탐색 구현

1단계: 활성 상태가 있는 탐색 탭 생성

// src/components/navigation/mui-nav-tabs.tsx
import { useMatchRoute } from '@tanstack/react-router'
import { Tabs, Tab, type TabsProps } from '@mui/material'
import { RouterLink } from '@/components/ui/mui-router-link'

interface NavTab {
label: string
to: string
value: string
icon?: React.ReactNode
}

interface MuiNavTabsProps extends Omit<TabsProps, 'value' | 'onChange'> {
tabs: NavTab[]
}

export function MuiNavTabs({ tabs, ...tabsProps }: MuiNavTabsProps) {
const matchRoute = useMatchRoute()

// Find active tab based on current route
const activeTab =
tabs.find((tab) => matchRoute({ to: tab.to, fuzzy: true }))?.value || false

return (
<Tabs value={activeTab} {...tabsProps}>
{tabs.map((tab) => (
<Tab
key={tab.value}
label={tab.label}
value={tab.value}
icon={tab.icon}
component={RouterLink}
to={tab.to}
sx={{
'&.Mui-selected': {
fontWeight: 'bold',
},
}}
/>
))}
</Tabs>
)
}

2단계: 탐색 드로어 생성

// src/components/navigation/mui-nav-drawer.tsx
import { useMatchRoute } from '@tanstack/react-router'
import {
Drawer,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
Box,
type DrawerProps,
} from '@mui/material'
import { RouterLink } from '@/components/ui/mui-router-link'

interface DrawerItem {
label: string
to: string
icon?: React.ReactNode
}

interface MuiNavDrawerProps extends Omit<DrawerProps, 'children'> {
items: DrawerItem[]
title?: string
}

export function MuiNavDrawer({
items,
title,
...drawerProps
}: MuiNavDrawerProps) {
const matchRoute = useMatchRoute()

return (
<Drawer {...drawerProps}>
<Box sx={{ width: 250 }} role="presentation">
{title && (
<Typography
variant="h6"
sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}
>
{title}
</Typography>
)}

<List>
{items.map((item) => {
const isActive = matchRoute({ to: item.to, fuzzy: true })

return (
<ListItem key={item.to} disablePadding>
<ListItemButton
component={RouterLink}
to={item.to}
selected={isActive}
sx={{
'&.Mui-selected': {
backgroundColor: 'primary.main',
color: 'primary.contrastText',
'&:hover': {
backgroundColor: 'primary.dark',
},
},
}}
>
{item.icon && <ListItemIcon>{item.icon}</ListItemIcon>}
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
)
})}
</List>
</Box>
</Drawer>
)
}

3단계: 탐색이 있는 앱 바 생성

// src/components/navigation/mui-app-bar.tsx
import { useState } from 'react'
import {
AppBar,
Toolbar,
Typography,
IconButton,
Menu,
MenuItem,
Box,
} from '@mui/material'
import { Menu as MenuIcon, AccountCircle } from '@mui/icons-material'
import { RouterButton, RouterLink } from '@/components/ui/mui-router-link'
import { MuiNavDrawer } from './mui-nav-drawer'

interface AppBarItem {
label: string
to: string
icon?: React.ReactNode
}

interface MuiAppBarProps {
title: string
navigationItems: AppBarItem[]
userMenuItems?: AppBarItem[]
}

export function MuiAppBar({
title,
navigationItems,
userMenuItems,
}: MuiAppBarProps) {
const [drawerOpen, setDrawerOpen] = useState(false)
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null)

const handleUserMenuClick = (event: React.MouseEvent<HTMLElement>) => {
setUserMenuAnchor(event.currentTarget)
}

const handleUserMenuClose = () => {
setUserMenuAnchor(null)
}

return (
<>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={() => setDrawerOpen(true)}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>

<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<RouterLink to="/" color="inherit" underline="none">
{title}
</RouterLink>
</Typography>

{/* Desktop Navigation */}
<Box sx={{ display: { xs: 'none', md: 'flex' }, mr: 2 }}>
{navigationItems.map((item) => (
<RouterButton
key={item.to}
to={item.to}
color="inherit"
startIcon={item.icon}
sx={{ ml: 1 }}
>
{item.label}
</RouterButton>
))}
</Box>

{/* User Menu */}
{userMenuItems && (
<>
<IconButton color="inherit" onClick={handleUserMenuClick}>
<AccountCircle />
</IconButton>
<Menu
anchorEl={userMenuAnchor}
open={Boolean(userMenuAnchor)}
onClose={handleUserMenuClose}
>
{userMenuItems.map((item) => (
<MenuItem
key={item.to}
component={RouterLink}
to={item.to}
onClick={handleUserMenuClose}
>
{item.label}
</MenuItem>
))}
</Menu>
</>
)}
</Toolbar>
</AppBar>

{/* Mobile Navigation Drawer */}
<MuiNavDrawer
items={navigationItems}
title="Navigation"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
/>
</>
)
}

사용 예제

완전한 페이지 예제

// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import {
Container,
Typography,
Box,
Card,
CardContent,
CardActions,
Chip,
Stack,
} from '@mui/material'
import { Edit, Delete, ArrowBack } from '@mui/icons-material'
import { RouterButton, RouterLink } from '@/components/ui/mui-router-link'

export const Route = createFileRoute('/posts/$postId')({
component: PostPage,
})

function PostPage() {
const { postId } = Route.useParams()

return (
<Container maxWidth="md" sx={{ py: 4 }}>
{/* Breadcrumb Navigation */}
<Box sx={{ mb: 3 }}>
<RouterLink
to="/posts"
color="primary"
sx={{ display: 'flex', alignItems: 'center', mb: 2 }}
>
<ArrowBack sx={{ mr: 1 }} />
Back to Posts
</RouterLink>
</Box>

{/* Post Content */}
<Card>
<CardContent>
<Typography variant="h4" component="h1" gutterBottom>
Post {postId}
</Typography>

<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
<Chip label="React" color="primary" size="small" />
<Chip label="TypeScript" color="secondary" size="small" />
</Stack>

<Typography variant="body1" paragraph>
This is the content of post {postId}. It demonstrates how
Material-UI components work seamlessly with TanStack Router.
</Typography>
</CardContent>

<CardActions>
<RouterButton
to="/posts/$postId/edit"
params={{ postId }}
variant="contained"
startIcon={<Edit />}
size="small"
>
Edit Post
</RouterButton>

<RouterButton
to="/posts/$postId/delete"
params={{ postId }}
variant="outlined"
color="error"
startIcon={<Delete />}
size="small"
>
Delete Post
</RouterButton>
</CardActions>
</Card>
</Container>
)
}

탐색이 있는 레이아웃

// src/routes/_layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
import { Box } from '@mui/material'
import { Home, Article, Info, Contact } from '@mui/icons-material'
import { MuiAppBar } from '@/components/navigation/mui-app-bar'

export const Route = createFileRoute('/_layout')({
component: LayoutComponent,
})

const navigationItems = [
{ label: 'Home', to: '/', icon: <Home /> },
{ label: 'Posts', to: '/posts', icon: <Article /> },
{ label: 'About', to: '/about', icon: <Info /> },
{ label: 'Contact', to: '/contact', icon: <Contact /> },
]

const userMenuItems = [
{ label: 'Profile', to: '/profile' },
{ label: 'Settings', to: '/settings' },
{ label: 'Logout', to: '/logout' },
]

function LayoutComponent() {
return (
<Box sx={{ flexGrow: 1 }}>
<MuiAppBar
title="My App"
navigationItems={navigationItems}
userMenuItems={userMenuItems}
/>

<Box component="main" sx={{ mt: 2 }}>
<Outlet />
</Box>
</Box>
)
}

일반적인 문제

컴포넌트 props 관련 TypeScript 오류

문제: MUI 컴포넌트에서 TanStack Router props를 사용할 때 TypeScript 오류가 발생합니다.

해결 방법: 올바른 타입 지정을 위해 항상 createLink를 사용합니다.

// ❌ This will cause TypeScript errors
const BadButton = (props: any) => <Button {...props} />

// ✅ This provides full type safety
export const RouterButton = createLink(
forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
return <Button ref={ref} component="button" {...props} />
}),
)

스타일 충돌

문제: MUI 스타일이 다른 라이브러리 또는 사용자 지정 스타일과 충돌합니다.

해결 방법:

  1. MUI의 emotion 캐시 사용:

    import { CacheProvider } from '@emotion/react'
    import createCache from '@emotion/cache'

    const cache = createCache({
    key: 'mui',
    prepend: true,
    })

    export function App() {
    return (
    <CacheProvider value={cache}>
    <MuiThemeProvider>{/* Your app */}</MuiThemeProvider>
    </CacheProvider>
    )
    }
  2. CSS 특이성 높이기:

    const StyledButton = styled(Button)(({ theme }) => ({
    '&.router-active': {
    backgroundColor: theme.palette.primary.main,
    color: theme.palette.primary.contrastText,
    },
    }))

테마가 올바르게 적용되지 않음

문제: MUI 테마 변경 사항이 라우터로 생성한 컴포넌트에 적용되지 않습니다.

해결 방법: 테마 프로바이더가 앱 전체를 감싸는지 확인합니다.

// ❌ Theme provider inside routes won't work for navigation
export const Route = createFileRoute('/some-route')({
component: () => (
<ThemeProvider theme={theme}>
<SomeComponent />
</ThemeProvider>
),
})

// ✅ Theme provider at root level
export const Route = createRootRoute({
component: () => (
<ThemeProvider theme={theme}>
<Outlet />
</ThemeProvider>
),
})

대규모 앱의 성능 문제

문제: 번들 크기 또는 런타임 성능에 문제가 있습니다.

해결 방법:

  1. 트리 셰이킹 사용:

    // ✅ Import only what you need
    import Button from '@mui/material/Button'
    import TextField from '@mui/material/TextField'

    // ❌ Avoid importing everything
    import { Button, TextField } from '@mui/material'
  2. 대형 컴포넌트에 동적 임포트 사용:

    import { lazy, Suspense } from 'react'
    import { CircularProgress } from '@mui/material'

    const DataGrid = lazy(() =>
    import('@mui/x-data-grid').then((module) => ({
    default: module.DataGrid,
    })),
    )

    function MyComponent() {
    return (
    <Suspense fallback={<CircularProgress />}>
    <DataGrid {...props} />
    </Suspense>
    )
    }

프로덕션 체크리스트

MUI + TanStack Router 앱을 배포하기 전에 다음을 확인합니다.

기능

  • 모든 탐색 컴포넌트가 라우터 상태와 함께 작동합니다.
  • 탭과 탐색에 활성 상태가 올바르게 반영됩니다.
  • TypeScript 컴파일이 성공합니다.
  • 모든 MUI 컴포넌트가 올바르게 렌더링됩니다.

성능

  • 트리 셰이킹으로 번들 크기를 최적화합니다.
  • Emotion CSS-in-JS 성능이 허용 가능한 수준입니다.
  • 라우트 변경 시 불필요한 재렌더링이 없습니다.
  • 대형 컴포넌트가 적절하게 코드 분할됩니다.

스타일

  • 모든 라우트에서 테마가 일관됩니다.
  • CSS 충돌이 해결되었습니다.
  • 반응형 디자인이 올바르게 작동합니다.
  • 다크 모드가 통합되어 있습니다(해당하는 경우).

접근성

  • 키보드 탐색이 작동합니다.
  • 스크린 리더 호환성이 유지됩니다.
  • 라우트 전환에서 포커스가 관리됩니다.
  • ARIA 라벨과 역할이 올바르게 설정됩니다.