React를 다루는 기술 13 - 리액트 라우터로 SPA 만들기

6 minute read

SPA란?

SPA는 Single Page Application의 약자로 한 개의 페이지로 이루어진 애플리케이션을 의미한다.
이전의 웹페이지들은 여러개의 페이지를 준비하고 서버에 요청하면 서버가 요청받은 각각의 페이지를 보여주는 방식이었다.

그런데 요즘은 웹에서 보여주는 정보의 양이 많기 때문에 이러한 방식으로 웹페이지가 구성되면 일일히 정보가 변경될 때마다 새로운 페이지를 보여줘야 하고 이를 위해 서버가 받는 부담이 매우 커지게 되었다.
서버의 성능을 높이거나 최적화를 통해 이를 어느정도 개선할 수는 있지만 인터페이스적으로도 매번 페이지가 변경될 때마다 새롭게 로딩을 하는 것은 매우 번거로운 일이다.

SPA는 이러한 문제를 해결하기 위해 하나의 페이지에서 모든 애플리케이션을 불러와서 실행시킨 후 사용자와의 인터랙션이 발생하면 변경된 부분만 새로 렌더링하는 방식으로 이루어져있다.

물론 SPA라고 해서 완전치많은 않은것이 처음 렌더링 될 때 모든 애플리케이션을 불러오다보니 프로젝트가 커질수록 초기부담이 커지게 되며 유저가 전혀 관심없는 부분까지도 일단은 렌더링을 하기 때문에 불필요한 낭비가 발생할 수 있다.
이 문제는 코드 스플리팅을 통해 라우팅별로 파일들을 나누는 것으로 추후 해결할 수 있다.

SPA에서의 페이지 전환

SPA는 하나의 페이지로 되어있는만큼 본래의 HTML에서 사용하던 페이지 이동방식은 사용할 수 없다.
대신 라우팅 라이브러리를 통해 이를 구현할 수 있는데 리액트 라이브러리 내에는 내장되어있지 않기 때문에 외부 라이브러리를 사용해야한다.
통상적으로 React-router, Reach-router, Next.js 등을 사용하며 오늘은 이 중에서 가장 대중적인 React-router를 알아보고자 한다.

프로젝트에 라우터 적용

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';

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

serviceWorker.unregister();

라우터를 적용하는 방법은 react-router-dom에 내장되어있는 BrowserRouter라는 컴포넌트를 사용하여 감싸면 된다.
이 컴포넌트는 HTML5의 History API를 이용해 페이지를 새로고침하지 않고도 주소를 변경하고 현재 주소와 관련된 정보를 props로 쉽게 열람할 수 있도록 해준다.

Route컴포넌트로 특정주소로 연결

Route 컴포넌트는 특정 규칙을 가진 경로에 원하는 컴포넌트를 보여줄 수 있는 기능을 한다.

  <Route path="주소규칙" component={보여  컴포넌트} />

이렇게 path 부분에 대응할 주소규칙을 쓰고 component에 해당 규칙에 맞을 시 렌더링 할 컴포넌트를 넣으면 된다.

import React from 'react';
import { Route } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  return (
    <div>
      <Route path="/" component={Home}>
      <Route path="/about" component={About}>
    </div>
  )
}

위와 같이 작성하면 주소창에 /으로 주소를 입력하면 Home 컴포넌트가 출력된다.
그런데 /about을 입력하면 About 컴포넌트만 출력되어야하는데 실제로는 Home, About 둘 다 출력되는 것을 볼 수 있다.
이는 //about에 부분일치하기 때문에 Route 컴포넌트에서 주소규칙에 해당된다고 판단하기 때문이다.
이런 경우 부분일치가 아닌 완전히 일치할 경우에만 보여주게 하기 위해서는 exact를 props로 붙여주면 된다.

import React from 'react';
import { Route } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  return (
    <div>
      <Route path="/" component={Home} exact>
      <Route path="/about" component={About}>
    </div>
  )
}

이렇게 하면 주소가 /일 때만 Home 컴포넌트가 출력되게된다.
exact props를 붙일 때 아무런 값도 할당해주지 않을 경우 리액트는 자동으로 true를 할당해주게 되기 때문에 위의 코드는 실제로는 exact={true}와 같다.

만약 Route 컴포넌트 하나에 여러개의 URL을 매핑하고 싶다면 2가지 방법이 있다.

