상황
App.tsx에 다양한 경로를 만들었는데 Nav를 추가하게 됐다.
여러 페이지들 중 Home, Recommend, My page에만 Nav를 추가하려고 한다.
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Frame>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/:placeId" element={<DetailPage />} />
<Route path="/location-search" element={<LocationSearch />} />
<Route
path="/recommended-place-search"
element={<RecommendedPlaceSearch />}
/>
<Route path="/search-results" element={<SearchResults />} />
<Route path="/inquiry" element={<Inquiry />} />
<Route path="/survey" element={<Survey />} />
<Route path="/recommend" element={<RecommendPage />} />
<Route path="/mypage" element={<MyPage />} />
</Routes>
</Frame>
</BrowserRouter>
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
/, /recommend, /mypage에만 nav를 추가하려면 react router의 outlet을 사용하면 된다.
Outlet
An <Outlet> should be used in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered. If the parent route matched exactly, it will render a child index route or nothing if there is no index route.
부모 경로 요소에 <Outlet>을 사용해 자식 경로로 렌더링 해야 한다.
이렇게 하면 자식 경로가 렌더링 될 때 중첩된 UI가 표시될 수 있다.
부모 경로가 정확히 일치하면 자식 인덱스 경로를 렌더링하고, 인덱스 경로가 없으면 렌더링 하지 않는다.
라우터의 중첩된 구조에서 사용하는 컴포넌트로, 라우터가 특정 경로에 일치하는 컴포넌트를 렌더링 할 때, 해당 컴포넌트의 하위 컴포넌트를 표시하는 역할을 한다. 그래서 주로 중첩된 라우트에서 사용되며 부모 라우트 컴포넌트 내에서 자식 라우트 컴포넌트를 렌더링 할 때 활용한다.
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* This element will render either <DashboardMessages> when the URL is
"/messages", <DashboardTasks> at "/tasks", or null if it is "/"
*/}
<Outlet />
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<Dashboard />}>
<Route
path="messages"
element={<DashboardMessages />}
/>
<Route path="tasks" element={<DashboardTasks />} />
</Route>
</Routes>
);
}
말보다 공식 문서에 있는 예제를 보면 더 이해가 잘 된다.
경로가 /일 때 /message면 DashboardMessage가 렌더 되고, /tasks면 DashboardTasks가 렌더되는 코드다.
이런 식으로 특정 경로에서 특정 컴포넌트를 렌더해보자!
Nav 만들기
먼저 Nav를 만들어준다. (코드가 좀 지저분하다... 좀 더 고민해봐야함)
1. 버튼은 home, recommend, mypage 3가지가 있어야 함
2. 각 버튼은 (비어 있는 아이콘 || 채워진 아이콘) + 버튼 이름으로 구성되어야 함
3. 그 버튼을 클릭했다는 의미로 클릭 시 비어 있는 아이콘 -> 채워진 아이콘으로 바뀌어야 함 + 이동
4. 호버 시 그 아이콘을 호버했다는 의미로 비어 있는 아이콘 -> 채워진 아이콘으로 바뀌어야 함
5. home이 디폴트이기 때문에 무조건 홈 버튼은 채워진 아이콘이어야 함
useEffect(() => {
setActiveTab(
location.pathname === '/' ? 'home' : location.pathname.replace('/', '')
);
}, [location.pathname]);
이 부분은 3번에 대한 코드다. (아이콘 바꾸기)
컴포넌트가 마운트 되었을 때 /, 즉 루트 경로라면 home이기 때문에 setActiveTab('home')이 된다.
만약 /이 아니라면, 예를 들어 /recommend라면 /를 빈 문자열로 치환해 setActiveTab('recommend')을 만들어준다.
const handleNavigation = (tab: string) => {
navigate(tab === 'home' ? '/' : `/${tab}`);
};
3번의 이동에 대한 코드다.
tab에 따라 navigate를 사용해 경로를 정해준다.
const renderButton = (
tab: string,
ActiveIcon: React.ReactNode,
InactiveIcon: React.ReactNode,
label: string
) => (
<button
className="flex flex-col items-center w-1/3"
onMouseEnter={() => setActiveTab(tab)}
onMouseLeave={() =>
setActiveTab(
location.pathname === '/'
? 'home'
: location.pathname.replace('/', '')
)
}
onClick={() => handleNavigation(tab)}
>
{activeTab === tab ? ActiveIcon : InactiveIcon}
<div>{label}</div>
</button>
);
tab은 어떤 아이콘이 클릭되었는지, Active/InactiveIcon은 각각 비어 있는 아이콘과 채워진 아이콘을 의미한다. label은 그 아이콘의 이름으로 이렇게 renderButton은 4가지 props를 필요로 한다.
<div className="fixed bottom-0 left-1/2 transform -translate-x-1/2 bg-white py-5 border-t-[1px] w-[375px] z-50">
<div className="flex justify-around text-mainTextColor">
{renderButton('home', <GoHomeFill />, <GoHome />, 'Home')}
{renderButton('recommend', <GoHeartFill />, <GoHeart />, 'Recommend')}
{renderButton(
'mypage',
<HiUserCircle />,
<HiOutlineUserCircle />,
'My Page'
)}
</div>
</div>
결론적으로 이렇게 생긴 UI가 만들어진다.
renderButton이 저렇게 props로 주지 않으면 내용 빼고 다 중복되어서 저렇게 바꾸어줬다.
이렇게 Nav를 만들었다.
1번째가 처음 렌더링 될 때 home이 디폴트인 경우고, 2번째가 Recommend를 클릭했을 때다.
Outlet 사용하기
원래 같으면 저 Nav를 App.tsx에 그냥 때려 박으면 모든 경로에 Nav를 사용할 수 있게 되지만, 특정 경로에만 Nav를 사용하려면 위에서 설명했던 대로 Outlet을 사용할 수 있다.
import { Outlet } from 'react-router-dom';
import Nav from '../components/Nav';
const NavLayout = () => {
return (
<>
<Outlet />
<Nav />
</>
);
};
export default NavLayout;
이런 식으로 NavLayout을 만들어줬다.
여기서 Outlet은 페이지에 따라 바뀌는 부분을 말한다.
즉 Home, RecommendPage, Mypage가 페이지에 따라 바뀌는 부분이 된다.
이때 순서를 바꾸면 Nav가 위에 올라가게 되는데, 현재 디자인은 Nav가 Footer처럼 밑에 붙어 있어야 해서 밑에 놨다.
이제 이것을 App.tsx에 넣어주자.
<BrowserRouter>
<Frame>
<Routes>
<Route path="/:placeId" element={<DetailPage />} />
<Route path="/location-search" element={<LocationSearch />} />
<Route
path="/recommended-place-search"
element={<RecommendedPlaceSearch />}
/>
<Route path="/search-results" element={<SearchResults />} />
<Route path="/inquiry" element={<Inquiry />} />
<Route path="/survey" element={<Survey />} />
<Route element={<NavLayout />}>
<Route path="/" element={<Home />} />
<Route path="/recommend" element={<RecommendPage />} />
<Route path="/mypage" element={<Mypage />} />
</Route>
</Routes>
</Frame>
</BrowserRouter>
<Route element={<NavLayout />}> 으로 랩핑한 부분을 보자.
일단 / 경로면 Home이 나오고 위에서 적은 대로 Outlet이 Home이 되니 그 밑의 Nav도 나오게 된다.
경로가 /recommend 라면 Recommend가 Outlet이 되고 그 밑의 Nav도 나오게 된다.
이런 구조로 Home, RecommendPage, Mypage는 Nav를 갖게 되는 반면, 나머지 Route들은 영향을 받지 않게 된다.
약 1년 전에 존안 강사님의 리액트 강의를 들으면서 outlet 파트에서 한 번 졸고(...) 제대로 보지도 않고 어렵다고 넘어갔었는데 이걸 이제 한다. 진작에 이렇게 썼어야 했는데 이전까지 규모가 작거나 header, nav, footer가 딱히 필요하지 않아 하드코딩한 부분들이 있었다. 앞으로 자주 사용해야겠다... 🥹 새로운 거 배워서 좋아!!
📌 참고
Outlet v6.23.1 | React Router
reactrouter.com
'⚛️ React > 🔖 라이브러리' 카테고리의 다른 글
[React] react-hook-form 라이브러리 사용하기 (0) | 2024.02.13 |
---|