본문 바로가기

javascript/react

[React] react-router

 리액트 라이브러리를 이용하면 SPA( single page application ) 형식의 웹페이지를 구현할 수 있다. 이때 SPA는 통상적으로 하나의 html 페이지만 사용하므로, 이에 따라 하나의 페이지에서 동작한다. 그러나 실제 사용자 입장에서 모든 동작을 하나에 페이지 안에서만 수행하는 것은 평범하지 않다. 로그인이 필요하면 로그인 페이지를 방문하고, 블로그에 글을 쓰려면 블로그 페이지를 방문하는 등, 목적에 따라 URL 기반으로 다양한 페이지가 존재하는 것이 좀더 기본적이라고 생각할 수 있다. 

 리액트 프레임워크를 이용하여 만든 웹사이트는 SPA 기반으로 동작하므로, 원칙적으로는 하나의 페이지 내에서 데이터를 유저 및 서버로부터 주고 받으며 동작한다. 이때 리액트가 하나의 페이지 내에서 URL을 변경하면서 마치 다수의 페이지가 있는 것 처럼 동작하도록, 즉 라우팅 기능을 지원하도록 만들어주는 라이브러리가 react-router이다.

 이 글을 쓰는 시점에서, react-router의 최신 버전은 v6 이다. 현재 글 역시 v6을 기준으로 작성한다.

사용법

 먼저 사용할 Router 타입을 라우팅을 사용할 범위에 적용한다. 웹 환경에서는 통상적으로 BrowserRouter을 이용하며, 앱 전반적으로 라우팅을 사용하는 것이 보통이므로 index 파일 내에서 지정하는 경우가 많다.

ReactDOM.render(
    <BrowserRouter>
        <App />
    </BrowserRouter>
, document.getElementById('root'));

BrowserRouter 인터페이스는 브라우저 주소창의 URL 정보 및 history 스택을 내부적으로 처리해준다고 한다.

 이 다음, 실제 라우팅을 할 장소에서 RoutesRoute을 사용하여 실제 라우팅을 수행한다. v5에서는 경로를 찾을 때 Switch 컴포넌트 내부에 지정된 경로들에 대해 위에서 아래 순서로 일치 여부를 검색했으나, v6에서는 주어진 url에 대해 가장 일치하는 경로를 검색한다.

 예를 들어 아래 예시에서 /quotes/123 이라는 경로를 방문하려고 하면, v5에서는 path='/quotes'인 경로가 '/quotes/:quoteId' 에 비해 더 상위에 있으므로 후자를 의도하더라도 특정 설정 없이는 후자에 방문할 수 없었다. 따라서 하위 경로를 상위 경로보다 더 위에 지정하는 등의 귀찮음이 있었다. 그러나 v6으로 업데이트 되면서 라우팅 방식이 위에서 아래 순서로 검사하는 방식에서 주어진 path와 가장 적합한 경로를 선택하는 방식으로 변경되었기 때문에 이러한 귀찮음이 사라졌다.

 경로를 동적으로 만들고 싶다면 ' : ' 을 앞에 붙인다. 

function App() {

  return (
    <div>
      <Layout>
        <Routes>{/* Route는 반드시 Routes 컴포넌트의 자식 */}
          <Route path='/' element={<Navigate to='/quotes' />} />
          <Route path='/quotes' element={<AllQuotes />} >
           <Route path='app' element={<AppInfo />}/>
          </Route> 
          {/* 하위 경로는 상대적으로 */}
          <Route path='/quotes/:quoteId/*' element={<QuoteDetail />} />
          <Route path='new-quote' element={<NewQuotes />} />
          <Route path='*' element={ <NotFound/> }/>
        </Routes>
      </Layout>
    </div>
    // 이제 가장 잘 맞는 Route을 선택한다.
  );
}

 Routes 및 Route는 현재 location 기반으로 동작한다. Route 컴포넌트는 하나 하나의 경로가 되며, Routes 컴포넌트는 이러한 경로의 분기를 관리하는 역할을 한다. 이때 v5와는 달리 v6에서는 Route 컴포넌트가 반드시 Routes 컴포넌트의 자식으로만 존재할 수 있다.

 Route 컴포넌트는 path, element, caseSensitive 등의 프로퍼티를 가지며, nested routes 을 구현하기 위한 목적으로 다른 Route 컴포넌트를 가질수 있다. 최상위 Route 컴포넌트의 경로는 절대 경로 또는 상대 경로로 지정될 수 있으며, 자식 컴포넌트인 경우 상위 컴포넌트에서의 경로와 현재 지정된 상대경로가 병합되므로 상대 경로로만 지정되어야 한다. 예를 들어 상위 Route의 path 는 '/app', 하위 경로의 path 는 '123' 으로 설정되었다면, 하위 Route의 실제 주소는 /app/123 이다. 만약 하위에서 '123' 대신 '/123'으로 지정하면 이는 절대 경로를 의미하므로, 라우팅에 실패한다. 이때 하나의 path만 오류가 있어도 전체가 동작하지 않을 수 있으므로, 이에 유의해야 한다.

  • path : 대응되는 경로를 나타낸다. 절대 또는 상대 경로로 지정할 수 있다. ( 상대 경로 지정을 추천 ) 
  • element : 특정 페이지에 대응되는 컴포넌트이다. v6 이전에는 Route 객체의 자식으로 이러한 컴포넌트들을 직접 지정하는 방식이었으나, v6이 되면서 변경되었다. element 프로퍼티의 추가로, 라우팅 및 일반적인 페이지를 위한 컴포넌트를 직접적으로 구분하여 각각을 파악하기 편하다는 장점이 존재한다.
  •  caseSensitive : 주어진 path와 정확히 일치하는 경로에 대해서만 라우팅 하고 싶은 경우에 사용된다.

