리알못 React: 5. 라우팅(Routing)

Table of Content

이 글은 Code with Mosh의 Mastering React 과정을 공부하며 정리한 글입니다. 리알못이라 이상하거나 틀린 내용이 있을 것입니다…

라우팅(Routing)

라우팅(Routing)이란 경로(URL)에 따라 페이지의 이동을 설정하는 것입니다. 여기서 알아볼 내용은 다음과 같습니다.

  • 페이지 별 URL 설정
  • URL로부터 정보를 추출하기 위한 쿼리 스트링(Query String)
  • 잘못된 URL로 접속 시 404 Not Found 페이지를 띄우기 위한 리디렉션(Redirection)
  • 중첩 라우팅(Nested Routing)

상단 네비게이션 바를 클릭하면 이에 대응하는 컴포넌트가 라우팅 되는 예제입니다.

사전준비

Bootstrap 설치

여기서는 네비게이션 바를 만들기 위해 Bootstrap을 사용합니다. npm i bootstrap 명령어로 React 프로젝트에 Bootstrap을 설치한 후 index.js 파일에 import 'bootstrap/dist/css/bootstrap.css';를 추가합니다.

React Router DOM 설치

React 프로젝트에서 라우팅을 하기 위해선 React Router DOM이라는 라이브러리가 필요합니다. React 프로젝트가 위치한 곳에서 npm i react-router-dom@4.3.1 명령어로 react-router-dom npm 패키지를 설치합니다.

기본적인 라우팅 해보기

화면 상단에 네비게이션 바를, 화면 하단엔 라우팅된 페이지를 렌더링하는 예제를 만들어봅니다.

BrowserRouter 컴포넌트 사용

index.js 파일 내에서 최상위 컴포넌트를 BrowserRouter 태그로 감싸줍니다. 여기서 최상위 컴포넌트는 App 컴포넌트입니다.

/***** index.js *****/

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'; // react-router-dom import
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.css';

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

라우팅할 페이지(컴포넌트) 생성

여기서는 Products, Posts, Admin Dashboard 페이지(컴포넌트)를 각각 만들어 이를 라우팅해보겠습니다. 다음과 같이 대충 만듭니다.

/***** ./components/Products.js*****/

import React from 'react';

const Product = () => {
  return <h1>Product</h1>;
}

export default Product;
/***** ./components/Posts.js*****/

import React from 'react';

const Posts = () => {
  return <h1>Posts</h1>;
}

export default Posts;
/***** ./components/Dashboard.js*****/

import React from 'react';

const Dashboard = () => {
  return <h1>Admin Dashboard</h1>;
}

export default Dashboard;
/***** ./components/Home.js*****/

import React from 'react';

const Home = () => {
  return <h1>Home</h1>;
}

export default Home;

네비게이션 바(Navigation Bar) 생성

Bootstrap을 사용하여 네비게이션 바를 만든 뒤 라우팅된 페이지로 이동할 수 있도록 합니다. 여기선 https://getbootstrap.com/docs/4.3/components/navbar 에 나와있는 예시를 참고하겠습니다.

/***** ./components/NavBar.js *****/

import React from 'react';

const NavBar = () => {
  return (
    <nav className="navbar navbar-expand-lg navbar-light bg-light">
      <a className="navbar-brand" href="/">Home</a>
      <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
        <span className="navbar-toggler-icon"></span>
      </button>
      <div className="collapse navbar-collapse" id="navbarNav">
        <ul className="navbar-nav">
          <li className="nav-item">
            <a className="nav-link" href="/products">Products</a>
          </li>
          <li className="nav-item">
            <a className="nav-link" href="/posts/2019/09">Posts</a>
          </li>
          <li className="nav-item">
            <a className="nav-link" href="/admin" >Admin</a>
          </li>
        </ul>
      </div>
    </nav>
  );
}

export default NavBar;

라우팅

라우팅을 하려는 컴포넌트에서 Route 컴포넌트를 호출해줍니다. Route 컴포넌트는 path props에 정의된 URL에 접속했을 때 component props에 명시한 컴포넌트를 렌더링합니다.

정의된 URL에 접속하지 않았을 때 해당 컴포넌트는 렌더링되지 않습니다.

/***** App.js *****/

