All Articles

라우터로 SPA 만들기

라우팅 방식

  • 브라우저에 의한 라우팅

    • HTML로만 웹페이지를 구성했을 때 사용하던 전통적인 방법
    • <a> 태그를 활용하거나, JavaScript에서 location.href 속성을 활용해 페이지 이동
  • JavaScript 단에서 라우팅 흉내내기

    • 화면 구성 후, 그 화면 콘텐츠에 맞게 주소창 주소를 임의로 변경(부여)하는 방식
    • 리액트는 SPA 방식으로 개발하는 것이 보다 적합
    • react-router-dom 라이브러리 활용

리액트에서 SPA 방식으로 웹페이지를 구현하려면 <a> 태그는 사용하면 안 된다. <a>태그를 쓰면 페이지 이동 개념이라 페이지 전체가 다시 새로 렌더링되므로 SPA 조건에 위배된다.

<a> 태그를 썼을 때 정말 페이지 전체가 다시 그려지는지 확인해보자.

메인화면, about 페이지, about company 페이지, profile 페이지, blog 페이지로 구성된 웹사이트를 만들었다. 각 페이지는 별도 컴포넌트로 만들었다.

import React from "react";

const App = () => {
  return (
    <div>
      <h1>App10</h1>
      <ul>
        <li>
          <a href="/about/">about</a>
        </li>
        <li>
          <a href="/about/company/">about company</a>
        </li>
        <li>
          <a href="/profile/">profile</a>
        </li>
        <li>
          <a href="/blog/">blog</a>
        </li>
      </ul>

      <AboutPage />
      <ProfilePage />
      <BlogPage />

    </div>
  );
};

// /about/
const AboutPage = () => {
  return (
    <div>
      <h2>About Page</h2>
    </div>
  );
};

// /about/company/
const AboutCompanyPage = () => {
  return (
    <div>
      <h2>About Company Page</h2>
    </div>
  );
};

// /profile/
const ProfilePage = () => {
  return (
    <div>
      <h2>Profile Page</h2>
    </div>
  );
};

// /blog/
const BlogPage = () => {
  return (
    <div>
      <h2>Blog Page</h2>
    </div>
  );
};

export default App;

a 태그 썼을 때 페이지 새로 렌더링

파비콘을 보면 페이지 전체가 새로 그려지고 있다는 걸 알 수 있다.

BrowserRouter, Link, Route를 사용해 SPA를 구현해보자. 우선 react-router-dom 패키지를 설치한다.

yarn add react-router-dom

Link, Route는 반드시 BrowserRouter 안에서 써야한다. BrowserRouter가 context API를 통해 현재 라우팅 정보를 전달하기 때문이다.

<BrowserRouter></BrowserRouter>는 보통 앱 최상위에서 감싸준다.

import React from "react";
import { BrowserRouter, Link, Route } from "react-router-dom";

const App = () => {
  return (
    <BrowserRouter>
      <div>
        <h1>App10</h1>
        <ul>
          <li>
            <Link to="/about/">about</Link>
          </li>
          <li>
            <Link to="/about/company/">about company</Link>
          </li>
          <li>
            <Link to="/profile/">profile</Link>
          </li>
          <li>
            <Link to="/blog/">blog</Link>
          </li>
        </ul>

        <Route path="/about/" component={ AboutPage } />
        <Route path="/about/company/" component={ AboutCompanyPage } />
        <Route path="/profile/" component={ ProfilePage } />
        <Route path="/blog/" component={ BlogPage } />

      </div>
    </BrowserRouter>
  );
};

// /about/
const AboutPage = () => {
  return (
    <div>
      <h2>About Page</h2>
    </div>
  );
};

// /about/company/
const AboutCompanyPage = () => {
  return (
    <div>
      <h2>About Company Page</h2>
    </div>
  );
};

// /profile/
const ProfilePage = () => {
  return (
    <div>
      <h2>Profile Page</h2>
    </div>
  );
};

// /blog/
const BlogPage = () => {
  return (
    <div>
      <h2>Blog Page</h2>
    </div>
  );
};