Nested Routes

/quotes/123 페이지에서는 인용문만이 나타나고, /quotes/123/comments 페이지에서는 기존 인용문에 더해 해당 인용문에 대한 사람들의 댓글이 보이도록 페이지를 만들고 싶은경우를 생각해보자. 후자는 전자의 컴포넌트들에 대해 "댓글" 에 관한 컴포넌트를 추가한 형태일 뿐이므로, 각각을 다른 페이지로 만드는 것은 적합하지 않을 것이다. 이런 경우 Nested Route을 고려해볼 수 있다. Nested Route는 말 그대로 중첩된 라우팅으로, 특정 Route 컴포넌트에게 자식 경로를 둬서 해당 경로를 방문하면 상위 + 하위 컴포넌트들을 모두 보여주도록 만든다.

Nested Route는 크게 2가지 방법으로 구현한다. 

  1.  상위 경로에서 마지막에 * 를 추가하여 모든 경로에 대응되게 만든 후, element로 전달되는 컴포넌트 내부에서 다시 Routes 및 Route을 이용하여 라우팅을 수행한다.
  2.  상위 경로에서 모든 라우팅을 지정한 이후, 상위 경로의 반환값에 <Outlet /> 컴포넌트를 추가한다.

 첫번째 방법을 이용하면 경로를 여러 파일에 분산할 수 있고, 두번째 방법을 이용하면 경로를 하나의 파일 내부에서 일관되게 관리할 수 있다. 다만 Outlet 컴포넌트를 이용하면 상위 및 하위 컴포넌트 간의 데이터 관리가 까다로워질 수 있다는 단점은 있다 ( useOutletContext을 이용하여 하위 컴포넌트의 상태를 관리할 수 있기는 하다. )

1의 방법 :

상위 라우팅의 끝에 * 이 들어가지 않으면 전체 라우팅이 동작하지 않을 수 있으므로 잊지 말자.

//상위 컴포넌트에서의 라우팅
<Routes>
  <Route path='/' element={<Navigate to='/quotes' />} />
  <Route path='/quotes' element={<AllQuotes />} />
  <Route path='/quotes/:quoteId/*' element={<QuoteDetail />} />
  <Route path='new-quote' element={<NewQuotes />} />
  <Route path='*' element={ <NotFound/> }/>
</Routes>

// QuoteDetail 컴포넌트에서의 라우팅
 return (
        <Fragment>
            <HighlightedQuote text={quote.text} author={quote.author} />
            <Routes>
                <Route path='/' element={<LoadComments/>} />
                <Route path='comments' element={<Comments />} />
            </Routes>
        </Fragment>
    );

 

2의 방법 :

<Outlet /> 컴포넌트는 하위 주소에 따라 다른 컴포넌트로 대체된다. 

//상위 컴포넌트에서 전부 라우팅.
<Routes>
  <Route path='/' element={<Navigate to='/quotes' />} />
  <Route path='/quotes' element={<AllQuotes />} />
  <Route path='/quotes/:quoteId' element={<QuoteDetail />} >
    <Route path='/' element={<LoadComments/>} />
    <Route path='comments' element={<Comments />} />
  <Route/>
  <Route path='new-quote' element={<NewQuotes />} />
  <Route path='*' element={ <NotFound/> }/>
</Routes>