import React from 'react';
import { Route } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  return (
    <div>
      <Route path="/" component={Home} exact>
      <Route path="/about" component={About}>
      <Route path="/info" component={About}>
    </div>
  )
}

첫번째는 위와 같이 다른 URL에 대응하며 같은 컴포넌트를 출력하는 Route 컴포넌트를 하나 더 만드는 것으로 본래는 이 방법이 유일했으나 React Router v5부터 새로운 방식이 추가되었다.

import React from 'react';
import { Route } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  return (
    <div>
      <Route path="/" component={Home} exact>
      <Route path={['/about', '/info']} component={About}>
    </div>
  )
}

이렇게 배열로 만들어서 넣어주는 것으로 좀 더 간단하게 적용이 가능하다.

Link 컴포넌트는 클릭 시 다른 페이지로 이동하는 컴포넌트이다.
보통의 HTML 페이지에서는 a태그나 onClick과 자바스크립트를 이용해 이동하지만 이러한 방식들은 페이지를 완전히 새로고침하여 리액트 컴포넌트 전체가 재렌더링되는 상황을 초래한다.
때문에 리액트 프로젝트에서는 새로고침 없이 다른 페이지로 이동하는 방법이 필요한데 이를 Link 컴포넌트가 HTML5 History API를 이용해 페이지의 주소만 변경시켜준다.

import React from 'react';
import { Route, Link } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  return (
    <div>
      <ul>
        <li>
          <Link to="/"></Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
    </div>
    <div>
      <Route path="/" component={Home} exact>
      <Route path="/about" component={About}>
    </div>
  )
}

Link 컴포넌트를 클릭할 경우 to props를 통해 전달해준 주소로 브라우저의 주소를 변경해주고 이를 통해 Route 컴포넌트에 해당되는 컴포넌트가 출력되는 형태로 페이지를 이동한다.

URL 파라미터와 쿼리

리액트 라우터도 URL을 통해 파라미터나 쿼리를 보낼 수 있다.
통상적으로 파라미터는 특정 아이디 혹은 이름을 사용하여 조회할 때 사용하고 쿼리는 특정 키워드를 검색하거나 옵션을 전달할 때 사용되는데
우선 URL 파라미터부터 알아보자.

import React from 'react';
import { Route, Link } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  return (
    <div>
      <ul>
        <li>
          <Link to="/"></Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/profile/myName">myName 프로필</Link>
        </li>
        <li>
          <Link to="/profile/nickName">nickName 프로필</Link>
        </li>
      </ul>
    </div>
    <div>
      <Route path="/" component={Home} exact>
      <Route path="/about" component={About}>
      <Route path="/profile/:username" component={Profile}>
    </div>
  )
}

위에서 중요한 것은 Route 컴포넌트에 넣은 URL 유형에 포함된 :username이다.
이는 /profile/myName에서 뒤의 myName을 username이라는 key값을 가진 파라미터로 넘긴다는 뜻이다.

const Profile = ({ match }) => {
  const { username } = match.params; 
  return (
    <div>{username}</div>
  )
}

이렇게 넘어간 파라미터는 받은 컴포넌트에서 match라는 객체 안에 params값에 포함되어서 받을 수 있다.

URL 쿼리는 location이라는 객체를 통해 쿼리문을 받아올 수 있다.

{
  "pathname": "/about",
  "search": "?detail=true",
  "hash": ""
}

location 객체는 웹 애플리케이션의 현재 주소에 대한 정보를 위와 같은 형태로 담고 있는데 이를 JSON 형태로 파싱하여 값을 전달받게 되는데 이를 위해 qs라는 라이브러리를 사용한다.

import React from 'react';
import qs from 'qs';

const About = ({ location }) => {
  const query = qs.parse(location.search, {
    ignoreQueryPrefix: true // 이 설정을 통해 문자열 맨 앞의 ?를 생략한다.  
  });

  const showDetail = query.detail; // 쿼리의 파싱결과값은 문자열이다.  

  return (
    <div>{showDetail}</div> // <div>true</div>
  )
}

qs를 이용한 파싱에 주의할 점은 우리가 전달할 때는 int형 혹은 boolean형으로 값을 보내더라도 파싱한 결과값은 모두 문자열이라는 점이다.
때문에 파싱한 후 다시 원래의 형태로 복구해주는 과정이 필요하다.

React 라우터의 부가기능

  1. history
    history 객체는 라우트로 사용된 컴포넌트에 match, location과 함께 전달되는 props 중 하나로 이 객체를 통해 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있다.