export default App;

BrowserRouter, Link, Route 사용 예시

그런데 /about/company/ 페이지에서 about 컴포넌트와 about company 컴포넌트가 모두 리턴된다. Route는 매칭되는 모든 컴포넌트가 이어서 렌더링 되기 때문인데, 주소가 /about/ 이라는 조건에 부합하므로 두 컴포넌트가 모두 리턴되는 것이다.

필요한 컴포넌트만 렌더링하기 위해 정확히 일치할 때만 리턴하는 exact 속성을 추가한다. (기본값 exact={true})

<Route exact path="/about/" component={ AboutPage } />
<Route exact path="/about/company/" component={ AboutCompanyPage } />
<Route exact path="/profile/" component={ ProfilePage } />
<Route path="/blog/" component={ BlogPage } />

exact 사용 예시

/blog/post/1, blog/post/2 이렇게 주소가 계속 뒤에 붙는 경우는 exact를 붙이지 않는 것이 맞다.

Link와 유사하나, activeStyleactiveClassName 속성을 지원한다. 스타일 속성과 클래스 이름을 객체 형태 속성값으로 넘겨받아 동적으로 적용할 수 있다.

속성을 이렇게 추가해주고,

const navActiveStyle = {
  fontWeight: 'bold',
  backgroundColor: 'yellow'
};

컴포넌트에 activeStyle 속성을 추가해준다.

import React from "react";
import { BrowserRouter, NavLink, Route } from "react-router-dom";

const App = () => {
  return (
    <BrowserRouter>
      <div>
        <h1>App10</h1>
        <ul>
          <li>
            <NavLink exact to="/about/" activeStyle={ navActiveStyle }>about</NavLink>
          </li>
          <li>
            <NavLink exact to="/about/company/" activeStyle={ navActiveStyle }>about company</NavLink>
          </li>
          <li>
            <NavLink to="/profile/" activeStyle={ navActiveStyle }>profile</NavLink>
          </li>
          <li>
            <NavLink to="/blog/" activeStyle={ navActiveStyle }>blog</NavLink>
          </li>
        </ul>

        <Route exact path="/about/" component={ AboutPage } />
        <Route exact path="/about/company/" component={ AboutCompanyPage } />
        <Route exact path="/profile/" component={ ProfilePage } />
        <Route path="/blog/" component={ BlogPage } />

      </div>
    </BrowserRouter>
  );
};

const navActiveStyle = {
  fontWeight: 'bold',
  backgroundColor: 'yellow'
};

// /about/
const AboutPage = () => {
  return (
    <div>
      <h2>About Page</h2>
    </div>
  );
};

// /about/company/
const AboutCompanyPage = () => {
  return (
    <div>
      <h2>About Company Page</h2>
    </div>
  );
};

// /profile/
const ProfilePage = () => {
  return (
    <div>
      <h2>Profile Page</h2>
    </div>
  );
};

// /blog/
const BlogPage = () => {
  return (
    <div>
      <h2>Blog Page</h2>
    </div>
  );
};

export default App;

NavLink로 스타일 동적으로 적용하기

about company 컴포넌트를 선택했을 때 about 컴포넌트까지 스타일이 적용되는데, Route와 마찬가지로 NavLink에도 exact 속성을 추가해서 해결할 수 있다.

<NavLink exact to="/about/" activeStyle={ navActiveStyle }>about</NavLink>
<NavLink exact to="/about/company/" activeStyle={ navActiveStyle }>about company</NavLink>
<NavLink to="/profile/" activeStyle={ navActiveStyle }>profile</NavLink>
<NavLink to="/blog/" activeStyle={ navActiveStyle }>blog</NavLink>

exact 사용 예시

Switch와 No Match 처리

Switch를 이용해 잘못된 경로로 접근했을 때(=일치하는 컴포넌트가 없을 때) 내보낼 컴포넌트를 생성할 수도 있다.

