본문으로 건너뛰기

TanStack Router와 Chakra UI 통합 방법

이 가이드에서는 테마 구성과 반응형 접근성 컴포넌트 생성을 포함해 TanStack Router와 함께 Chakra UI를 설정하는 방법을 설명합니다.

빠른 시작

예상 시간: 3040분
난이도: 초급
중급
사전 요구 사항: 기존 TanStack Router 프로젝트

수행할 작업

  • TanStack Router와 함께 Chakra UI를 설치하고 구성합니다.
  • 테마 provider와 사용자 지정 테마를 설정합니다.
  • 타입 안전한 라우터 호환 Chakra 컴포넌트를 만듭니다.
  • 반응형 내비게이션 패턴을 구현합니다.
  • 라우터 통합으로 접근성 UI 컴포넌트를 구축합니다.

설치 및 설정

1단계: Chakra UI 의존성 설치

npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion

2단계: Chakra Provider 설정

// src/components/chakra-provider.tsx
import { ChakraProvider, extendTheme, type ThemeConfig } from '@chakra-ui/react'
import { ReactNode } from 'react'

// Extend the theme with custom colors and configurations
const config: ThemeConfig = {
initialColorMode: 'light',
useSystemColorMode: true,
}

const theme = extendTheme({
config,
colors: {
brand: {
50: '#e3f2fd',
100: '#bbdefb',
200: '#90caf9',
300: '#64b5f6',
400: '#42a5f5',
500: '#2196f3',
600: '#1e88e5',
700: '#1976d2',
800: '#1565c0',
900: '#0d47a1',
},
},
fonts: {
heading: 'Inter, sans-serif',
body: 'Inter, sans-serif',
},
components: {
Button: {
defaultProps: {
colorScheme: 'brand',
},
},
Link: {
baseStyle: {
_hover: {
textDecoration: 'none',
},
},
},
},
})

interface ChakraAppProviderProps {
children: ReactNode
}

export function ChakraAppProvider({ children }: ChakraAppProviderProps) {
return <ChakraProvider theme={theme}>{children}</ChakraProvider>
}

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

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

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

라우터 호환 컴포넌트 만들기

1단계: 라우터 호환 Chakra 컴포넌트 만들기

// src/components/ui/chakra-router-link.tsx
import { createLink } from '@tanstack/react-router'
import { Link as ChakraLink, Button, IconButton } from '@chakra-ui/react'
import { forwardRef } from 'react'

// Router-compatible Chakra Link
export const RouterLink = createLink(
forwardRef<HTMLAnchorElement, any>((props, ref) => {
return <ChakraLink ref={ref} {...props} />
}),
)

// Router-compatible Chakra Button
export const RouterButton = createLink(
forwardRef<HTMLButtonElement, any>((props, ref) => {
return <Button ref={ref} as="button" {...props} />
}),
)

// Router-compatible Chakra IconButton
export const RouterIconButton = createLink(
forwardRef<HTMLButtonElement, any>((props, ref) => {
return <IconButton ref={ref} as="button" {...props} />
}),
)

2단계: 내비게이션 컴포넌트 만들기

// src/components/navigation/chakra-nav.tsx
import { useMatchRoute } from '@tanstack/react-router'
import {
Box,
Flex,
HStack,
IconButton,
useDisclosure,
useColorModeValue,
Stack,
Collapse,
} from '@chakra-ui/react'
import { HamburgerIcon, CloseIcon } from '@chakra-ui/icons'
import { RouterLink } from '@/components/ui/chakra-router-link'

interface NavItem {
label: string
to: string
exact?: boolean
}

interface ChakraNavProps {
items: NavItem[]
brand?: string
brandTo?: string
}

export function ChakraNav({
items,
brand = 'Logo',
brandTo = '/',
}: ChakraNavProps) {
const { isOpen, onToggle } = useDisclosure()
const matchRoute = useMatchRoute()

return (
<Box>
<Flex
bg={useColorModeValue('white', 'gray.800')}
color={useColorModeValue('gray.600', 'white')}
minH="60px"
py={{ base: 2 }}
px={{ base: 4 }}
borderBottom={1}
borderStyle="solid"
borderColor={useColorModeValue('gray.200', 'gray.900')}
align="center"
>
<Flex
flex={{ base: 1, md: 'auto' }}
ml={{ base: -2 }}
display={{ base: 'flex', md: 'none' }}
>
<IconButton
onClick={onToggle}
icon={
isOpen ? <CloseIcon w={3} h={3} /> : <HamburgerIcon w={5} h={5} />
}
variant="ghost"
aria-label="Toggle Navigation"
/>
</Flex>

<Flex flex={{ base: 1 }} justify={{ base: 'center', md: 'start' }}>
<RouterLink
to={brandTo}
fontFamily="heading"
fontWeight="bold"
fontSize="xl"
color={useColorModeValue('gray.800', 'white')}
>
{brand}
</RouterLink>

<Flex display={{ base: 'none', md: 'flex' }} ml={10}>
<DesktopNav items={items} />
</Flex>
</Flex>
</Flex>

<Collapse in={isOpen} animateOpacity>
<MobileNav items={items} />
</Collapse>
</Box>
)
}