import React from "react";
import NavBar from "./components/NavBar";
import Products from "./components/Products";
import Posts from "./components/Posts";
import Dashboard from "./components/Dashboard";
import Home from "./components/Home";
import { Route } from "react-router-dom"; // Route 컴포넌트 imort

const App = () => {
  return (
    <>
      <NavBar />
      <div className="content">
        <Route path="/products" component={Products} />
        <Route path="/posts" component={Posts} />
        <Route path="/admin" component={Dashboard} />
        <Route path="/" component={Home} />
      </div>
    </>
  );
};

export default App;

위 소스코드는 NavBar 컴포넌트에서 a 태그의 href 속성에 정의된 URL이 App 컴포넌트에서 호출한 Route 컴포넌트의 path props에 대응하며 작동합니다.

정확한 라우팅(Exact Routing)

위에서 설명한 라우팅 방법은 어떤 URL을 클릭하든 Home 컴포넌트가 렌더링 됩니다. 그 이유는 Route 컴포넌트에서 path props 값과 일치하는 값이 아닌 시작되는 값이면 컴포넌트를 렌더링하기 때문입니다.

예를 들어 Home 컴포넌트를 구현하는 Route 컴포넌트는 path props 값이 "/" 입니다. 모든 Route 컴포넌트의 path props 값은 "/"으로 시작하므로 어떤 URL을 클릭하든 Home 컴포넌트가 렌더링되는 것입니다.

이 문제를 해결하기 위해 다음 두 가지 방법을 사용할 수 있습니다.

exact props 사용

exact props를 사용하면 path 값이 정확히 일치해야 라우팅됩니다.

  <Route exact path="/" component={Home} /> {/* exact props */}

Switch 컴포넌트 사용

Switch 컴포넌트를 사용하면 Route 컴포넌트의 path props 값과 일치하는 URL에 대응하는 컴포넌트를 렌더링 합니다.

/***** App.js *****/

import React from "react";
import NavBar from "./components/NavBar";
import Products from "./components/Products";
import Posts from "./components/Posts";
import Dashboard from "./components/Dashboard";
import Home from "./components/Home";
import { Route } from "react-router-dom"; // Route 컴포넌트 imort

const App = () => {
  return (
    <>
      <NavBar />
      <div className="content">
        <switch>
          <Route path="/products" component={Products} />
          <Route path="/posts" component={Posts} />
          <Route path="/admin" component={Dashboard} />
          <Route exact path="/" component={Home} />
        </switch>
      </div>
    </>
  );
};

export default App;

라우팅 영역만 새로고침

웹 브라우저 개발자 도구의 Network 탭을 열고 위에서 라우팅한 링크를 여러 번 클릭할 때마다 확인해보면 bundle.js, webpack.js 등외 여러가지 파일을 새로 다운받는 걸 확인할 수 있습니다. 최초 한 번만 모든 정보를 새로 고치면 효율적이지만 매번 모든 정보를 새로 고치는 건 매우 비효율적입니다.

이를 해결하려면 a 태그 대신 React Router DOM에서 제공하는 Link 컴포넌트를 사용하여 일부 영역만 수정토록 하면 됩니다. Link 컴포넌트를 사용하면 a 태그에 onClick 이벤트를 구현하여 앵커의 기본 동작(모든 콘텐츠 새로고침)을 방지합니다.

/***** ./components/NavBar.js *****/

import React from 'react';
import { Link } from 'react-router-dom';

const NavBar = () => {
  return (
    // 콘텐츠 영역만 수정하기 위한 Link 컴포넌트 사용
    // <a href=></a> -> <Link to=></Link>
    <nav className="navbar navbar-expand-lg navbar-light bg-light">
      <Link className="navbar-brand" to="/">Home</Link>
      <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
        <span className="navbar-toggler-icon"></span>
      </button>
      <div className="collapse navbar-collapse" id="navbarNav">
        <ul className="navbar-nav">
          <li className="nav-item">
            <Link className="nav-link" to="/products">Products</Link>
          </li>
          <li className="nav-item">
            <Link className="nav-link" to="/posts/2019/09">Posts</Link>
          </li>
          <li className="nav-item">
            <Link className="nav-link" to="/admin" >Admin</Link>
          </li>
        </ul>
      </div>
    </nav>
  );
}

