[React] 중첩 라우터 언제 사용하는가? :: 마이구미
이 글은 React 에서 중첩 라우터를 활용한 예제를 다룬다.
React 에 국한되지 않고, 중첩 라우터는 어떤 경우에 사용하고 이점은 무엇인가? 를 다루게 된다.
react-router-dom v6 을 기준으로 다룬다.
react-router - https://reactrouter.com/docs/en/v6/getting-started/overview
특정 사용자 페이지가 있다. (/user)
그리고 사용자 페이지에는 프로필 정보와 계정 정보를 위한 각각의 탭이 존재한다고 가정해보자.
이것은 단순하게 구현하면 다음과 같다.
const UserPage = () => {
const [tab, setTab] = useState('profile');
return (
<>
<p>User : { name }</p>
<Tab onClick={() => setTab('profile')}>Profile</Tab>
<Tab onClick={() => setTab('account')}>Account</Tab>
<Content>
{ tab === 'profile' ? <Profile /> : <Account /> }
</Content>
</>
)
}
탭에 대한 State 에 따라 컨텐츠 영역에 대한 컴포넌트를 구분하고 있다.
즉, 탭 클릭시 마다 state 를 변경하여 컨텐츠 영역을 교체해줄 수 있다.
이 방식의 문제점은 무엇인가?
지금 탭 State 의 기본값은 "profile" 이라서 사용자 페이지에 접근하면 컨텐츠 영역은 프로필 정보를 나타낸다.
컨텐츠 영역이 프로필이 아닌 계정 정보를 노출하고 싶어도 state 값을 변경해야하기 때문에 현재는 불가능하다.
사용자 페이지에 접근하는 시점에 탭의 상태를 이미 알고 있어야 컨텐츠 영역을 판단할 수 있다.
탭의 상태를 판단하는 도구로는 URL 을 활용할 수 있다.
/user/profile, /user/accout 또는 /user?tab=profile, /user?tab=account 등과 같은 형태이다.
결과적으로 프로필 정보, 계정 정보를 각각 별도 라우터로 구분해주면 된다.
그리고 계정 정보를 보고 싶다면, /user/account 로 직접 접근하면 된다.
const App = () => {
return (
<Routes>
<Route path="/user/profile" element={Profile} />
<Route path="/user/account" element={Account} />
</Routes>
)
}
const Profile = () => {
const [tab, setTab] = useState('profile');
return (
<>
<p>User : { name }</p>
<Tab onClick={() => setTab('profile')}>Profile</Tab>
<Tab onClick={() => setTab('account')}>Account</Tab>
Profile!
</>
)
}
const Account = () => {
const [tab, setTab] = useState('account');
return (
<>
<p>User : { name }</p>
<Tab onClick={() => setTab('profile')}>Profile</Tab>
<Tab onClick={() => setTab('account')}>Account</Tab>
Account!
</>
)
}
이 문제의 문제점은 무엇인가?
Profile, Account 를 컴포넌트를 보면 중복된 코드가 보인다.
그리고 두개는 완전히 다른 영역으로 컨텐츠 영역뿐만 아니라 모든 영역이 교체되는 현상이 발생하게 된다.
우리는 사용자 이름 영역과 탭 영역은 Profile, Account 상관없이 같이 사용하고 컨텐츠 영역만 교체되길 원한다.
이 모든 것을 해결하기 위해 중첩 라우터를 사용할 수 있다.
const App = () => {
return (
<Routes>
<Route path="/user" element={<UserPage />} >
<Route path="profile" element={Profile} />
<Route path="account" element={Account} />
</Route>
</Routes>
)
}
// UserPage.tsx
const UserPage = () => {
reuturn (
<Tabs />
<Outlet /> // path 에 따라 Profile 또는 Account 랜더링
)
}
위 코드는 중첩 라우터를 활용한 코드이다.
탭으로 구성된 페이지를 작업할 때 중첩 라우터 기능을 많이 사용한다.
크게 2가지 이점을 만들어낼 수 있게 된다.
- 중첩 라우터는 결국 URL 이 분리되어 원하는 탭의 상태를 URL 로 구분하여 직접 접근이 가능해진다는 것이다.
- 컨텐츠 이외의 UI는 아무 영향없이 컨텐츠 영역만 교체할 수 있다.
여기까지는 아마 대부분 이해하고 있는 내용이라고 생각한다.
더 나아가 위와 같은 아이디어는 많은 곳에서 활용될 수 있다.
다른 예제로는 모달 형태에서도 적용할 수 있다.
만약 리스트 페이지에 검색 조건을 선택할 수 있는 필터 버튼을 제공하고 있다.
그리고 필터 버튼을 클릭하면 모달 형태의 검색 조건이 노출된다.
일반적으로 이것을 구현한다면, 다음과 같다.
const ListPage = () => {
const [showFilter, setShowFilter] = useState(false);
return (
<div>
<p>ListPage!</p>
<button type="button" onClick={() => setShowFilter(true)}>
Filter
</button>
{showFilter && (
<div className="filterArea">
...Modal
</div>
)}
</div>
);
};
모달이 노출되고 뒤로 가기 버튼을 클릭한다면 어떻게 될까?
안드로이드 폰이라면 취소 버튼을 누르면 어떻게 될까?
당연히 리스트 페이지가 아닌 리스트 페이지의 이전 페이지(홈 or 다른 페이지) 로 이동하게 될 것이다.
/ => /list 로 접근했다면, /list 페이지에서 뒤로가기 한다면, 당연히 / 경로로 이동하는 것이다.
하지만 페이지가 이동하는 것이 아니라 모달이 off 되기를 원한다면?
리스트 페이지는 그대로이면서, 모달이 off 되기를 바란다는 것이다.
이것을 대응하기 위해서는 모달의 on/off 를 체크하여 뒤로 가기 이벤트를 무시하고 모달을 off 해야한다.
이를 위해 필터 영역을 중첩 라우터 기능을 사용하게 되면, 우리는 위와 같은 대응을 신경쓸 필요가 없다.
/list => /list/filter 로 히스토리를 쌓는다면, 뒤로가기의 경우에 /list/filter => /list 로 브라우저의 흐름대로 그대로 따라가게 된다.
기존 브라우저의 히스토리 방식을 그대로 따르는 것이 훨씬 자연스러워진다.
위 예제는 List 와 ListWithoutNestedRoute 로 구분된다.
List 는 중첩 라우터 기능을 사용한 형태이고, ListWithoutNestedRoute 은 아닌 형태이다.
두 곳 모두 접근하여 필터 모달을 띄운 후 뒤로 가기를 해보면 어떤 차이가 있는지 이해할 수 있을 것이다.
* 더 많은 기능과 고도화한다면, 브라우저 history 흐름과 관리 측면에 대한 이해가 더 필요할 수 있다.