[React] 리액트를 다루는 기술 - 13장 리액트 라우터로 SPA 개발하기

기존 웹은 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아 오고, 페이지를 로딩할 때마다 서버에서 리소스를 전달받아 해석한 뒤 화면에 보여 줬다. 하지만 요즘은 웹 상의 정보가 매우 많기 때문에 화면이 전환될 때마다 html을 계속 서버에 새로 요청하면 UI의 상태를 유지하는 것도 힘들고, 바뀌지 않는 부분까지 새로 불러와서 보여 주어야 하기 때문에 비효율적이다.

따라서 리액트 같은 프레임워크를 사용하여 뷰 렌더링은 사용자의 브라우저가 담당하도록 하고, 사용자와의 인터랙션이 발생하면 그 부분만 자바스크립트를 사용하여 업데이트해 준다. 만약 새로운 데이터가 필요하다면 서버 API를 호출하여 필요한 데이터만 새로 불러와 애플리케이션에 넣을 수도 있다.

SPA


Single Page Application, SPA. 즉 한 개의 페이지로 구성된 애플리케이션을 뜻한다. 근데 우리가 보는 웹들은 분명 페이지가 여러 종류인데, 어떻게 싱글 페이지라는 거지?

리액트 라우터


SPA의 경우 서버에서 사용자에게 제공하는 페이지는 한 종류(index.html)이지만, 해당 페이지에서 로딩된 자바스크립트와 현재 사용자 브라우저의 주소 상태에 따라 다양한 화면을 보여준다. 이를 라우팅이라고 하는데, 리액트 라우터 라이브러리를 사용해서 구현할 수 있다.

  • 설치
    npm add react-router-dom

  • 적용
    src/index.js에서 react-router-dom에 내장되어 있는 BrowserRouter라는 컴포넌트로 다른 컴포넌트들을 감싸면 된다.

...
import { BrowserRouter } from 'react-router-dom';
...

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);
...
  1. 페이지 컴포넌트 두 개를 만들어 보자.
  • Home.js
import React from 'react';

const Home = () => {
    return (
        <div>
            <h1></h1>
            <p>, 가장 먼저 보여지는 페이지.</p>
        </div>
    );
};

export default Home;
  • About.js
import React from 'react';

const About = () => {
    return (
        <div>
            <h1>소개</h1>
            <p>이 프로젝트는 리액트 라우터 기초를 실습해 보는 예제 프로젝트입니다.</p>
        </div>
    );
};

export default About;
  1. RouteLink 컴포넌트를 이용해서 li 태그를 누르면 특정 주소로 이동하고, 우리가 만든 페이지 컴포넌트들이 나타나게 해보자.
  • App.js
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">소개</Link>
        </li>
      </ul>
      <hr/>
      <Route path="/" component={Home} exact={true} />
      <Route path="/about" component={About} />
    </div>
  );
};

export default App;

📌 a 태그가 아닌 Link를 사용하는 이유?
a 태그는 페이지를 전환하는 과정에서 페이지를 새로 불러오기 때문에 애플리케이션이 들고 있던 상태들을 모두 날려 버리고 처음부터 렌더링하게 된다.
Link 컴포넌트는 HTML5 History API를 사용하여 페이지 전환 없이 페이지의 주소만 변경해 준다.

📌 <Route ... exact={true} /> 에서 exact는?
'/about' 경로로 들어가면 About 컴포넌트만 나타나지 않고, '/' 경로인 Home 컴포넌트까지 두 컴포넌트가 모두 나타난다. '/about' 경로가 '/' 규칙에도 일치하기 때문에 발생한 현상이므로 Home을 위한 Route 컴포넌트를 사용할 때 exact라는 propstrue로 설정해야 한다.

📌 Route 하나에 여러 개의 path 설정하기
<Route path={['/about', '/info']} component={About} />
위와 같이 pathprops를 배열로 설정해 주면 여러 경로에서 같은 컴포넌트를 보여 줄 수 있다.



URL 파라미터와 쿼리


페이지 주소로 가끔은 유동적인 값을 전달해야 할 때도 있다. 이는 파라미터와 쿼리로 나눌 수 있다.

  • 파라미터 : 특정 아이디 혹은 이름을 사용하여 조회할 때 사용
    ex) /profile/dazzlynnnn
  • 쿼리 : 어떤 키워드를 검색하거나 페이지에 필요한 옵션을 전달할 때 사용
    ex) /about?details=true