export default NavBar;

Route 컴포넌트의 Props

위에서 Route 컴포넌트를 구현하는 데 사용한 props는 다음과 같습니다.

  • path: 라우팅할 컴포넌트의 URL
  • component: path props에 지정한 URL 클릭 시 렌더링 할 컴포넌트

이 외에 Route 컴포넌트는 기본적으로 history, location 및 match 등의 props를 지닙니다. 이에 대한 설명은 https://reacttraining.com/react-router/web/guides/quick-start 등을 참고하기 바랍니다.

라우팅되는 컴포넌트에 Props 전달하기

Route 컴포넌트에 의해 렌더링되는 컴포넌트에 props를 전달해야할 경우가 있을 수 있습니다. 이 때 Route 컴포넌트의 render props를 사용하여 라우팅되는 컴포넌트에 props를 전달합니다. 이 때 component props는 사용할 필요가 없습니다.

아래 예제는 Products 컴포넌트에 sortBy라는 props를 전달하여 라우팅하는 예제입니다.

<Route
  path="/products"
  render={() => <Products sortBy="newest" />}
/>

단, 위처럼 Producs 컴포넌트를 구현하면 Products 컴포넌트는 라우팅 관련 props인 history, location 및 match 등이 사라지게 됩니다. 이를 방지하려면 아래와 같이 Spread 연산자(…)를 사용하여 props를 전달해야 합니다.

<Route
  path="/products"
  render={(props) => <Products sortBy="newest" {...props} />}
/>

매개변수

경로 매개변수(Route Parameter) (:)

경로 매개변수(Route Parameter)란 URL에 표시되는 식별 값입니다. 컴포넌트에 전달할 값을 URL에 명시할 수 있습니다. 예를 들어 2019년 9월에 작성한 글의 링크를 클릭했을 때 웹 브라우저 주소 창에 http://localhost:3000/2019/9 로 뜬다면 2019와 9는 경로 매개변수로 볼 수 있습니다.

경로 매개변수임을 명시하려면 Route 컴포넌트의 path props를 명시할 때 경로 매개변수 앞에 콜론(:)을 붙입니다.

<Route path="/products/:id" component={ProductDetails} /> {/* id: 경로 매개변수*/}

라우팅된 컴포넌트 내에서 경로 매개변수 값을 가져오려면 props.match.params.경로_매개변수명을 사용합니다.

/***** ./components/ProductDetails.js *****/

import React from 'react';

const ProductDetails = (props) => {
  return <h1>Product Details: {props.match.params.id}</h1>
}

export default ProductDetails;

선택적 매개변수(Optional Parameters) (?)

2019년 9월에 작성한 글을 보는 링크를 http://localhost:3000/:year/:month 로 구현했다고 가정해봅시다. 그렇다면 월에 상관없이 2019년에 작성한 글을 보는 링크는 http://localhost:3000/:year 가 되는 걸까요? 그렇지 않습니다. 경로 매개변수는 기본적으로 해당 매개변수에 대응하는 값이 반드시 필요하기 때문입니다.

위에서 가정한 문제를 해결하려면 선택적 매개변수(Optional Parameters)를 사용해야 합니다. 선택적 매개변수임을 명시하려면 Route 컴포넌트의 path props를 명시할 때 경로 매개변수 뒤에 JavaScript 정규 표현식의 일부인 물음표(?)를 붙이면 됩니다. ?를 붙이면 해당 매개변수는 선택적인 표현임을 뜻합니다.

<Route path="/posts/:year?/:month?" component={Posts} /> {/* year와 month는 선택적 매개변수 */}

위 처럼 구현하면 year 값만 입력하고 month 값은 입력하지 않으면 Posts 컴포넌트에서 year 값만 입력받을 수 있게 됩니다.

쿼리 문자열 매개변수(Query String Parameters)

선택적 매개변수는 어떤 매개변수 값이 빠질지 애매해지는 경우가 있기 때문에 사용하지 않는게 나을 수 있습니다. 대신 쿼리 문자열 매개변수(Query String Parameters)라는 구조화된 방법을 사용하는 것이 좋습니다.