function DesktopNav({ items }: { items: NavItem[] }) {
const matchRoute = useMatchRoute()
const linkColor = useColorModeValue('gray.600', 'gray.200')
const linkHoverColor = useColorModeValue('gray.800', 'white')

return (
<HStack spacing={4}>
{items.map((item) => {
const isActive = matchRoute({ to: item.to, fuzzy: !item.exact })

return (
<RouterLink
key={item.to}
to={item.to}
p={2}
fontSize="sm"
fontWeight={isActive ? 'bold' : 'medium'}
color={isActive ? 'brand.500' : linkColor}
_hover={{
textDecoration: 'none',
color: linkHoverColor,
}}
>
{item.label}
</RouterLink>
)
})}
</HStack>
)
}

function MobileNav({ items }: { items: NavItem[] }) {
const matchRoute = useMatchRoute()

return (
<Stack
bg={useColorModeValue('white', 'gray.800')}
p={4}
display={{ md: 'none' }}
>
{items.map((item) => {
const isActive = matchRoute({ to: item.to, fuzzy: !item.exact })

return (
<RouterLink
key={item.to}
to={item.to}
py={2}
fontWeight={isActive ? 'bold' : 'medium'}
color={
isActive ? 'brand.500' : useColorModeValue('gray.600', 'gray.200')
}
_hover={{
textDecoration: 'none',
}}
>
{item.label}
</RouterLink>
)
})}
</Stack>
)
}

3단계: 브레드크럼 내비게이션 만들기

// src/components/navigation/chakra-breadcrumb.tsx
import { useRouter } from '@tanstack/react-router'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbSeparator,
} from '@chakra-ui/react'
import { ChevronRightIcon } from '@chakra-ui/icons'
import { RouterLink } from '@/components/ui/chakra-router-link'

interface BreadcrumbConfig {
[key: string]: string
}

interface ChakraBreadcrumbProps {
config?: BreadcrumbConfig
separator?: React.ReactElement
}

export function ChakraBreadcrumb({
config = {},
separator = <ChevronRightIcon color="gray.500" />,
}: ChakraBreadcrumbProps) {
const router = useRouter()
const pathSegments = router.state.location.pathname.split('/').filter(Boolean)

if (pathSegments.length === 0) return null

const breadcrumbItems = pathSegments.map((segment, index) => {
const path = '/' + pathSegments.slice(0, index + 1).join('/')
const label =
config[segment] || segment.charAt(0).toUpperCase() + segment.slice(1)
const isLast = index === pathSegments.length - 1

return {
path,
label,
isLast,
}
})

return (
<Breadcrumb spacing="8px" separator={separator}>
<BreadcrumbItem>
<BreadcrumbLink as={RouterLink} to="/">
Home
</BreadcrumbLink>
</BreadcrumbItem>

{breadcrumbItems.map(({ path, label, isLast }) => (
<BreadcrumbItem key={path} isCurrentPage={isLast}>
<BreadcrumbLink
as={isLast ? 'span' : RouterLink}
to={isLast ? undefined : path}
color={isLast ? 'gray.500' : undefined}
>
{label}
</BreadcrumbLink>
</BreadcrumbItem>
))}
</Breadcrumb>
)
}

반응형 디자인 패턴

1단계: 반응형 레이아웃 컴포넌트 만들기

// src/components/layout/chakra-layout.tsx
import { ReactNode } from 'react'
import {
Box,
Container,
Flex,
useColorModeValue,
VStack,
useBreakpointValue,
} from '@chakra-ui/react'
import { ChakraNav } from '@/components/navigation/chakra-nav'
import { ChakraBreadcrumb } from '@/components/navigation/chakra-breadcrumb'

interface ChakraLayoutProps {
children: ReactNode
showBreadcrumb?: boolean
maxWidth?: string
}

const navItems = [
{ label: 'Home', to: '/', exact: true },
{ label: 'Posts', to: '/posts' },
{ label: 'About', to: '/about' },
{ label: 'Contact', to: '/contact' },
]