//상위 컴포넌트(QuoteDetail)에서는 Outlet 컴포넌트를 포함
 return (
        <Fragment>
            <HighlightedQuote text={quote.text} author={quote.author} />
            <Outlet />
        </Fragment>
    );

 

상위 경로

 

하위 경로. comments가 추가된다.

 

댓글과 관련된 컴포넌트가 추가된다.

 

특정 위치로의 링크

 html 에서 <a> 태그를 이용하면 지정된 url에 따라 해당 주소 사이트로 이동할 수 있다. 이때 리액트는 SPA 기반의 페이지이므로, 하나의 페이지 내에서만 정보가 관리된다. 따라서 <a> 태그 기반의 이동은 리액트가 관리하고 있는 모든 정보와 무관한 페이지로 이동하게 만든다. 따라서 동일 리액트 앱에 대해 이동한다고 하더라도, 기존 페이지와 이동한 페이지는 다른 환경을 가지기 때문에 <a>를 이용해 리액트의 각 페이지를 라우팅할 수는 없다. 따라서 이를 대체할 컴포넌트가 필요한데, Link 및 NavLink가 이 역할을 수행한다.

Link

리액트 페이지 내부에서 이동할 때 이용한다. a 태그와 거의 유사하며, to 프로퍼티를 통해 이동할 경로를 지정할 수 있다. 대부분의 경우 절대 경로를 이용하겠지만, 상대 경로로도 동작하는 것 같다.

<Link className='btn' to={`/quotes/${props.id}`}>
        View Fullscreen
</Link>

NavLink

 네비게이션 항목을 만들 때 현재 위치가 어디인지 지정하는 경우가 있다. NavLink는 이 상황에 사용하며, class 및 style 지정이 Link와 다르다.

 NavLink의 className 및 style에는 함수를 반환할 수 있다. 해당 함수의 구조는 다음과 같다.

({isActive : boolean }) => any // 보통 문자열 반환

 isActive는 현재 페이지가 활성화된 페이지인지 알려준다. 이를 이용하여 적용할 css 스타일을 지정할 수 있다.

<ul>
    <li>
        <NavLink to='/quotes' className={({isActive}) => isActive?styles.active:''}>
            All Quotes
        </NavLink>
    </li>
    <li>
        <NavLink to='/new-quote' className={({isActive}) => isActive?styles.active:''}>
            Add a Quote
        </NavLink>
    </li>
</ul>

 

Redirect 기능

특정 경로가 기존에 존재하는 페이지로 연결되도록 구현하고 싶은경우 우리는 redirect를 사용한다. react-router에서는 이 기능을 Navigate 컴포넌트를 통해 지원한다. 

 <Route path='new-quotes' element={<Navigate to='/new-quote' replace={true} />}/>

Navigate 컴포넌트는 Route 컴포넌트의 element 프로퍼티로 사용된다.

  • to : redirect 할 경로를 가리킨다.
  • replace : redirect를 통해 페이지를 "이동"할지, "대체"할지 결정한다. 이동하면 history 상에 기록이 남지만, 대체하면 기록이 안남는다.

여러가지 hook들

사용경험 향상을 위한 다양한 hook 들이 존재한다.

  • useLocation : 현재 위치와 관련된 정보를 가진 객체를 반환한다. window.location과 거의 유사하다.
  • useParams : URL의 search 정보를 가진 객체를 반환한다. 자동으로 파싱해준다.
  • useSearchParams : URLSearchParams 인터페이스를 쓰기 쉽게 만든 hook이다.
  • useNavigate : 위에서 언급한 Navigate 동작을 직접 수행할 수 있도록 한 hook이다. 전체적으로 window.history의 pushState, replaceState, go 메서드를 섞어놓은 것처럼 동작한다.
const navigate = useNavigate();

navigate(-1); // 인덱스를 전달하면 history 기반 뒤로 가기 / 앞으로 가기
navigate('/home',{replace:true}); // 주소를 전달하면 redirect

 

정리

react-router은 리액트 기반 페이지에서 라우팅이 가능하도록 지원해주는 라이브러리로, Routes 및 Route 컴포넌트를 기본으로 하며, Link 나 Navigate, 여러가지 hook 등을 이용하여 적절하게 라우팅을 묘사할 수 있다.

'javascript > react' 카테고리의 다른 글

[React] 개발 환경 프록시 설정  (0) 2022.12.06
[React] 타입스크립트와 children 사용  (0) 2022.10.01
[React] custom hook  (0) 2021.12.29
[React] react-redux  (0) 2021.12.24
[React] redux에 대한 개념  (0) 2021.12.23