예를 들어 2019년 9월에 작성한 글 중 공개된 글을 최신순으로 정렬하여 보여주는 URL을 http://localhost:3000/posts/2019/09?sortBy=newest&approved=true와 같이 구현할 수 있습니다.

React Developer Tools 확장 프로그램을 설치한 크롬에서 http://localhost:3000/posts/2019/09?sortBy=newest&approved=true에 접속한 후 개발자 도구로 Posts 컴포넌트의 props.location 값을 열어보면 search값이 ?sortBy=newest&approved=true라는 쿼리 문자열로 설정되어 있습니다. 이 값을 분석 및 추출하여 사용하면 됩니다.

query-string이라는 npm 패키지를 사용하면 위의 search 값을 편하게 추출하여 사용할 수 있습니다. React 프로젝트가 위치한 곳에서 npm i query-string@6.1.0 명령어로 query-string 패키지를 설치한 후 query-string을 사용할 컴포넌트에서 import queryString from "query-string";을 추가하여 다음과 같이 사용하면 됩니다.

/***** ./components/Posts.js *****/

import React from "react";
import queryString from "query-string"; // query-string npm 패키지 import

const Posts = (props) => {
  const result = queryString.parse(props.location.search);
  const { approved, sortBy } = result; // query-string을 이용한 값 접근
  const { year, month } = props.match.params;

  return (
    <div>
      <h1>Posts</h1>
      <p>Year: {year}, Month: {month}</p>
      <p>Approved: {approved}</p>
      <p>SortBy: {sortBy}</p>
    </div>
  );
};

export default Posts;

리디렉션(Redirection)

지금까지의 예제에서 잘못된 URL로 접속 시 App 컴포넌트의 Switch 컴포넌트가 렌더링되지 않습니다. Switch 컴포넌트 내에 구현된 Home 컴포넌트를 구현하는 Route 컴포넌트에 exact라는 props가 명시되어 있기 때문입니다. exact props를 제거한다면 잘못된 URL로 접속 시 Home 컴포넌트가 구현됩니다.

<Switch>
  <Route path="/products" component={Products} /> 
  <Route path="/posts" component={Posts} />
  <Route path="/admin" component={Dashboard} />
  <Route path="/" exact component={Home} />
  {/* 리디렉션에 대한 코드가 없음 */}
</Switch>

잘못된 URL로 접속 시 잘못된 URL로 접속했음을 알려주도록 리디렉션(Rediction)을 해야 합니다. 이를 위해 Redirect 컴포넌트를 사용합니다. Redirect 컴포넌트를 사용하면 특정 URL로 접속 시 다른 URL로 자동 이동할 수 있도록 구현할 수도 있습니다.

/***** App.js *****/

import React from 'react';
import NavBar from './components/NavBar';
import Products from './components/Products';
import ProductsDetail from './components/ProductDetails';
import Posts from './components/Posts';
import Dashboard from './components/Dashboard';
import Home from './components/Home';
import NotFound from './components/NotFound';
import { Route, Switch, Redirect } from 'react-router-dom'; // Redirect 컴포넌트 import
import './App.css';

const App = () => {
  return (
    <>
        <NavBar />
        <div className="content">
          <Switch>
            <Route exact path="/products" component={Products} />
            <Route exact path="/products/:id" component={ProductsDetail} />
            <Route path="/posts/:year?/:month?" component={Posts} /> {/* year와 month는 선택적 매개변수 */}
            <Route path="/admin" component={Dashboard} />
            <Route path="/not-found" component={NotFound} /> {/* NotFound 컴포넌트 */}
            <Route exact path="/" component={Home} />
            <Redirect from="/message" to="/posts" /> {/* 주소/message 로 접속 시 주소/posts 로 리디렉션 */}
            <Redirect to="/not-found" component={NotFound} /> {/* NotFound 컴포넌트로 리디렉션 */}
          </Switch>
        </div>
      </>
  );
};

export default App;

프로그램적인 탐색(Programmatic Navigation)

React Developer Tools 확장 프로그램을 설치한 크롬에서 Route 컴포넌트에 의해 렌더링된 컴포넌트의 props.history 값을 보면 다양한 이벤트 처리기가 있음을 알 수 있습니다.

  • go(), goBack(), goForward(), push(), replace() 등