Profile 페이지에서 파라미터를 사용해 보자. /profile/dazzlynnnn처럼 뒷부분에 유동적인 username 값을 넣어 줄 때 해당 값을 props로 받아 와서 조회할 수 있다.

  • Profile.js
import React from 'react';

const data = {
    dazzlynnnn: {
        name: '린',
        description: '리액트 공부 중'
    },
    gildong: {
        name: '홍길동',
        description: '고전 소설의 주인공'
    }
};

const Profile = ({ match }) => {
    const { username } = match.params;
    const profile = data[username];
    if (!profile) { 
        return <div>존재하지 않는 사용자입니다.</div>;
    }
    return (
        <div>
            <h3>
                {username}({profile.name})
            </h3>
            <p>{profile.description}</p>
        </div>
    );
};

export default Profile;

라우트로 사용되는 컴포넌트에서 받아 오는 match라는 객체 안의 params 값을 참조한다. match 객체 안에는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 들어 있다.

이제 App.js에서 Profile 컴포넌트를 위한 라우트를 정의해야 한다. path 규칙에 /profile/:username 을 넣어 주면 match.params.username 값을 통해 현재 username 값을 조회할 수 있다.

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

const App = () => {
  return (
    <div>
      <ul>
	...
        </li>
        <li>
          <Link to="/profile/dazzlynnnn">dazzlynnnn 프로필</Link>
        </li>
        <li>
          <Link to="/profile/gildong">gildong 프로필</Link>
        </li>
      </ul>
      <hr/>
	...
      <Route path="/profile/:username" component={Profile} />
    </div>
  );
};

export default App;

이번에는 About 페이지에서 쿼리를 받아와보자. 쿼리는 match가 아닌 location 객체에 들어 있는 search 값에서 조회할 수 있다. location 객체는 라우트로 사용된 컴포넌트에게 props로 전달되며, 웹 애플리케이션의 현재 주소에 대한 정보를 지니고 있다.

📌 location 객체의 형태

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

URL 쿼리를 읽을 때는 위 객체가 지닌 값 중에서 search 값을 확인해야 한다. URL 쿼리는 ?detail=true&another=1 과 같이 문자열에 여러 가지 값을 설정해 줄 수 있다. search 값에서 특정 값을 읽어 오기 위해서는 이 문자열을 객체 형태로 변환해 주어야 한다. 이를 위한 라이브러리가 바로 qs이다. npm add qs 명령어로 설치해주고, About 컴포넌트에서 location.search 값의 detail이 true인지 아닌지에 따라 추가 정보를 보여 주도록 코드를 고쳐 보자.

  • About.js
import React from 'react';
import qs from 'qs';

const About = ({ location }) => {
    const query = qs.parse(location.search, {
        ignoreQueryPrefix: true // 이 설정을 통해 문자열 맨 앞의 ?를 생각한다.
    })
    const showDetail = query.detail === 'true';
    return (
        <div>
            <h1>소개</h1>
            <p>이 프로젝트는 리액트 라우터 기초를 실습해 보는 예제 프로젝트입니다.</p>
            {showDetail && <p>detail 값을 true로 설정하셨군용</p>}
        </div>
    );
};

export default About;

📌 쿼리의 파싱 결과 값 (const query = qs.parse(location.search, {ignoreQueryPrefix: true}))은 언제나 문자열이기 때문에 query.detail === 'true'이처럼 확인해야 한다. boolean이나 숫자가 아니라 항상 문자열!! 숫자를 확인해야 할 때는 parseInt 함수를 써 주자.



서브 라우트


라우트 내부에 라우트를 또 정의하는 것을 서브 라우트라고 한다. 현재 App 컴포넌트에서 각각 따로 연결된 Profile 라우트들을 Profiles라는 라우터 컴포넌트로 묶어서 다시 정의해보자.

  • Profiles.js
import React from "react";
import { Link, Route } from "react-router-dom";
import Profile from "./Profile";

const Profiles = () => {
    return (
        <div>
            <h3>사용자 목록: </h3>
            <ul>
                <li>
                    <Link to="/profiles/dazzlynnnn">dazzlynnnn</Link>
                </li>
                <li>
                    <Link to="/profiles/gildong">gildong</Link>
                </li>
            </ul>

            <Route
                path="/profiles"
                exact // JSX에서 props 값을 생략하면 기본값 true
                render={() => <div>사용자를 선택해 주세요.</div>}
            />
            <Route path="/profiles/:username" component={Profile} />
        </div>
    );
};