export function ChakraLayout({
children,
showBreadcrumb = true,
maxWidth = 'container.xl',
}: ChakraLayoutProps) {
const containerPadding = useBreakpointValue({ base: 4, md: 6 })

return (
<Box minH="100vh" bg={useColorModeValue('gray.50', 'gray.900')}>
<ChakraNav items={navItems} brand="My App" />

<Container maxW={maxWidth} py={containerPadding}>
{showBreadcrumb && (
<Box mb={6}>
<ChakraBreadcrumb />
</Box>
)}

<Box>{children}</Box>
</Container>
</Box>
)
}

2단계: 반응형 카드 그리드 만들기

// src/components/ui/chakra-card-grid.tsx
import { ReactNode } from 'react'
import { SimpleGrid, Box, useBreakpointValue } from '@chakra-ui/react'

interface ChakraCardGridProps {
children: ReactNode
minChildWidth?: string
spacing?: number
}

export function ChakraCardGrid({
children,
minChildWidth = '300px',
spacing = 6,
}: ChakraCardGridProps) {
const columns = useBreakpointValue({
base: 1,
md: 2,
lg: 3,
xl: 4,
})

return (
<SimpleGrid
columns={columns}
spacing={spacing}
minChildWidth={minChildWidth}
>
{children}
</SimpleGrid>
)
}

전체 사용 예제

1단계: 게시물 목록 페이지

// src/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import {
Box,
Card,
CardBody,
CardHeader,
Heading,
Text,
Badge,
VStack,
HStack,
useColorModeValue,
} from '@chakra-ui/react'
import { ChakraLayout } from '@/components/layout/chakra-layout'
import { ChakraCardGrid } from '@/components/ui/chakra-card-grid'
import { RouterLink, RouterButton } from '@/components/ui/chakra-router-link'

export const Route = createFileRoute('/posts/')({
component: PostsPage,
})

function PostsPage() {
const posts = [
{
id: '1',
title: 'Getting Started with TanStack Router',
excerpt: 'Learn how to build type-safe routing in React applications.',
category: 'Tutorial',
readTime: '5 min read',
},
{
id: '2',
title: 'Chakra UI Best Practices',
excerpt: 'Tips and tricks for building beautiful UIs with Chakra UI.',
category: 'Design',
readTime: '8 min read',
},
]

return (
<ChakraLayout>
<VStack spacing={8} align="stretch">
<Box>
<Heading size="xl" mb={4}>
Blog Posts
</Heading>
<Text color={useColorModeValue('gray.600', 'gray.400')}>
Discover our latest articles and tutorials
</Text>
</Box>

<Box>
<RouterButton colorScheme="brand" mb={6}>
Create New Post
</RouterButton>

<ChakraCardGrid>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</ChakraCardGrid>
</Box>
</VStack>
</ChakraLayout>
)
}

function PostCard({ post }: { post: any }) {
const cardBg = useColorModeValue('white', 'gray.800')
const cardBorder = useColorModeValue('gray.200', 'gray.700')

return (
<Card
bg={cardBg}
borderColor={cardBorder}
borderWidth="1px"
_hover={{
shadow: 'lg',
transform: 'translateY(-2px)',
transition: 'all 0.2s',
}}
>
<CardHeader pb={3}>
<HStack justify="space-between" align="start">
<Badge colorScheme="brand" variant="subtle">
{post.category}
</Badge>
<Text fontSize="sm" color="gray.500">
{post.readTime}
</Text>
</HStack>
</CardHeader>

<CardBody pt={0}>
<VStack align="start" spacing={3}>
<RouterLink to="/posts/$postId" params={{ postId: post.id }}>
<Heading size="md" _hover={{ color: 'brand.500' }}>
{post.title}
</Heading>
</RouterLink>

<Text color={useColorModeValue('gray.600', 'gray.400')}>
{post.excerpt}
</Text>

<RouterButton
to="/posts/$postId"
params={{ postId: post.id }}
variant="ghost"
colorScheme="brand"
size="sm"
alignSelf="flex-start"
>
Read More →
</RouterButton>
</VStack>
</CardBody>
</Card>
)
}

2단계: 게시물 상세 페이지

// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import {
Box,
Heading,
Text,
VStack,
HStack,
Button,
useColorModeValue,
Divider,
Tag,
TagLabel,
} from '@chakra-ui/react'
import { ArrowBackIcon, EditIcon, DeleteIcon } from '@chakra-ui/icons'
import { ChakraLayout } from '@/components/layout/chakra-layout'
import { RouterButton, RouterLink } from '@/components/ui/chakra-router-link'

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

function PostPage() {
const { postId } = Route.useParams()
const textColor = useColorModeValue('gray.600', 'gray.300')

return (
<ChakraLayout>
<VStack spacing={8} align="stretch">
{/* Back Navigation */}
<Box>
<RouterLink
to="/posts"
color="brand.500"
_hover={{ textDecoration: 'none' }}
>
<HStack spacing={2}>
<ArrowBackIcon />
<Text>Back to Posts</Text>
</HStack>
</RouterLink>
</Box>

{/* Post Header */}
<VStack spacing={4} align="start">
<Heading size="2xl">Understanding TanStack Router</Heading>

<HStack spacing={3}>
<Tag colorScheme="brand">
<TagLabel>Tutorial</TagLabel>
</Tag>
<Text color={textColor}>March 15, 2024</Text>
<Text color={textColor}></Text>
<Text color={textColor}>5 min read</Text>
</HStack>

<Divider />
</VStack>

{/* Post Content */}
<Box>
<VStack spacing={4} align="start">
<Text color={textColor} lineHeight="tall">
This is the detailed content of post {postId}. In this
comprehensive guide, we'll explore how to integrate TanStack
Router with Chakra UI to create beautiful, accessible, and
responsive web applications.
</Text>

<Text color={textColor} lineHeight="tall">
Chakra UI provides a simple, modular, and accessible component
library that gives you the building blocks you need to build React
applications with speed.
</Text>
</VStack>
</Box>

{/* Action Buttons */}
<HStack spacing={4}>
<RouterButton
to="/posts/$postId/edit"
params={{ postId }}
leftIcon={<EditIcon />}
colorScheme="brand"
variant="outline"
>
Edit Post
</RouterButton>

<Button leftIcon={<DeleteIcon />} colorScheme="red" variant="outline">
Delete Post
</Button>
</HStack>
</VStack>
</ChakraLayout>
)
}

일반적인 문제

테마 Provider 문제

문제: 라우트 전반에 Chakra 테마가 올바르게 적용되지 않습니다.

해결 방법: 루트 수준에서 ChakraProvider가 앱 전체를 감싸는지 확인합니다.

// ❌ Don't put provider inside individual routes
export const Route = createFileRoute('/some-route')({
component: () => (
<ChakraProvider>
<SomeComponent />
</ChakraProvider>
),
})

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

라우터 통합 시 TypeScript 오류

문제: TanStack Router와 함께 Chakra 컴포넌트를 사용할 때 TypeScript 오류가 발생합니다.

해결 방법: createLink에 올바른 타입을 사용합니다.

import { createLink } from '@tanstack/react-router'
import { Button, type ButtonProps } from '@chakra-ui/react'
import { forwardRef } from 'react'

// Properly typed router button
export const RouterButton = createLink(
forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
return <Button ref={ref} as="button" {...props} />
}),
)

색상 모드 유지

문제: 라우트가 변경되면 색상 모드가 유지되지 않습니다.

해결 방법: 올바른 색상 모드 관리자를 설정합니다.

import { ColorModeScript } from '@chakra-ui/react'

// Add to your index.html head
;<ColorModeScript initialColorMode={theme.config.initialColorMode} />

// Or use localStorage manager
import { localStorageManager } from '@chakra-ui/react'
;<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
{children}
</ChakraProvider>

반응형 디자인 문제

문제: 반응형 브레이크포인트가 올바르게 작동하지 않습니다.

해결 방법: Chakra의 반응형 유틸리티를 올바르게 사용합니다.

// ✅ Use breakpoint values correctly
const columns = useBreakpointValue({
base: 1,
md: 2,
lg: 3,
xl: 4,
})

// ✅ Or use responsive props
<Box
display={{ base: 'block', md: 'flex' }}
flexDirection={{ base: 'column', md: 'row' }}
>

프로덕션 체크리스트

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

기능

  • 모든 라우터 호환 컴포넌트가 올바르게 작동합니다.
  • 내비게이션 상태가 올바르게 반영됩니다.
  • 라우트 변경 시 테마가 유지됩니다.
  • TypeScript 컴파일이 성공합니다.

접근성

  • 키보드 내비게이션이 작동합니다.
  • 스크린 리더 호환성을 테스트했습니다.
  • 색상 대비가 WCAG 표준을 충족합니다.
  • 포커스 관리가 올바르게 작동합니다.

성능

  • 번들 크기를 최적화했습니다.
  • 색상 모드 전환 성능이 적절합니다.
  • 불필요한 재렌더링이 없습니다.
  • 이미지와 아이콘을 최적화했습니다.

반응형

  • 모바일 기기에서 작동합니다.
  • 태블릿 레이아웃이 기능합니다.
  • 데스크톱 환경이 최적화되어 있습니다.
  • 브레이크포인트가 올바르게 작동합니다.