여기서 헷갈릴만한 이벤트 처리기는 push() 함수와 replace() 함수입니다.

  • push() 함수: 이벤트 처리 후 브라우저 뒤로가기 버튼을 누르면 반응이 있음. browser history를 가지기 때문.
  • replace() 함수: 이벤트 처리 후 브라우저 뒤로가기 버튼을 누르면 반응이 없음. browser history를 가지지 않고 새로 만들기 때문.

이 두 이벤트 처리기는 로그인에서 사용되곤 하는데 push()를 사용하면 뒤로가기 버튼을 클릭했을 때 로그인 페이지가 보입니다. 반면 replace()를 사용하는 경우 뒤로가기 버튼을 클릭했을 때 로그인 페이지가 다시 보이지 않습니다. 어떤 방법을 사용할지는 선택하기 나름인 것 같습니다.

/***** ./components/ProductDetails.js *****/

import React from 'react';

const ProductDetails = (props) => {
  const handleSave = () => {
    props.history.push("/products"); // push() 또는 replace() 사용
  }

  return (
    <>
      <h1>Product Details: {props.match.params.id}</h1>
      <button onClick={handleSave}>Save</button>
    </>
  );
}

export default ProductDetails;

중첩 라우팅(Nested Routing)

중첩 라우팅(Nested Routing)은 Route 컴포넌트에 의해 렌더링되는 컴포넌트를 두 개 이상 동시에 동시에 렌더링하는 것입니다. 예를 들어 Admin 대시보드 컴포넌트를 띄움과 동시에 Admin 관련 정보도 동시에 보여주는 경우가 이에 해당합니다.

중첩 라우팅을 하려면 Route 컴포넌트에 의해 렌더링된 컴포넌트 내에서 한 번 더 Route 컴포넌트를 구현해주면 됩니다. 아래 예제는 App 컴포넌트에서 Dashboard 컴포넌트를 라우팅한 후 Dashboard 컴포넌트에서 추가로 라우팅을 하는 예제입니다. 과정을 정리하자면 다음과 같습니다.

  1. App 컴포넌트에서 Dashboard 컴포넌트를 라우팅
  2. Dashboard 컴포넌트에서 또 다른 컴포넌트 라우팅
  3. 이렇게 구현하면 Dashboard 컴포넌트와 또 다른 컴포넌트가 동시에 렌더링 됨
/***** App.js *****/

import React from 'react';
import NavBar from './components/NavBar';
import Products from './components/Products';
import ProductsDetail from './components/ProductDetails';
import Posts from './components/Posts';
import Dashboard from './components/Dashboard';
import Home from './components/Home';
import NotFound from './components/NotFound';
import { Route, Switch, Redirect } from 'react-router-dom'; // Redirect 컴포넌트 import
import './App.css';

const App = () => {
  return (
    <>
        <NavBar />
        <div className="content">
          <Switch>
            <Route exact path="/products" component={Products} />
            <Route exact path="/products/:id" component={ProductsDetail} />
            <Route path="/posts/:year?/:month?" component={Posts} /> {/* year와 month는 선택적 매개변수 */}
            <Route path="/admin" component={Dashboard} />
            <Route path="/not-found" component={NotFound} /> {/* NotFound 컴포넌트 */}
            <Route exact path="/" component={Home} />
            <Redirect from="/message" to="/posts" /> {/* 주소/message 로 접속 시 주소/posts 로 리디렉션 */}
            <Redirect to="/not-found" component={NotFound} /> {/* NotFound 컴포넌트로 리디렉션 */}
          </Switch>
        </div>
      </>
  );
};

export default App;
/***** ./components/Dashboard.js *****/

import React from 'react';
import Products from './Products'
import Posts from './Posts';
import { Route, Link } from "react-router-dom";

const Dashboard = () => {
  return (
    <>
      <h1>Admin Dashboard</h1>
      <p><Link to="/admin/products">Products</Link></p>
      <p><Link to="/admin/posts">Posts</Link></p>

      {/* 이중 라우팅 */}
      <Route path="/admin/products" component={Products} />
      <Route path="/admin/posts" component={Posts} />
    </>
  );
}

export default Dashboard;

중첩 라우팅 캡처 화면: Dashboard 컴포넌트와 Posts 컴포넌트가 동시에 라우팅됨

댓글 남기기