반응형

 

SPA (Single Page Application)과 react-router

 

react-router는 SPA와 밀접한 관련이 있습니다. 왜냐하면 react-router를 이용한 페이지의 이동은 서버에 대한 요청이 없이도 페이지와 url을 옮길 수 있기 때문입니다. SPA는 일단 한번 필요한 스크립트를 다운로드하면 다음에 링크 이동을 했을 때 새로 보여줘야 할 내용을 이미 가지고 있기 때문에 굳이 서버에 재요청을 할 필요가 없습니다. (요청할 필요가 있다면 ajax를 이용합니다.)

즉, react-router를 이용해 구현한 모든 링크 이동은 결국 하나의 페이지 내에서 이루어지는 것이고 이것은 SPA(Single Page Application)을 구현하는 핵심적인 기능이 되는 것입니다.

 

SPA가 아닌 경우 페이지 이동은 서버에 새로운 요청을 보내고 서버로부터 받아온 html 파일로 새로 렌더트리를 만들고 렌더링을 하는 방식입니다. 즉, 페이지 이동마다 매번 html 파일을 리로드 하고 모든 요소들을 처음부터 렌더링 하게 되죠.

그러나, SPA는 처음에 페이지에 접속할 때 최초로 받은 html파일이 유일하게 처음이자 마지막이고, 이후 아무리 페이지 이동을 해도 페이지를 리로드 하지 않습니다. 대신에 바뀌어야할 요소들만 부분적으로 다시 렌더링 합니다.

 

React와 react-router를 사용한다면 기본적으로 이러한 SPA를 구현하기가 매우 쉽습니다. 라이브러리의 핵심기능이 그것을 구현하는 것과 밀접한 관련이 있기 때문이죠.

 

 

react-router 다뤄보기

 

우선 react 프로젝트가 이미 있다고 가정하고, 그 안에서 react-router를 설치해보겠습니다. 터미널에서 다음과 같이 설치해줍니다.

npm i --save react-router-dom

 

그다음에는 App.js부터 살펴볼까요?? App.js부터 살펴보는 이유는 react-router를 앱 전반에서 사용할 것이기 때문에 가장 최상위 컴포넌트를 찾아가는 것입니다.

 

* App.js

import React, { Component } from 'react';
import { BrowserRouter } from 'react-router-dom';
import Router from './routers/Router';

class App extends Component {
    render () {
        return (
            <BrowserRouter>
            	<Router/>
            </BrowserRouter>
        );
    }
};

export default App;

 

여기서 react-router-dom을 import 한 뒤, BrowserRouter 컴포넌트를 비구조화 할당으로 받아와 렌더 함수 내에서 사용했습니다. 모든 컴포넌트를 감싸는 가장 바깥에서 사용해주면 됩니다. 이렇게 하면 하위 컴포넌트들 중에 Route 컴포넌트들은 history 객체를 props값으로 전달받아 접근할 수 있게 됩니다.

 

Route 컴포넌트는 react-rouet-dom에서 임포트 할 수 있는 컴포넌트이고 라우팅을 담당하는 컴포넌트입니다. 저는 Route 컴포넌트들을 Router.js라는 파일에 모아서 사용했습니다.

 

 

History 객체 소개

 

history 객체는 url의 변화를 기록하고 url을 컨트롤하는 메서드들을 내장하고 있습니다.

 

여기서는 주로 쓰일 만한 메소드 3가지를 소개하겠습니다.

바로 앞으로 가기, 뒤로 가기, 원하는 주소로 이동하기입니다. 딱 봐도 많이 쓰일 기능들이죠?

 

뒤로 가기 : history.goback() 혹은 history.go(-1)

앞으로 가기 : history.go(1)

 

go() 안에 파라미터의 역할이 감이 오시죠? 양수만큼 앞으로 가기를 하고, 음수만큼 뒤로 가기를 합니다.

예를 들어 history.go(-2)는 뒤로 두 번 가기입니다.

 

원하는 주소로 이동 : history.push(url)

 

push()는 배열에서도 사용되는 내장 메서드인데요. 똑같습니다. history 객체는 url의 히스토리를 배열에 저장하고 있는데 push()를 사용하면 마지막 값으로 파라미터 값을 밀어 넣게 됩니다.

 

 

 

 

라우팅 하기

 

다음은 하위 컴포넌트인 Router를 보러 갈까요? (제가 임의로 만든 컴포넌트입니다. react-router-dom에서 제공하는 컴포넌트인 Route와 혼동하지 마세요!)

 

* Router.js

import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Home, Article, NotFound } from '../pages/index';

const Router = () => (
    <Switch>
        <Route path='/article' component={Article}/>
        <Route exact path="/" component={Home}/>
        <Route component={NotFound}/>
    </Switch>
);

export default Router;

 

Route 컴포넌트는 각각의 url주소마다 보여줄 컴포넌트를 지정하는 라우팅 처리를 하고 있습니다.

이런 라우트들은 모두 Switch 컴포넌트로 감싸줍니다.

Switch컴포넌트로 Route 컴포넌트들을 감싸주는 이유는 이렇게 해야 url에 매칭 되는 첫 번째 라우트만 보여주고 나머지는 생략하기 때문입니다. 예를들어 첫번째줄 Route컴포넌트의 path값이 '/'이고, 두번째줄 Route컴포넌트의 path값이 '/abc'라고 가정하면, /abc 경로로 요청이 들어올 때 첫번째 Route컴포넌트의 path값이 걸려서 '/' 경로에 해당하는 컴포넌트를 생성하게 됩니다.

물론 이런 것을 방지하기 위해 exact 키워드를 path 앞에 붙이면 정확하게 path값이 일치해야만 채택을 합니다. 따라서 모든 Route 컴포넌트에 exact 키워드를 붙이면 사실 Switch의 주기능이 필요하지는 않게 됩니다.

게다가 라우터를 몇 번 만들다 보면 중복을 방지하게끔 url구조와 route 순서를 만드는 법을 알게 됩니다. 그래서 사실 Switch의 기능을 위해 반드시 써야 하는가 하면 그건 아니라고 할 수 있습니다.

 

그런데 저의 경우 그래도 그냥 Switch를 써주는 이유가 있습니다. 어차피 리액트의 렌더 함수에서는 가장 바깥 요소는 하나만 존재해야 한다는 룰이 있기 때문에 Switch 컴포넌트로 모든 Route컴포넌트를 감싸주면 그 룰을 지킬 수 있게 됩니다. 게다가 그로 인해 "이 Router.js는 라우터만 렌더링 하는 컴포넌트야."라는 사실을 더 분명하게 코드에서 의미적으로 나타낼 수 있습니다.

 

라우트의 path를 찾는 원리를 조금만 더 부연해보겠습니다.

위 코드의 경우는 /article로 이동하면 Article컴포넌트를 보여주고 /로 이동하면 Home 컴포넌트를 보여줍니다.

그런데 /의 경우에는 path앞에 exact라는 것이 붙어있어 정확하게 루트 도메인의 경우일 때에만 선택이 되지만,

/article의 경우 exact가 없기 때문에 /article 뒤에 다른 글자들이 와도 해당 라우트가 선택이 됩니다.

예를 들어 /articleabc 라던가 /article/123 이라던가 하는 도메인으로 이동해도 해당 라우트가 선택되는 것입니다.

 

반면 path값이 없는 마지막 라우트는 위에 있는 모든 라우트가 선택되지 않았을 때 예외처리를 위해 만든 것입니다. path가 없기 때문에 무조건 선택이 됩니다. 따라서 위의 모든 라우트가 선택되지 않으면 마지막 최후에 선택될 녀석인 것입니다. 그리고 그 녀석이 불러올 컴포넌트의 이름은 NotFound입니다. 이름에서도 알 수 있다시피 예외처리를 위한 페이지가 될 것 같네요. 이런 식으로 예외처리를 위한 페이지가 있다면 그것을 위한 라우트도 하나 만들 수 있습니다.

 

앞서 BrowserRouter 컴포넌트는 history 객체를 Route 컴포넌트에게 props로 전달한다고 했는데요. 그로 인해 Route 컴포넌트들의 component속성 값인 Article 컴포넌트, Home 컴포넌트, NotFound 컴포넌트 들은 history객체를 props로 전달받습니다. 그래서 컴포넌트 안에서 this.props.history와 같은 형태로 접근할 수 있게 되죠. Route컴포넌트가 아니라 그냥 <Article/> 이런 식으로 썼다면 해당 컴포넌트에는 history 객체가 전달되지 않습니다. 

 

exact path='/' 경로에 지정된 다음 하위 요소인 Home 컴포넌트를 구경해볼까요?

 

* Home.js

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

const Home = () => {
    return (
    	<h1>
            <Link to='/' className='link'>Home</Link>
        </h1>
        <div className='article-list'>
            <ul>
                <li><Link to='/article/1'>React</Link></li>
                <li><Link to='/article/2'>React-Router</Link></li>
                <li><Link to='/article/3'>SPA</Link></li>
            </ul>
        </div>
    )
};

export default Home;

 

링크 이동을 할 때에는 a tag가 아니라 Link 컴포넌트를 사용합니다. a tag와는 다르게 페이지를 리로드 하지 않고 주소를 옮길 수 있기 때문이죠. 이것이 SPA구현을 위한 핵심기능 중 하나입니다.

 

이제 해당 링크를 누르면 주소이동을 하고 그에 맞는 새로운 컴포넌트를 불러와서 리렌더링을 하는 절차를 밟습니다.

Home이라고 써져있는 링크를 누르면 '/'로 이동하여 Home 컴포넌트가 불려질 테니 딱히 변할 것은 없겠네요.

그런데 React, React-Router, SPA라고 써져있는 링크들을 누르면 각각 /article/1, /article/2, /article/3으로 이동하여

앞서 라우터에서 등록한 것을 통해 Article 컴포넌트를 불러서 바뀐 부분만 리렌더링 하게 되겠죠.

 

 

링크를 타고 갈 때 LifeCycle은 어떨까?

 

Link 컴포넌트를 통해 이동할 때 생명주기는 어떨까요?

 

만약 현재 주소가 루트 도메인 '/'이고, 위 예제에서 Home 링크를 누른다면 똑같이 Home 컴포넌트가 사용이 됩니다. 이때, Home컴포넌트는 이미 렌더링 되어 있는 상태기 때문에 생명주기는 업데이트 주기만 돌고 컴포넌트가 새로 생성될 때 호출되는 생명주기 함수들은 호출되지 않습니다. 즉, constructor나 componentDidMount 등은 호출되지 않습니다.

 

그런데 만약 현재 주소가 루트 도메인 '/'이고, 주소가 '/article/1'인 React 링크를 누른다면 어떻게 될까요? 이때, 이미 렌더링 되어 있는 컴포넌트는 Home이고 새로 호출된 컴포넌트는 Article입니다. 따라서 Home컴포넌트는 사라지고, Article 컴포넌트가 새로 불려지게 되므로, 생명주기는 처음부터 진행됩니다. Article컴포넌트의 constructor와 componentDidMount 등이 호출되고 업데이트 주기의 함수들은 호출되지 않는 것이죠. 물론 컴포넌트 호출 이후 로직에 따라 state값이 변하면 업데이트 주기가 돌게 되긴 하겠습니다.

 

 

 

 

react-router 사용 시 주소 변경 감지하는 방법

 

* Article.js

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

class Article extends Component {
    componentDidMount() {
        this.unlisten = this.props.history.listen((location, action) => { 
            console.log("on route change");
        });
    }

    componentWillUnmount() {
        console.log('#### component will unmount')
        this.unlisten();
    }
    
    render () {
        <Fragment>
            <h1>
                <Link to='/' className='link'>Article</Link>
            </h1>
            <div className='article-list'>
                <ul>
                    <li><Link to='/article/1'>React</Link></li>
                    <li><Link to='/article/2'>React-Router</Link></li>
                    <li><Link to='/article/3'>SPA</Link></li>
                </ul>
                <div className='article-body'>
                    contents
                </div>
            </div>
        </Fragment>
    }
};

export default Article;

 

이번엔 생명주기 함수를 사용하기 위해서 클래스형 컴포넌트인 Article을 보면서 링크 이동을 탐지하는 법을 소개하겠습니다.

 

현재 주소가 /article/1이라고 가정하겠습니다. React-Router 링크를 눌러서 /article/2로 이동할 때 위에서 말한 대로 똑같은 Article 컴포넌트이므로 업데이트 생명주기 함수만 호출이 됩니다. 그런데, 링크를 이동하는 그 순간을 탐지하고 싶다면 어떻게 해야 할까요? 업데이트 생명주기 함수가 호출되는 순간을 찾으면 될까요? 업데이트 생명주기 함수는 state값이 변할 때마다 호출되기 때문에 링크 이동을 탐지하는 방법으로 사용하기에는 더 범용적으로 호출되므로 힘들 것입니다. 

 

따라서 다른 방법이 있습니다.

위 예제에서 componentDidMount()에서 설치한 이벤트 리스너를 봐주세요.

저렇게 하면 /article/1에서 /article/2로 이동할 때 'on route change'가 콘솔에 출력되는 것을 볼 수 있을 것입니다. 그런데 홈으로 이동한다면 'on route change'도 출력되고 '#### component will unmount'도 출력될 것입니다. Article 컴포넌트가 사라지기 때문이죠.

 

 

history is undefined 에러

 

간혹 history를 undefined라고 찾을 수 없다고 에러가 뜰 수도 있습니다. 이런 에러가 발생하는 원인은 BrowserRouter 컴포넌트로부터 history를 props값으로 받아오지 못했기 때문입니다. 위에 App.js와 Route.js에서 history를 하위 컴포넌트들에게 어떻게 전달하는지 설명한 부분들을 잘 지키시면 해당 에러는 해결할 수 있습니다.

 

 

마무리

 

이렇게 해서 react-router를 사용하는 기본적인 방법들에 대해 알아봤고, history객체에 대해서도 알아봤습니다. 또한 history객체를 사용할 때 마주할 수 있는 흔한 에러도 하나 살펴봤습니다. 그리고 route의 원리, Link컴포넌트와 a태그의 차이점에 대해서 알아봤고, react-router로 SPA를 만드는 것이 왜 쉬운지에 대해 알아봤습니다. 

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기