import React, { Component } from 'react';

class HistorySample extends Component {
  //뒤로 가기
  handleGoBack = () => {
    this.props.history.goBack();
  }

  //홈으로 이동
  handleGoHome = () => {
    this.props.history.push('/');
  }

  componentDidMount() {
    // 이것을 설정하고 나면 페이지에 변화가 생길 때 정말 나갈 것인지를 질문함
    this.unblock = this.props.history.block('정말 떠나실 건가요?');
  }

  componentWillMount() {
    // 컴포넌트가 언마운트되면 질문을 멈춤
    if (this.unblock) {
      this.unblock();
    }
  }
}
  1. withRouter
    withRouter 함수는 HoC(Higher-order Component)로 라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체에 접근할 수 있게 해준다.
import React from 'react';
import { withRouter } from 'react-router-dom';
const WithRouterSample = ({ location, match, history }) => {
  return (
    <div>
      <h4>location</h4>
      <textarea
        value={JSON.stringify(location, null, 2)}
        rows={7}
        readOnly={true}
      />
      <h4>match</h4>
      <textarea
        value={JSON.stringify(match, null, 2)}
        rows={7}
        readOnly={true}
      />
      <button onClick={() => history.push('/')}>홈으로</button>
    </div>
  )
}

export default withRouter(WithRouterSample);

평범하게 location, match, history객체를 사용한 후 마지막 export 해줄 때 컴포넌트명을 withRouter로 감싸주기만 하면 된다.

  1. Switch
    Switch는 나열되어있는 Route 컴포넌트들을 차례대로 URL과 매칭하여 가장 먼저 매칭하는 Route 컴포넌트 1개만을 보여주는 컴포넌트이다. 이를 통해 Not Found 페이지 등을 구현할 수 있다.
import React from 'react';
import { Route, Link, Switch } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  return (
    <div>
      <ul>
        <li>
          <Link to="/"></Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/profile/myName">myName 프로필</Link>
        </li>
        <li>
          <Link to="/profile/nickName">nickName 프로필</Link>
        </li>
      </ul>
    </div>
    <div>
      <Switch>
        <Route path="/" component={Home} exact>
        <Route path="/about" component={About}>
        <Route path="/profile/:username" component={Profile}>
        <Route render={<div>이 페이지는 존재하지 않습니다.</div>}>
      </Switch>
    </div>
  )
}

위의 코드에서 주소창에 Route 컴포넌트 중 해당되는게 없는 URL을 입력하면 차례대로 매칭한 후 맨 마지막의 path가 적혀있지 않은 Route 컴포넌트를 보여주게 된다. path에 아무것도 안 적는 것은 모든 URL을 허용한다는 뜻이기 때문이다.

  1. NavLink
    React Router v4부터 추가된 컴포넌트로 Link컴포넌트와 동일한 기능을 하며 차이점은 매핑한 URL과 현재 웹 어플리케이션의 URL이 동일할 경우 style 혹은 클래스명을 적용할 수 있다는 점이다.
import React from 'react';
import { Route, NavLink, Switch } from 'react-router-dom';
import About from './About';
import Home from './Home';

const App = () => {
  const activeStyle = {
    background: `black`
  }
  return (
    <div>
      <ul>
        <li>
          <NavLink activeStyle={activeStyle} to="/"></NavLink>
        </li>
        <li>
          <NavLink activeStyle={activeStyle} to="/about">About</NavLink>
        </li>
        <li>
          <NavLink activeStyle={activeStyle} to="/profile/myName">myName 프로필</NavLink>
        </li>
        <li>
          <NavLink activeStyle={activeStyle} to="/profile/nickName">nickName 프로필</NavLink>
        </li>
      </ul>
    </div>
    <div>
      <Switch>
        <Route path="/" component={Home} exact>
        <Route path="/about" component={About}>
        <Route path="/profile/:username" component={Profile}>
        <Route render={<div>이 페이지는 존재하지 않습니다.</div>}>
      </Switch>
    </div>
  )
}

위 코드의 경우 NavLink를 클릭하여 페이지를 이동할 때마다 해당 NavLink 컴포넌트에 activeStyle이 적용되어 배경색이 까맣게 변하는 것을 볼 수 있다.

출처자료

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=209354690 (리액트를 다루는 기술(개정판))

Categories:

Updated: