01.17 REACT ROUTER

React.js

01.17. React Router

01.17.1. SPA

SPASingle Page Application, 말 그대로 페이지가 하나인 애플리케이션이라는 의미이다.
전통적인 페이지는 여러 페이지로 구성되어 있다.
유저가 요청할 때 마다 페이지를 새로고침하며, 페이지를 로딩할 때마다 서버에서 리소스를 전달받아 해석한 후 렌더링한다. HTML 파일 또는 템플릿 엔진 등을 사용해서 애플리케이션 뷰를 어떻게 보일지도 서버에서 담당한다.

웹에서 제공하는 정보가 점점 많아지면서 속도 문제가 발생했는데, 이를 해소하려고 캐싱과 압축을 해서 서비스를 제공한다. 그러나 이 방법은 사용자와 상호작용(interaction)이 많은 모던 웹 애플리케이션에선느 충분하지 않을 수 이다. 서버에서 렌더링을 담당한다는 것은 그만큼 서버 사줭늘 렌더링하는데 사용한다는 의미로, 불필요한 트래픽이 낭비가 되기 때문이다.

리액트 같은 라이브러리 또는 프레임워크를 사용해서 뷰 렌더링을 유저의 웹 브라우저가 담당하도록 하고, 애플리케이션을 우선 웹 브라우저에 로드시킨 후 필요한 데이터만 전달받아 보여 줄 것이다.

싱글 페이지 애플리케이션은 서버에서 제공하는 페이지가 하나이지만, 로딩을 한 번 하고 나면 웹 브라우저에서 나머지 페이지들을 정의한다. 페이지에 들어온 후 다른 페이지로 이동할 때는 서버에 새로운 페이지를 요청하는 것이 아니라, 새 페이지에서 필요한 데이터만 받아 와 그에 따라 웹 브라우저가 다른 종류의 뷰를 만들어 주는 것이다.

SPA 단점은 앱 규모가 커지면 자바스크립트 파일 크기도 너무 커진다는 것이다. 페이지를 로딩할 때, 유저가 실제로 방문하지 않을 수도 있는 페이지와 관련된 컴포넌트 코드도 함께 불러오기 때문이다. 하지만 코드 스플리팅(code splitting)을 사용하면 라우트별로 파일을 나누어 트래픽과 로딩 속도를 개선할 수 있다.

01.17.2. React Router

리액트 라우터를 사용한 클라이언트 라우팅








리액트 라우터를 사용하면 페이지 주소를 변경했을 때 주소에 따라 다른 컴포넌트를 렌더링해 주고, url 정보(파라미터, 쿼리 등)를 컴포넌트의 props 로 전달해서 컴포넌트 단에서 url 상태에 따라 다른 작업을 하도록 설정할 수 있다.
$ create-react-app react-route-sample
$ cd react-route-sample
$ yarn add react-router-dom
프로젝트 초기화를 위해 아래 파일을 삭제한다.
  • src/App.css
  • src/App.test.js
  • src/logo.svg
디렉터리를 생성한다.
  • src/components: 컴포넌트들이 위치하는 디렉터리
  • src/pages: 각 라우트들이 위치하는 디렉터리

01.17.2.1. NODE_PATH 설정

컴포넌트나 모듈을 import 할 때 보통 상대 경로로 불러왔다. 하지만 디렉터리 구조가 복잡하면 상대 경로 주소도 복잡해지기 때문에 헷갈릴 수 있다. 이런 문제는 프로젝트의 루트 경로를 지정하여 파일을 절대 경로로 불러오면 쉽게 해결할 수 있다.
// package.json
  "scripts": {
    "start": "NODE_PATH=src react-scripts start",
    "build": "NODE_PATH=src react-scripts build",
    (...)
  },
※ Windows 운영체제에서는 yarn 으로 cross-env를 설치해야 정상적으로 작동한다.
$ yarn add cross-env

// package.json
  "scripts": {
    "start": "cross-env NODE_PATH=src react-scripts start",
    "build": "cross-env NODE_PATH=src react-scripts build",
    (...)
  },
이렇게 설정하면 파일을 절대 경로로 불러올 수 있다.

01.17.2.2. 컴포넌트 생성

// src/App.js
import React, { Component } from 'react';

class App extends Component {
  render() {
    return (
      <div>
        리액트 라우터
      </div>
    );
  }
}

