React Router

React Router v5 기준으로 일단 설명하고, v6에서 바뀐 점은 밑에서 설명합니다.

React Router란?

React는 SPA를 만드는 것을 도와주는 라이브러리이다. 즉, 페이지가 1개인 애플리케이션을 만든다.
전통적인 웹 애플리케이션이 여러 페이지로 구성되어 있는 것과 완전히 다르다. 전통적인 웹 앱은 유저가 요청할 때마다 페이지가 새로고침되면서 서버로부터 리소스를 전달받아 해석 후 렌더링을 한다. HTML 파일, 혹은 템플릿 엔진 등을 사용하여 애플리케이션의 뷰가 어떻게 보여질지 또한 서버가 담당했다.

그런데 이는 속도적인 측면에서 문제가 있었고, 이를 해소하기 위하여 캐싱과 압축을 하여 서비스를 제공했다. 근데 이것만으로는 사용자와 인터랙션이 많은 모던 웹 애플리케이션에서 충분하지 않았다. 렌더링하는 것을 서버 쪽에서 담당한다는 것은 그만큼 렌더링을 위한 서버 자원이 사용되는 것이고, 불필요한 트래픽도 낭비되기 때문이다.

그래서 우리는 리액트 같은 라이브러리를 사용해서 뷰 렌더링을 유저의 브라우저가 담당하도록 하고, 우선 애플리케이션을 브라우저에 로드한 다음에 정말 필요한 데이터만 서버로부터 전달받아서 보여준다.

그런데 앞에서 말했듯, SPA 즉, 싱글 페이지 애플리케이션이라고 해서 한 종류의 화면만 보여주는 것은 아니다. (아닐 수 있고, 아니어야 한다.) 여러 개의 화면이 있고, 이 화면에 따른 주소도 있다. 주소가 있어야 유저들이 북마크를 할 수도 있고 구글을 통해 유입될 수가 있다.

주소에 따라 다른 뷰를 보여주는 것을 라우팅이라고 한다. 리액트 자체적으로 이 기능을 지원하지 않는다. 따라서 따로 라이브러리를 설치하여야 한다.

react-router는 페이스북(메타)에서 제공하는 공식 라우팅 라이브러리(존재X)는 아니지만 가장 많이 사용되고 있는 라이브러리이다. 이 라이브러리는 클라이언트 사이드에서 이루어지는 라우팅을 간단하게 해주고, 서버 사이드 렌더링 또한 도와준다. 그리고 react-native에서도 사용될 수 있다. (Web에서 사용 시 react-router-dom 설치, App에서 사용 시 react-router-native 설치)

여러 화면으로 구성된 웹 애플리케이션을 만든다면 react-router는 필수 라이브러리이다.

리액트 라우터는 눈속임이다. 실제로 페이지가 바뀌는 것이 아니라 바뀐 척을 하는 것이다. 화면만 바꾸는 것이다.

SPA의 단점

SPA는 앱의 규모가 커지면 자바스크립트 파일 사이즈가 너무 커진다는 단점이 있다. 유저가 실제로 방문하지 않을 수도 있는 페이지에 관련된 렌더링 관련 스크립트도 불러오기 때문이다. 하지만 Code Splitting을 사용한다면 라우트별로 파일들을 나눠서 트래픽과 로딩 속도를 개선할 수 있다.

Route

사용자가 요청하는 주소에 따라 다른 컴포넌트를 보여주기 위해 Route라는 컴포넌트를 사용한다.

Link 컴포넌트는 클릭하면 다른 주소로 이동시키는 컴포넌트이다.

리액트 사용 시에는 일반 <a> 태그를 사용하면 안 된다. 그 이유는 a 태그 사용시 페이지를 이동시키면서 페이지를 아예 새로 불러오므로(새로고침 발생) 리액트 앱이 지니고 있는 상태들도 초기화되고, 렌더링된 컴포넌트 또한 모두 사라지고 새로 렌더링을 하게 된다. 그렇기에 Link 컴포넌트를 사용하고, 이 컴포넌트는 HTML5 History API를 사용하여 브라우저의 주소만 바꿀 뿐 페이지를 새로 불러오지는 않는다.


NavLink 컴포넌트는 기본적으로는 Link와 동일한데 설정한 URL이 활성화가 되면 특정 스타일 혹은 클래스를 지정할 수 있다.

activeStyle 혹은 activeClassName prop을 활용하여 링크가 활성화되었을 때 스타일과 클래스를 적용할 수 있다.

location, match, history

Route로 설정한 컴포넌트는 3가지의 props를 전달받게 된다.

history 객체를 통해 다른 경로로 이동하거나 앞 뒤 페이지로 전환(replace, push)할 수 있다.
location 객체는 현재 경로에 대한 정보를 지니고 있고, 쿼리 (/about?foo=bar 형식) 정보도 가지고 있다. match 객체에는 어떤 라우트에 매칭이 되었는지에 대한 정보가 있고, 파라미터(params) (/about/:name 형식) 정보를 가지고 있다.