import { BrowserRouter, Route, NavLink, Switch } from "react-router-dom";
const RouteNoMatch = () => {
    return <div>잘못된 경로로 접근하셨습니다.</div>;
};

그리고 컴포넌트는 <Switch></Switch>로 묶어준다.

<Switch>
    <Route exact path="/about/" component={ AboutPage } />
    <Route exact path="/about/company/" component={ AboutCompanyPage } />
    <Route exact path="/profile/" component={ ProfilePage } />
    <Route path="/blog/" component={ BlogPage } />
    <Route component={ RouteNoMatch } />
</Switch>

어떤 경로로 접속했을 때 가장 위에 있는 컴포넌트부터 일치 여부를 확인 후, 아무것도 일치하는 게 없으면 RouteNoMatch 컴포넌트를 렌더링하도록 할 수 있다.

Route로 설정한 컴포넌트가 받는 3가지 props

  • history: 히스토리 조작

    • .location, .push(...), .replace(...), .goBack(), .goForward()
  • location: 현재 경로 정보

    • .hash, .pathname, .search, .state 속성
  • match: Router 매칭 정보

    • .isExact, .url, .path, .params 속성

location props 사용하기

location을 사용해 잘못 접속한 경로를 보여줄 수도 있다.

콘솔에 location을 찍어보면 객체가 어떻게 구성되어있는지 볼 수 있다.

location 객체

pathname 키로 접근하면 경로를 얻을 수 있겠다.

const RouteNoMatch = ({ location }) => {
    return <div>잘못된 경로로 접근하셨습니다. 접속 경로: { location.pathname }</div>;
};

match props의 .url, .params 속성 이용하기

url 하위 경로에 따라 렌더링할 내용을 다르게 지정할 수 있다.

블로그 포스트 내용을 담을 PostDetail 컴포넌트를 추가한다.

const PostDetail = ({ match }) => {
  const { params: { post_id }, } = match;

  return (
    <div>
      <h2>Post Detail #{ post_id }</h2>
    </div>
  )
}

기존 BlogPage 컴포넌트에는 이동할 링크를 생성한다. <a>태그를 사용하면 SPA가 아니게 되므로 LinkNavLink를 사용한다.

const BlogPage = ({ match }) => {
  return (
    <div>
      <h2>Blog Page</h2>
      <ul>
        <li>
          <Link to={`${match.url}100/`}>100번 글</Link>
        </li>
        <li>
          <Link to={`${match.url}101/`}>101번 글</Link>
        </li>
      </ul>
    </div>
  );
};

post id값을 받아 PostDetail을 렌더링 하려면 blog Route보다 위에 위치시켜야 한다.

<Switch>
  <Route exact path="/about/" component={ AboutPage } />
  <Route exact path="/about/company/" component={ AboutCompanyPage } />
  <Route exact path="/profile/" component={ ProfilePage } />
  <Route path="/blog/:post_id/" component={ PostDetail } />
  <Route path="/blog/" component={ BlogPage } />
  <Route component={ RouteNoMatch } />
</Switch>

match와 params 사용하기

QueryString 처리

query-string 라이브러리를 사용해 QueryString으로 넘어온 값을 파싱해 사용할 수도 있다. 다만 이 라이브러리는 넘어온 값을 객체로 파싱해주므로 값을 숫자 타입으로 사용하려면 적절히 변환해주어야 한다.

yarn add query-string

location 내 search 키 값이 쿼리스트링 값을 담고 있으므로 그것을 파싱하면 된다.

const ProfilePage = ({ location }) => {
  console.log('location: ', queryString.parse(location.search));
  return (
    <div>
        <h2>Profile Page</h2>
    </div>
  );
};

콘솔에 찍어보니 잘 나온다.

쿼리스트링 파싱하기

쿼리스트링 값을 변수에 할당해 어딘가에 이용해보자.

const ProfilePage = ({ location }) => {
  const { token } = queryString.parse(location.search);
  return (
    <div>
        <h2>Profile Page</h2>
        token: { token }
    </div>
  );
};

쿼리스트링 파싱한 값 이용하기