export default Profiles;

📌 Route 컴포넌트에 component 대신 render라는 props를 넣어 주었다.
이처럼 따로 컴포넌트를 만들기 애매한 상황에는 그냥 보여 주고 싶은 JSX만 전달할 수 있다.

이에 맞추어 App 컴포넌트도 수정해 주자.

  • App.js
...
import Profiles from './Profiles';

const App = () => {
  return (
    <div>
	...
        <li>
          <Link to="/profiles">프로필</Link>
        </li>
	...
      <Route path="/profiles" component={Profiles} />
    </div>
  );
};

export default App;




리액트 라우터 부가 기능


history

history 객체는 라우트로 사용된 컴포넌트에 match, location과 함께 전달되는 props 중 하나로, 이 객체를 통해 컴포넌트 내에 구현하는 메서드에서 라우트 API를 호출할 수 있다. 예를 들어 특정 버튼을 눌렀을 때 뒤로 가거나, 로그인 후 화면을 전환하거나, 다른 페이지로 이탈하는 것을 방지해야 할 때 사용한다.

이 객체를 사용해볼 예제 컴포넌트 HistorySample을 생성하고, App 컴포넌트에도 새로운 링크와 라우트를 만들어 준다.

  • HistorySample.js
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('정말 떠나실 건가요...');
    };

    componentWillUnmount() {
        // 컴포넌트가 언마운트되면 질문을 멈춤
        if (this.unblock) {
            this.unblock();
        }
    };

    render() {
        return (
            <div>
                <button onClick={this.handleGoBack}>뒤로</button>
                <button onClick={this.handleGoHome}>홈으로</button>
            </div>
        );
    };
};

export default HistorySample;

withRouter

withRouter는 HoC(Higher-order Component)로, 라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체에 접근할 수 있게 해준다.
withRouter를 사용해볼 예제 컴포넌트 WithRouterSample을 생성해보자.

  • WithRouterSample.js
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, 4)}
                rows={7}
                readOnly
            />
            <h4>match</h4>
            <textarea
                value={JSON.stringify(match, null, 4)}
                rows={7}
                readOnly
            />
            <button onClick={() => history.push('/')}>홈으로</button>
        </div>
    );
};

export default withRouter(WithRouterSample);

위처럼 withRouter를 사용할 때는 export 할 때 컴포넌트를 함수로 감싸 준다.

📌 JSON.stringify(object, null, 4)
object를 화이트리스트 없이 4칸 들여쓰기된 문자열로 반환

라우트로 사용된 컴포넌트가 아니기 때문에 그냥 일반 컴포넌트처럼 Profile.js에서 렌더링 한다.
그럼 match에서 params까지 제대로 보이는 걸 확인할 수 있다!

Switch

switch 구문과 비슷하게 생긴 switch 컴포넌트는 여러 Route를 감싸서 그중 일치하는 단 하나의 라우트만을 렌더링시켜 준다. 모든 규칙과 일치하지 않을 때 렌더링할 Not Found 페이지를 구현할 때 쓰인다.

  • App.js
...
import { Route, Link, Switch } from 'react-router-dom';
...

const App = () => {
  return (
	...
      <Switch>
        <Route path="/" component={Home} exact={true} />
        <Route path={['/about', '/info']} component={About} />
        <Route path="/profiles" component={Profiles} />
        <Route path="/history" component={HistorySample} />
        <Route
          render={({ location }) => (
            <div>
              <h2>이 페이지는 존재하지 않습니다.</h2>
              <p>{location.pathname}</p>
            </div>
          )}
        />
      </Switch>
    </div>
  );
};

...

NavLink

Link와 비슷한 NavLink에는 특정 스타일 혹은 CSS 클래스를 적용할 수 있다는 특징이 있다.

  • Profiles.js
...
import { NavLink, Route } from "react-router-dom";
...

const Profiles = () => {
    const activeStyle = {
        background: 'darkgray',
        color: 'white'
    };
    return (
        <div>
            <h3>사용자 목록: </h3>
            <ul>
                <li>
                    <NavLink activeStyle={activeStyle} to="/profiles/dazzlynnnn">dazzlynnnn</NavLink>
                </li>
                <li>
                    <NavLink activeStyle={activeStyle} to="/profiles/gildong">gildong</NavLink>
                </li>
            </ul>
	...
    	<div>
    );
};

export default Profiles;

좋은 웹페이지 즐겨찾기