Route 컴포넌트가 아닌 곳에서 location, match, history 사용하는 방법

  1. withRouter HOC를 이용하여 컴포넌트를 감싸준다.
  2. React Router hooks를 사용한다. (추천)
    React Router v5.1부터 추가되었다.

    • useHistory (history)
    • useLocation (location)
    • useRouteMatch (match)
    • useParams (match.params)

Route인 컴포넌트의 경우에는 기본적으로 props 객체에 들어있긴 하나 Route 컴포넌트인 곳에서 hooks를 이용하여 사용해도 된다.

동적 라우팅 (파라미터와 쿼리)

페이지 주소를 정의 할 때, 우리는 유동적인 값을 전달해야 할 때도 있다. 이는 파라미터와 쿼리로 나뉘어질 수 있다.

scheme://host:port/path?query#fragment

파라미터

  • 파라미터(= Params)는 /:name, 즉 : 뒷 부분에 해당한다. (path 부분에 해당)
  • 파라미터는 match.params에 들어있다.

쿼리

  • 쿼리(= 쿼리스트링 = 쿼리 파라미터)는 ?query, 즉 ? 뒷 부분에 해당한다.
  • 쿼리는 location.search에 들어있는데 이걸 파싱(문자열->객체)하기 위해서 제공해주는 건 없고, 브라우저에서 제공하는 URLSearchParams를 이용해서 파싱한다.
    공식 문서 - Query Parameters 예제에서도 new URLSearchParams를 이용하여 파싱하는 것을 볼 수 있다.
  • 여러 개일 경우 &로 연결한다. ?query=10&hello=seona&bye=naseo
  • 쿼리에서 =로 연결된 key-value pair를 통해 값을 읽어낸다.
  • new URLSearchParams(쿼리).get(key) 이렇게 하면 key에 해당하는 value를 얻을 수 있다.
    즉, 위의 예시에서 new URLSearchParams(?query=10&hello=seona&bye=naseo).get(query)를 하면 10,
    new URLSearchParams(?query=10&hello=seona&bye=naseo).get(hello)를 하면 seona,
    new URLSearchParams(?query=10&hello=seona&bye=naseo).get(bye)를 하면 naseo를 얻을 수 있다.

일반적으로 파라미터는 특정 ID나 이름을 가지고 조회할 때 사용하고, ex) 게시글 카테고리
쿼리는 어떤 키워드를 검색할 때, 요청 시 필요한 옵션을 전달할 때(부가적인 정보 전달할 때) 사용한다. ex) 게시글 목록 페이지 (?page=1)

추가 정보

path와 쿼리스트링은 서버도 안다. #는 해시라고 하는데 이 정보는 서버는 모르고 브라우저만 안다.

Code Splitting

리액트 프로젝트 코드 스플리팅 정복하기

React Router Detail

render 패턴과 component 패턴

render 패턴은 props를 넘겨줘야 한다. props 안에 location, match, history가 들어 있다.

<Route path="/survey" render={(props) => <Survey {...props} />} />
<Route path="/typing-shop" component={TypingShop} />

Switch와 Route

Switch 안에 있으면 첫 번째로 일치하는 Route만 렌더링된다.

(만약 Switch 없었다면 일치하는 Route 화면에 순서대로 주르륵 다 나옴)

exact

exact를 붙이면 이 주소와 정확하게 일치하는 경우에만 렌더링된다.

Route가 이렇게 있다고 쳐보자.

<Route path="/" component={Landing} />
<Route path="/survey" render={(props) => <Survey {...props} />} />

그러면 /survey로 접속했다고 해도 Landing 컴포넌트와 Survey 컴포넌트가 위 아래 순서대로 화면에 보인다.
왜냐면 기본적으로 /도 들어 있기 때문에 일치한다고 생각한다.
즉, 상위 주소도 항상 일치한다고 생각한다.

<Switch>
  <Route path="/" component={Landing} />
  <Route path="/survey" render={props => <Survey {...props} />} />
</Switch>

이럴 때는 Switch로도 해결이 안 된다.
왜냐면 첫 번째로 만나는 path가 /인데 /survey 주소로 접근해도 일치한다고 생각해서 Landing 컴포넌트를 보여주는 Route를 렌더링한다.
물론 이 경우에는 Survey 컴포넌트가 보이진 않는다. Switch는 첫 번째로 일치하는 단 하나의 Route만 렌더링하니까.
그러나 원하는 동작이 아닌 건 마찬가지다.

이렇게 해결해야 한다.

<Switch>
  <Route exact path="/" component={Landing} />
  <Route path="/survey" render={props => <Survey {...props} />} />
</Switch>

Switch를 써도 문제 해결이 안 되면 exact도 붙이자.

React Router v6

모든 내용은 React Router v5 → v6 빠르게 훑어보기를 참고하여 작성하였습니다.

1. Switch 대신 Routes

Switch가 사라지고, Routes가 그 자리를 대신한다.

