React Router, Outlet

sumjo
|2024. 1. 29. 08:13

과거의 웹사이트는 페이지가 바뀔때마다 새로운 페이지를 로드해야 했다.

직관적으로 생각해봐도 사용자 경험을 저하시킬 수 밖에 없다.

싱글 페이지 애플리케이션(spa)는 이를 개선하기 위해 만들어졌다.

전체 페이지를 갱신하는 것이 아니라 필요한 부분만 랜더링하여 갱신해준다.

 

라우터는 사용자가 입력한 URL에 따라 필요한 데이터를 보여주는 역할을 한다.

근데, 앞서 말했듯이 경로마다 새로운 페이지를 가져오는 것이 사용자 경험을 저하시킨다면,

싱글 페이지 애플리케이션의 특성을 살려서, 모든 요소를 랜더링으로 바꾸면 라우터를 쓸 필요도 없지 않나?

맞는 말이지만, 그러면 URL이 하나이기 새로고침 하면 초기 페이지로 이동하고, 뒤로가기도 사용하기 힘들다.

때문에 (프로젝트 관리와 유지를 위해서라도) 경로를 나누어 웹페이지를 구성할 필요가 있다.

그리고 이를 라우팅 이라고 한다. 

덧붙이자면 우리가 사용할 라우터 또한 사실 페이지 경로에 따라 필요한 컴포넌트만 랜더링 해서 보여주는 것이다.

 

리액트는 기본적으로 html파일이 하나만 있는 SPA 라이브러리이고 내장된 route 기능이 없다.

그래서 라우팅을 위해서는 다른 라이브러리가 필요한데, react-router-DOM 라이브러리를 사용해볼것이다.

 npm install react-router-dom

react-router-dom 을 설치해준다.

 

먼저 최상단을 <BrowserRouter> 태그로 감싸주어야 한다.

index.tsx

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

 

 

App.tsx

import { Route, Routes } from 'react-router-dom';

return (
    <Routes>
      <Route element={<Container />}>
        <Route path={MAIN_PATH()} element={<Main />} / >
        <Route path={AUTH_PATH()} element={<Authentication />} / >
        <Route path={SEARCH_PATH(':searchWord')} element={<Search />} / >
        <Route path={USER_PATH(':userEmail')} element={<UserP />} / >
        <Route path={BOARD_PATH()}>
          <Route path={BOARD_WRITE_PATH()} element={<BoardWrite/>}/>
          <Route path={BOARD_DETAIL_PATH(':boardNumber')} element={<BoardDetail/>}/>
          <Route path={BOARD_UPDATE_PATH('boardNumber')} element={<BoardUpdate/>} />
        </Route> 
      <Route path="*" element={<h1>Not Found</h1>} />
      </Route>
    </Routes>
  );
}

<Routes> 태그는 모든 <Route>의 상위 경로에 존재해야 한다.

<Route> 태그의 인자

path에는 경로를 전달하고 element로는 경로에 따라 랜더링 해줄 엘리멘트(화면)을 전달한다.

경로를 변수로 바꿔놓은 상태라 알아보기 힘들 수 있는데,

예시를 들자면

MAIN_PATH = http://localhost:3000/

BOARD_PATH() = http://localhost:3000/board

BOARD_WRITE_PATH(:boardNumber) = http://localhost:3000/:boardNumber <- 쿼리스트링

 

이런식으로 경로에 따른 화면을 랜더링 해준다.

 

Outlet

Outlet 태그는 부모 엘리먼트에서 사용해 자식 엘리먼트를 랜더링 하고 싶을 때 사용한다.

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>
  );
}

App에 DashboardMessages와 DashboardTasks를 <Route path="/" element={<Dashboard>} 로 감싸고 있는 것을 볼 수있다.

Dashboard를 보면 div태그 안에 <Outlet /> 을 확인할 수 있다.

해당 요소는 URL이 "/messages"일 때 <DashboardMessages>를 렌더링하고, "/tasks"일 때는 <DashboardTasks>를 렌더링합니다. 만약 URL이 "/"일 경우에는 null을 렌더링합니다.

설명처럼, Outlet 태그 자리에 자식 엘리먼트가 랜더링이 된다고 알아두면 될 듯 하다. 이를 활용해서 Header와 Footer를 랜더링 할 것이다.

 

간단한 원리

깊게 공부한 건 아니지만 뇌피셜을 곁들인 원리를 써보자면

 

1. BrowserRouter는 HistoryAPI를 사용해서 사용자가 방문한 모든 경로를 객체로 만들어 하위 <Routes>에 전달한다.

 

2. HistoryAPI는 stack구조이기 때문에 경로가 바뀌면 stack의 최상단이 변경되면서 <Routes>가 참조하던 객체도 새로운 

것으로 변경된다.

 

3. 이로 인해 Routes 컴포넌트가 리랜더링 되고 하위 라우팅 관련 컴포넌트들도 리랜더링 된다.