export default App;
App 컴포넌트에서는 웹 브라우저의 주소에 따라 어떤 컴포넌트를 보여줄 지 정의한다.
Root 컴포넌트를 만들고 BrowserRouter를 적용한다. BrowserRouter는 HTML5의 history API를 사용하여 새로고침하지 않고도 페이지 주소를 교체할 수 있게한다.
// src/Root.js
import React from 'react';
import {BrowserRouter} from 'react-router-dom';
import App from './App';

const Root = () => {
    return (
        <BrowserRouter>
            <App />
        </BrowserRouter>
    );
};

export default Root;
index.js 에서 App 이 아닌 Root를 렌더링하도록 수정한다.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'index.css';
import Root from 'Root';
import * as serviceWorker from 'serviceWorker';

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
서버를 실행시켜 페이지가 렌더링되는지 확인한다.
$ yarn start
브라우저에서 작성한 텍스트가 확인된다면 적용된 것이다.
페이지를 열었을 때, 기본적으로 보여 줄 Home 라우트를 만든다.
// src/pages/Home.js
import React from 'react';

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

export default Home;
같은 형식으로 About 컴포넌트를 만든다. 이 컴포넌트는 /about 주소로 들어왔을 때 보이는 라우트이다.
// src/pages/About.js
import React from 'react';

const About = () => {
    return(
        <div>
            <h2>소개</h2>
            <p>
                리액트 라우트 소개 페이지.
            </p>
        </div>
    );
};

export default About;
생성한 페이지 컴포넌트들을 불러와 파일 하나로 내보낼 수 있도록 인덱스 파일을 만든다.
// src/pages/index.js
/* 다음 코드는 컴포넌트를 불러온 후 동일한 이름으로 내보낸다. */
export { default as Home } from './Home';
export { default as About } from './About';
만든 페이지에 주소를 설정한다. 페이지 주소를 설정할 때는 Route 컴포넌트를 사용한다.
// src/App.js
import React from 'react';
import {Route} from 'react-router-dom';

import {Home, About} from 'pages';

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

export default App;
Route 컴포넌트에서 경로는 path 값으로 설정하고, 보여 줄 컴포넌트는 component 값으로 설정한다.
첫 번재 라우트 Home 은 주소가 / 와 일치할 대 보여주도록 설정했다.
exact 값은 주소가 여기에서 설정한 path 와 정확히 일치할 때만 보이도록 설정하는 것이다.
exact 값을 제거하면 /about 경로로 들어와도 / 경로의 내부이기 때문에 일치하는 것으로 간주하여 컴포넌트가 보인다.

01.17.2.3. 라우트 파라미터와 쿼리 읽기

라우트의 경로에 특정 값을 넣는 방법은 두 가지이다.
하나는 params 를 사용하는 것이고, 나머지 하나는 Query String 을 사용하는 것이다.

params
// src/App.js
import React from 'react';
import {Route} from 'react-router-dom';

import {Home, About} from 'pages';

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

export default App;
URL 의  params 를 지정할 때는 :key 형식으로 설정한다.
// src/pages/About.js
import React from 'react';

const About = ({match}) => {
    return(
        <div>
            <h2>소개</h2>
            <p>
                {match.params.name} 리액트 라우트 소개 페이지. 
            </p>
        </div>
    );
};

export default About;
params 객체는 컴포넌트를 라우트로 설정했을 때 props 로 전달받는 match 객체 내부에 있다.
위 코드를 저장 후 'http://localhost:3000/about/이름' 주소를 입력하고 들어간다면 About 컴포넌트가 중복된다.
// src/App.js
(...)      
      <Route exact path="/about" component={About} />
(...)
exact를 설정하여 해결하거나
// src/App.js
(...)      
      <Route path="/about/:name?" component={About} />
(...)
:name 값을 선택적으로 입력받을 수 있게 params 뒷부분에 ? 를 입력하는 것이다.
파라미터가 여러 개일 경우에는
// src/App.js
(...)      
      <Route path="/about/:name?/:anotherValue" component={About} />
(...)
":name/:anotherValue" 와 같이 입력하면 된다.

Query String
Quert String 은 URL 뒤에 /about/something?key=value&anotherKey=value 형식으로 들어가는 정보이다. 이 문자열로 된 쿼리를 객체 형태로 파싱하려면 query-string 라이브러리를 설치해야한다.
Query String 은 App.js 에서 라우트를 절정할 때 정의하지 않고, 라우트 내부에서 정의한다.
Query 내용을 받아 오려면 라우트로 설정된 컴포넌트에서 받아 오는 props 중 하나인 location 객체의 search 값을 조회해야한다.
$ yarn add query-string

// src/pages/About.js
import React from 'react';
import queryString from 'query-string';

const About = ({location, match}) => {
    const query = queryString.parse(location.search);
    console.log(query);
    return(
        <div>
            <h2>소개</h2>
            <p>
                {match.params.name} 리액트 라우트 소개 페이지. 
                {location.search}.
            </p>
        </div>
    );
};

export default About;
query string 으로 받은 정보에 따라 폰트 색상을 변경해본다.
// src/pages/About.js
import React from 'react';
import queryString from 'query-string';

const About = ({location, match}) => {
    const query = queryString.parse(location.search);
    
    const {color} = query;

    return(
        <div>
            <h2 style={{color}}>소개</h2>
            <p>
                {match.params.name} 리액트 라우트 소개 페이지. 
                {location.search}.
            </p>
        </div>
    );
};

export default About;
http://localhost:3000/about/dd?color=red
소개 부분의 색상이 변경되었다. query string 을 사용할 때는 값들이 모두 문자열이라는 것에 주의해야한다. 따라서 query string 을 사용하여 받아 온 값을 비교해야 할 때는 Boolean 형태의 값을 불러오든, 숫자 형태로 불러오든 간에 문자열 형태로 비교를 하거나 알맞은 형태로 변환시킨 후 비교해야 한다.

01.17.2.4. 라우트 이동

Link 컴포넌트
애플리케이션 안에서 다른 라우트로 이동할 때는, 다른 페이지로 이동하는 링크를 작성할 때 사용하는 일반적인 태그인 <a href="">link</a> 형식으로 하면 안된다. a 태그를 클릭하면 페이지를 새로고침하면서 로딩하기 때문이다.
새로고침을 방지하려면 리액트 라우트에 있는 Link 컴포넌트를 사용해야 한다. 페이지를 새로고침하지 않고 주소 창 상태를 변경하고 원하는 라우트로 화면을 전환한다.
// src/component/Menu.js
import React from 'react';
import {Link} from 'react-router-dom';

const Menu = () => {
    return (
        <div>
            <ul>
                <li><Link to="/"></Link></li>
                <li><Link to="/about">소개</Link></li>
                <li><Link to="/about/이름">이름 소개</Link></li>
            </ul>
        </div>
    );
};

export default Menu;
Link 컴포넌트는 react-router-dom 에서 불러온다. 이 컴포넌트를 사용할 때는 이동할 주소를 컴포넌트의 to 으로 지정한다.
// src/App.js
import React from 'react';
import {Route} from 'react-router-dom';

import {Home, About} from 'pages';

import Menu from 'components/Menu';

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

export default App;

NavLink 컴포넌트
NavLink 컴포넌트는 Link 컴포넌트와 비슷하지만, 추가 기능이 있다. 현재 주소와 해당 컴포넌트의 목적지 주소가 일치한다면 특정 스타일 또는 클래스를 지정할 수 있다.
// src/components/Menu.js
import React from 'react';
import {NavLink} from 'react-router-dom';

const Menu = () => {
    const activeStyle = {
        color: 'green',
        fontSize: '2rem'
    }
    return (
        <div>
            <ul>
                <li><NavLink exact to="/" activeStyle={activeStyle}></NavLink></li>
                <li><NavLink exact to="/about" activeStyle={activeStyle}>소개</NavLink></li>
                <li><NavLink to="/about/이름" activeStyle={activeStyle}>이름 소개</NavLink></li>
            </ul>
        </div>
    );
};

export default Menu;
NavLink 컴포넌트를 사용하면 해당 링크를 활성화했을 때 activeStyle로 스타일을 지정할 수 있다. CSS 클래스를 적용하고 싶다면 activeClassName 값을 지정한다.

NavLink 컴포넌트를 사용할 때 exact 키워드도 포함해 주었는데, 이 키워드의 용도는 라우트를 설정할 때와 동일하다.

자바스크립트에서 라우팅
링크를 클릭하는 단순한 경우가 아니라. 자바스크립트에서 페이지를 이동해야 하는 로직을 작성해야할 때 라우트로 사용된 컴포넌트가 받아 오는 props 중 하나인 history 객체의 push 함수를 활용한다.
// src/pages/Home.js
import React from 'react';

const Home = ({history}) => {
    return(
        <div>
            <h2></h2>
            <button onClick={() => {
                history.push('/about/javascript');
            }}>자바스크립트 라우팅</button>
        </div>
    );
};

export default Home;

01.17.2.5. 라우트 안의 라우트

라우트 안에 또 다른 라우트를 정의하는 방법이다.
// src/pages/Post.js
import React from 'react';

const Post = ({match}) => {
    return (
        <p>
            포스트 #{match.params.id}
        </p>
    );
};

export default Post;

// src/pages/index.js
export { default as Home } from './Home';
export { default as About } from './About';
export { default as Post } from './Post';
포스트 목록을 보여 줄 Posts 페이지 컴포넌트를 생성한다.
// src/pages/Posts.js
import React from 'react';
import {Post} from 'pages';
import {Link, Route} from 'react-router-dom';

const Posts = ({match}) => {
    return (
        <div>
            <h3>포스트 목록</h3>
            <ul>
                <li><Link to={`${match.url}/1`}>포스트 #1</Link></li>
                <li><Link to={`${match.url}/2`}>포스트 #2</Link></li>
                <li><Link to={`${match.url}/3`}>포스트 #3</Link></li>
            </ul>
            <Route exact path={match.url} render={() => (<p>포스트를 선택하세요</p>)} />
            <Route exact path={`${match.url}/:id`} component={Post}/>
        </div>
    );
};

export default Posts;
링크를 설정하는 부분에서 match.url 을 사용했다. 이 컴포넌트는 '/posts'라는 라우트로 등록할 것이다.
match.url 은 현재 라우트에 설정된 경로 '/posts' 를 알려준다. 링크의 to 값을 지정할 때 '/posts/1' 로 설정해도 동일하게 작동한다.

차이점은 나중에 Posts 컴포넌트의 라우트 주소를 '/blog-posts' 로 변경했다고 가정했을 때, 내부 주소도 자동으로 반영하기 때문에 따로 변경할 필요가 없다는 것이다.

아래쪽에서 Route 의 path 를 설정할 때도 같은 이유로 match.url 을 사용하였다. 첫 번째 라우트에는 id 값이 주어져 있지 않고, /posts 와 정확히 일치할 때만 render 에 있는 내용을 보여 주도록 설정했다. 따로 컴포넌트를 만들어 등록하는 것이 아니라 무엇을 보여 줄지 JSX 를 직접 작성하는 경우에는 이렇게 render 라는 props 를 설정하면 된다.

두 번째 라우트에서는 현재 라우트의 주소에 :id 가 붙었을 때 Post 컴포넌트를 보여 주도록 설정했다.
// src/pages/index.js
export { default as Home } from './Home';
export { default as About } from './About';
export { default as Post } from './Post';
export { default as Posts } from './Posts';

// src/App.js
import React from 'react';
import {Route} from 'react-router-dom';

import {Home, About, Posts} from 'pages';

import Menu from 'components/Menu';

const App = () => {
  return (
    <div>
      <Menu />
      <Route exact path="/" component={Home} />
      <Route path="/about/:name?" component={About} />
      <Route path="/posts" component={Posts} />
    </div>
  );
}

export default App;

// src/components/Menu.js
import React from 'react';
import {NavLink} from 'react-router-dom';

const Menu = () => {
    const activeStyle = {
        color: 'green',
        fontSize: '2rem'
    }
    return (
        <div>
            <ul>
                <li><NavLink exact to="/" activeStyle={activeStyle}></NavLink></li>
                <li><NavLink exact to="/about" activeStyle={activeStyle}>소개</NavLink></li>
                <li><NavLink to="/about/이름" activeStyle={activeStyle}>이름 소개</NavLink></li>
                <li><NavLink to="/posts" activeStyle={activeStyle}>포스트 목록</NavLink></li>
            </ul>
        </div>
    );
};

export default Menu;
메뉴에 포스트 목록이 생성되었다. 라우트 안에 라우트가 잘 동작한다.

01.17.2.6. 라우트로 사용된 컴포넌트가 전달받는 props

location, match, history 값들을 props로 받아와 사용해보았다. 각 객체가 어떤 역할을 하는지 알아본다.

location
location 은 현재 페이지의 주소 상태를 알려 준다. Post 페이지 컴포넌트에서 location 을 조회하면 다음 과 같은 결과가 나온다.
{
    "pathname": "/posts/3",
    "search": "",
    "hash": "",
    "key": "xmsczi",
}
location 값은 어떤 라우트 컴포넌트에서 조회하든 같다. 주로 search 값에서 URL Query 를 읽는데 사용하거나 주소가 바뀐 것을 감지하는데 사용한다.
componentDidUpdate(prevProps, prevState) {
    if(prevProps.location != this.props.location) {
        // 주소가 바뀜
    }
}
match
match 는 <Route> 컴포넌트에서 설정한 path 와 관련된 데이터들을 조회할 때 사용한다. 현재 URL이 같을지라도 다른 라우트에서 사용된 match 는 다른 정보를 알려준다. Post 라우트와 Posts 라우트에서 match 값을 기록해 확인한다.
// src/pages/Post.js
import React from 'react';