2. useHistory 대신 useNavigate

또한 navigate는 객체가 아니고, 함수인 점이 다르다.
그렇기에 history.push("주소")history.goBack() 대신 navigate("주소")navigate(-1)로 함수를 호출하면 된다.
2번 뒤로 가고 싶다면 history.go(-2) 대신 navigate(-2)를 사용하면 된다.

3. useRouteMatch가 사라진 대신 상대 경로를 쓸 수 있게 됐다.

현재 경로에 기반하여 Link나 Route를 설정하기 위해 match.url이나 match.path를 사용했고, 이를 위해 useRouteMatch를 사용했었다.

근데 상대 경로를 설정할 수 있게 됨에 따라 이 match 값이 더 이상 필요하지 않아졌다.

따라서 ${match.url} 대신 단순히 공백("")으로 두면 현재 match된 Route의 경로가 사용된다.
주의해야 할 점은 ${match.url}/about/about이 아닌 about으로 해야 한다. /about으로 할 시 단순히 about 페이지로 이동하게 된다. about으로 해야만 현재 Route에 매치된 그 경로 뒷 부분에 about을 추가한 경로로 이동하게 된다.

4. Route에 render나 component 혹은 children으로 넣어주던 것 대신 element 사용

5. Route는 Routes의 직속 자식이어야 한다.


따라서 3, 4, 5번을 모두 적용하여 변경된 코드는 다음과 같다.

// v5
<div>
  <Link to={`${match.url}`} style={{ marginRight: 16 }}>
    @{username}
  </Link>
  <Link to={`${match.url}/about`}>About</Link>
</div>
<Route path={`${match.path}`} exact>
  <UserMain />
</Route>
<Route path={`${match.path}/about`}>
  <About />
</Route>
// v6
<div>
  <Link to="" style={{ marginRight: 16 }}>
    @{username}
  </Link>
  <Link to="about">About</Link>
</div>
<Routes>
  <Route path="" element={<UserMain />} />
  <Route path="about" element={<About />} />
</Routes>

6. Route에 exact Prop이 사라졌다. 서브 경로가 필요한 경우 path에 * 사용

예전에 있던 exact의 속성을 모든 Route가 기본적으로 가지고 있다고 보면 된다.
exact가 필요하지 않은 경우에만 *를 붙여준다.

// v5
<Route path="/users/:username">
  <User />
</Route>
<Route path="/users/:username/*" element={<User />} />

7. 서브 라우트를 구현하는 또 다른 방법, Outlet

Outlet을 이용해 더 깔끔하게 서브라우트를 구현할 수도 있다.

// 이전 App.js
<Route path="/users/:username/*" element={<User />} />
// 이전 User.js
<div>
  <Link to="" style={{ marginRight: 16 }}>
    @{username}
  </Link>
  <Link to="about">About</Link>
</div>
<Routes>
  <Route path="" element={<UserMain />} />
  <Route path="about" element={<About />} />
</Routes>
// 이후 App.js
<Route path="/users/:username/*" element={<User />}>
  <Route path="" element={<UserMain />} />
  <Route path="about" element={<About />} />
</Route>
// 이후 User.js
<div>
  <Link to="" style={{ marginRight: 16 }}>
    @{username}
  </Link>
  <Link to="about">About</Link>
</div>
<Outlet />

9. Optional URL 파라미터 (?)가 사라졌다. 필요하면 Route를 2개 만들어야 한다.

// v5
<Route path="/optional/:value?">
  <Optional />
</Route>
// v6
<Route path="/optional/:value" element={<Optional />} />
<Route path="/optional" element={<Optional />} />

10. NavLink에 activeStyle, activeClassName가 사라졌다.

대신에 style이나 className에 함수 타입의 값을 넣어줘야 한다.

// 이전 style
<NavLink
  to="/message"
  style={{ color: 'blue' }}
  activeStyle={{ color: 'green' }}
>
  Messages
</NavLink>
// 이전 class
<NavLink to="/message" className="nav-link" activeClassName="activated">
  Messages
</NavLink>
// 이후 style
<NavLink
  to="/message"
  style={({ isActive }) => ({ color: isActive ? 'green' : 'blue' })}
>
  Messages
</NavLink>
// 이후 class
<NavLink
  to="/message"
  className={({ isActive }) => 'nav-link' + (isActive ? ' activated' : '')}
>
  Messages
</NavLink>

Backwards Compatibility Package

React-Router 팀에서는 이 패키지를 개발 중이라고 한다. 이 패키지를 사용하면 최소한의 코드 변경으로 v5 API를 사용하던 프로젝트를 v6로 업그레이드할 수 있다고 한다.

따라서 현재 React-Router v5를 사용하는 복잡한 애플리케이션은 직접 업그레이드 버전으로 수정하지 말고 이 패키지의 개발을 기다리는 것이 좋다.

참고


Written by정선아
🌱 공부한 것을 기록하여 성장하기 위한 블로그입니다.

GitHubGmail