Routing libraries
Routing libraries (라우팅 라이브러리)
Material UI 컴포넌트를 Next.js의 Link나 react-router 같은 라우팅 라이브러리와 통합하는 방법을 알려드릴게요. 사용자가 화면을 이동할 때 브라우저 표준 링크 대신 라우터의 기능을 활용하고 싶다면 잘 따라와 보세요.
출처: 문서
본문
기본적으로 내비게이션은 네이티브 <a> 요소로 수행돼요. 예를 들어 Next.js의 Link나 react-router를 사용하도록 커스터마이즈할 수 있어요.
내비게이션 컴포넌트 (Navigation components)
내비게이션을 수행하는 데 사용할 수 있는 두 가지 주요 컴포넌트가 있어요. 가장 일반적인 것은 이름에서 알 수 있듯이 Link예요. 네이티브 <a> 요소를 렌더링하고 href를 속성으로 적용해요.
import Link from '@mui/material/Link';
import Box from '@mui/material/Box';
export default function LinkDemo() {
return (
<Box sx={{ typography: 'body1' }}>
<Link href="/">Link</Link>
</Box>
);
}
또한 버튼이 내비게이션 동작을 수행하게 만들 수도 있어요. 컴포넌트가 ButtonBase를 확장한다면, href prop을 제공하면 링크 모드가 활성화돼요. 예를 들어 Button 컴포넌트를 사용할 때:
import Button from '@mui/material/Button';
export default function ButtonDemo() {
return (
<Button href="/" variant="contained">
Link
</Button>
);
}
전역 테마 Link (Global theme Link)
실제 애플리케이션에서는 네이티브 <a> 요소만으로는 부족한 경우가 많아요. 향상된 Link 컴포넌트를 체계적으로 사용하면 사용자 경험을 개선할 수 있어요. Material UI 테마를 사용하면 이 컴포넌트를 한 번만 구성할 수 있어요. 예를 들어 react-router를 사용할 때:
import { Link as RouterLink, LinkProps as RouterLinkProps } from 'react-router';
import { LinkProps } from '@mui/material/Link';
const LinkBehavior = React.forwardRef<
HTMLAnchorElement,
Omit<RouterLinkProps, 'to'> & { href: RouterLinkProps['to'] }
>((props, ref) => {
const { href, ...other } = props;
// Map href (Material UI) -> to (react-router)
return <RouterLink ref={ref} to={href} {...other} />;
});
const theme = createTheme({
components: {
MuiLink: {
defaultProps: {
component: LinkBehavior,
} as LinkProps,
},
MuiButtonBase: {
defaultProps: {
LinkComponent: LinkBehavior,
},
},
},
});
import * as React from 'react';
import {
Link as RouterLink,
LinkProps as RouterLinkProps,
MemoryRouter,
StaticRouter,
} from 'react-router';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Link, { LinkProps } from '@mui/material/Link';
const LinkBehavior = React.forwardRef<
HTMLAnchorElement,
Omit<RouterLinkProps, 'to'> & { href: RouterLinkProps['to'] }
>((props, ref) => {
const { href, ...other } = props;
// Map href (MUI) -> to (react-router)
return <RouterLink data-testid="custom-link" ref={ref} to={href} {...other} />;
});
function Router(props: { children?: React.ReactNode }) {
const { children } = props;
if (typeof window === 'undefined') {
return <StaticRouter location="/">{children}</StaticRouter>;
}
return <MemoryRouter>{children}</MemoryRouter>;
}
const theme = createTheme({
components: {
MuiLink: {
defaultProps: {
component: LinkBehavior,
} as LinkProps,
},
MuiButtonBase: {
defaultProps: {
LinkComponent: LinkBehavior,
},
},
},
});
export default function LinkRouterWithTheme() {
return (
<Stack spacing={1} sx={{ alignItems: 'center', typography: 'body1' }}>
<ThemeProvider theme={theme}>
<Router>
<Link href="/">Link</Link>
<Button href="/" variant="contained">
Link
</Button>
</Router>
</ThemeProvider>
</Stack>
);
}
:::warning
이 접근 방식은 TypeScript에 제한이 있어요. href prop은 문자열만 허용해요. 더 풍부한 구조를 제공해야 한다면 다음 섹션을 참고하세요.
:::
component prop
component prop으로 제3자 라우팅 라이브러리와 통합을 달성할 수 있어요. 이 prop에 대해 더 알아보려면 **composition guide**를 참고하세요.
React Router 예제 (React Router examples)
다음은 React Router의 Link 컴포넌트를 사용한 몇 가지 데모예요. BottomNavigation, Card 등 모든 컴포넌트에 같은 전략을 적용할 수 있어요.
Link
import * as React from 'react';
import {
Link as RouterLink,
LinkProps as RouterLinkProps,
MemoryRouter,
StaticRouter,
} from 'react-router';
import Link from '@mui/material/Link';
import Box from '@mui/material/Box';
const LinkBehavior = React.forwardRef<any, Omit<RouterLinkProps, 'to'>>(
(props, ref) => (
<RouterLink
ref={ref}
to="/material-ui/getting-started/installation/"
{...props}
/>
),
);
function Router(props: { children?: React.ReactNode }) {
const { children } = props;
if (typeof window === 'undefined') {
return <StaticRouter location="/">{children}</StaticRouter>;
}
return <MemoryRouter>{children}</MemoryRouter>;
}
export default function LinkRouter() {
return (
<Box sx={{ typography: 'body1' }}>
<Router>
<Link component={RouterLink} to="/">
With prop forwarding
</Link>
<br />
<Link component={LinkBehavior}>Without prop forwarding</Link>
</Router>
</Box>
);
}
Button
import * as React from 'react';
import {
Link as RouterLink,
LinkProps as RouterLinkProps,
MemoryRouter,
StaticRouter,
} from 'react-router';
import Button from '@mui/material/Button';
const LinkBehavior = React.forwardRef<any, Omit<RouterLinkProps, 'to'>>(
(props, ref) => <RouterLink ref={ref} to="/" {...props} role={undefined} />,
);
function Router(props: { children?: React.ReactNode }) {
const { children } = props;
if (typeof window === 'undefined') {
return <StaticRouter location="/">{children}</StaticRouter>;
}
return <MemoryRouter>{children}</MemoryRouter>;
}
export default function ButtonRouter() {
return (
<div>
<Router>
<Button component={RouterLink} to="/">
With prop forwarding
</Button>
<br />
<Button component={LinkBehavior}>With inlining</Button>
</Router>
</div>
);
}
참고: ButtonBase 컴포넌트는 네이티브 <button> 요소 없이 버튼을 렌더링하려는 의도를 감지하면 role="button" 속성을 추가해요. 이로 인해 링크를 렌더링할 때 문제가 생길 수 있어요. href, to, component="a" prop 중 하나를 사용하지 않는다면 role 속성을 재정의해야 해요. 위 데모는 spread props 뒤에 role={undefined}를 설정해서 이를 달성해요.
const LinkBehavior = React.forwardRef((props, ref) => (
<RouterLink ref={ref} to="/" {...props} role={undefined} />
));
Tabs
import * as React from 'react';
import Box from '@mui/material/Box';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Typography from '@mui/material/Typography';
import {
MemoryRouter,
Route,
Routes,
Link,
matchPath,
useLocation,
StaticRouter,
} from 'react-router';
function Router(props: { children?: React.ReactNode }) {
const { children } = props;
if (typeof window === 'undefined') {
return <StaticRouter location="/drafts">{children}</StaticRouter>;
}
return (
<MemoryRouter initialEntries={['/drafts']} initialIndex={0}>
{children}
</MemoryRouter>
);
}
function useRouteMatch(patterns: readonly string[]) {
const { pathname } = useLocation();
for (let i = 0; i < patterns.length; i += 1) {
const pattern = patterns[i];
const possibleMatch = matchPath(pattern, pathname);
if (possibleMatch !== null) {
return possibleMatch;
}
}
return null;
}
function MyTabs() {
// You need to provide the routes in descendant order.
// This means that if you have nested routes like:
// users, users/new, users/edit.
// Then the order should be ['users/add', 'users/edit', 'users'].
const routeMatch = useRouteMatch(['/inbox/:id', '/drafts', '/trash']);
const currentTab = routeMatch?.pattern?.path;
return (
<Tabs value={currentTab}>
<Tab label="Inbox" value="/inbox/:id" to="/inbox/1" component={Link} />
<Tab label="Drafts" value="/drafts" to="/drafts" component={Link} />
<Tab label="Trash" value="/trash" to="/trash" component={Link} />
</Tabs>
);
}
function CurrentRoute() {
const location = useLocation();
return (
<Typography variant="body2" sx={{ color: 'text.secondary', pb: 2 }}>
Current route: {location.pathname}
</Typography>
);
}
export default function TabsRouter() {
return (
<Router>
<Box sx={{ width: '100%' }}>
<Routes>
<Route path="*" element={<CurrentRoute />} />
</Routes>
<MyTabs />
</Box>
</Router>
);
}
List
import * as React from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import Box from '@mui/material/Box';
import ListItemButton from '@mui/material/ListItemButton';
import Paper from '@mui/material/Paper';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Divider from '@mui/material/Divider';
import InboxIcon from '@mui/icons-material/Inbox';
import DraftsIcon from '@mui/icons-material/Drafts';
import Typography from '@mui/material/Typography';
import {
Link,
Route,
Routes,
MemoryRouter,
useLocation,
StaticRouter,
} from 'react-router';
function Router(props: { children?: React.ReactNode }) {
const { children } = props;
if (typeof window === 'undefined') {
return <StaticRouter location="/drafts">{children}</StaticRouter>;
}
return (
<MemoryRouter initialEntries={['/drafts']} initialIndex={0}>
{children}
</MemoryRouter>
);
}
interface ListItemLinkProps {
icon?: React.ReactElement<unknown>;
primary: string;
to: string;
}
function ListItemLink(props: ListItemLinkProps) {
const { icon, primary, to } = props;
return (
<ListItemButton component={Link} to={to}>
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
<ListItemText primary={primary} />
</ListItemButton>
);
}
function Content() {
const location = useLocation();
return (
<Typography variant="body2" sx={{ color: 'text.secondary', pb: 2 }}>
Current route: {location.pathname}
</Typography>
);
}
export default function ListRouter() {
return (
<Router>
<Box sx={{ width: 360 }}>
<Routes>
<Route path="*" element={<Content />} />
</Routes>
<Paper elevation={0}>
<List aria-label="main mailbox folders">
<ListItem disablePadding>
<ListItemLink to="/inbox" primary="Inbox" icon={<InboxIcon />} />
</ListItem>
<ListItem disablePadding>
<ListItemLink to="/drafts" primary="Drafts" icon={<DraftsIcon />} />
</ListItem>
</List>
<Divider />
<List aria-label="secondary mailbox folders">
<ListItem disablePadding>
<ListItemLink to="/trash" primary="Trash" />
</ListItem>
<ListItem disablePadding>
<ListItemLink to="/spam" primary="Spam" />
</ListItem>
</List>
</Paper>
</Box>
</Router>
);
}
더 많은 예제 (More examples)
Next.js Pages Router
예제 폴더는 Material UI와 함께 Next.js의 Link 컴포넌트를 사용하기 위한 어댑터를 제공해요.
-
어댑터의 첫 번째 버전은
NextLinkComposed컴포넌트예요. 이 컴포넌트는 스타일이 없으며 내비게이션 처리만 담당해요.hrefprop은 이름 충돌을 피하기 위해to로 이름이 바뀌었어요. react-router의 Link 컴포넌트와 비슷해요.import Button from '@mui/material/Button'; import { NextLinkComposed } from '../src/Link'; export default function Index() { return ( <Button component={NextLinkComposed} to={{ pathname: '/about', query: { name: 'test' }, }} > Button link </Button> ); } -
어댑터의 두 번째 버전은
Link컴포넌트예요. 이 컴포넌트는 스타일이 적용돼요.NextLinkComposed와 함께 Material UI Link 컴포넌트를 사용해요.import Link from '../src/Link'; export default function Index() { return ( <Link href={{ pathname: '/about', query: { name: 'test' }, }} > Link </Link> ); }
TanStack Router
TanStack Router는 createLink 헬퍼 함수를 통해 커스텀 링크를 지원해요. 아래 스니펫은 Material UI Link 컴포넌트를 감싸는 가장 기본적인 구현을 보여줘요. 더 많은 컴포넌트 통합 예제는 TanStack Router—Custom Link를 참고하세요.
import { createLink } from '@tanstack/react-router';
import { Link as MUILink } from '@mui/material';
const CustomLink = createLink(MUILink);
function App() {
return (
<CustomLink underline="none" to="/about">
Link to about page
</CustomLink>
);
}
더 알아보기 (Learn more)
- React Router — 라우팅 라이브러리
- TanStack Router — 라우팅 라이브러리
- Composition guide — component prop