const Post = ({match}) => {
    console.log('Post : ',match);
(...)

// src/pages/Posts.js
import React from 'react';
import {Post} from 'pages';
import {Link, Route} from 'react-router-dom';

const Posts = ({match}) => {
    console.log('Posts : ',match);
(...)


다른 라우트에서 기록한 match 객체는 다른 정보를 보여준다. match 객체는 주로 params를 조회하거나 서브 라우트를 만들 때 현재 path 를 참조하는데 사용한다.
history
history 는 현재 라우터를 조작할 때 사용한다. 페이지를 뒤로 이동하거나 앞으로 이동, 새로운 주소로 이동해야 할 대 이 객체가 지닌 함수를 호출한다.
리액트 개발자 도구를 열어 Home 컴포넌트를 조회한다.

이 객체에서 헷갈리 수 있는 함수는 push 와 replace 이다. replace 는 replac('/posts') 형식으로 작성한다. push 와 차이점은 페이지 방목 기록을 남기지 않아서 페이지 이동 후 뒤로가기 버튼을 눌렀을 때 방금 전 페이지가 아닌 방금 전의 전 페이지가 나타난다. action은 현재 history 상태를 알려준다. 페이지를 처음 방문했을 때는 POP가 나타나고 링크를 통한 라우팅 또는 push 를 통한 라우팅을 했을 때는 PUSH 가 나타나며, replace 를 통한 라우팅을 했을 때는 REPLACE 가 나타난다.
block 함수는 페이지에서 벗어날 때, 사용자에게 정말 페이지를 떠나겠냐고 묻는 창을 띄운다.
const block = history.block('정말로 떠나시겠습니까?');
unblock(); // 막는 작업을 취소할 때
go, goBack, goForward 는 이전 페이지 또는 다음 페이지로 이동하는 함수이다. go 함수에서 go(-1)로 뒤로가기를 할 수 있고, go(1)로 다음으로 가기를 할 수 있다.

01.17.2.7. withRouter로 기타 컴포넌트에서 라우터 접근

위에서 학습한 세 가지 props 는 라우트로 사용된 컴포넌트에서만 접근할 수 있었다. 즉, 라우트 내부 또는 외부 컴포넌트에서 history, location, match 등 값을 사용할 수 없다.
Menu 컴포넌트는 라우트 외부에 있기 때문에 이 세 가지 props 를 사용할 수 없다.
이때는 withRouter 를 사용하여 해당 props 에 접근할 수 있다.
Menu 컴포넌트 위쪽에서 withRouter 를 리액트 라우터에서 불러온 후, 컴포넌트를 내보낼 때 withRouter 함수로 감싸 주면 Menu 컴포넌트에서도 history 등 객체를 사용할 수 있다.
// src/component/Menu.js
import React from 'react';
import {NavLink, withRouter} from 'react-router-dom';
(...)
export default withRouter(Menu);
리액트 개발자 도구를 이용해 Menu 컴포넌트를 조회하면 세 가지 props 들이 확인된다.
withRouter 를 사용한 컴포넌트에서 match 값은 해당 컴포넌트가 위치한 상위 라우트의 정보이다. 지금은 Menu 컴포넌트가 라우트 외부에 있으니 path 는 / 이다. Menu 컴포넌트를 PostPage 내부에 렌더링 한다면 path 는 /posts/:id 형식이 될 것이다.

withRouter 는 주로 history 에 접근하여 컴포넌트에서 라우터를 조작하는데 사용한다.

큰 규모의 프로젝트를 진행하다 보면 리액트 라우터의 한 가지 문제가 발생한다. 바로 웹 브라우저에서 사용할 컴포넌트, 상태 관리를 하는 로직들, 여러 기능을 구현하는 함수들이 점점 쌓이면서 컴포넌트 코드를 많이 입력하기 때문에, 최종 결과물인 자바스크립트 파일 크기매우 커진다는 점이다.
이를 보완하는 것이 코드 스플리팅이다.

댓글

  1. 감사합니다!쉽게 알려주셔서
    이해하는데 도움이 됐습니다.

    다만 props로 post component에 :id값을 보냈습니다.
    이 props를 post에서 어떻게 받아들여야할지...
    갖고 있는 마크다운 파일들 중, 그 :id 값을 마크다운과 어떻게 딱 맞게 불러읽도록 만들 수 있는지 모르겠네요...

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

01.7 React ref (DOM에 이름 달기)

01.13 React Redux(리덕스)