리액트 라우터로 SPA 개발

  • SPA
    Single Page Action
    한개의 페이지로 이뤄진 애플리케이션
    전통적 웹 패이지는 여러 페이지로 구성되어 있다
    사용자가 다른 페이지로 이동시마다 새로운 html 받아오고
    로딩시마다 서버에서 리소스 전달받아 해석한 뒤 화면에 보여줌

즉 보이는 화면은 서버에서 준비했음

요즘은 웹에서 제공 정보 매우 많기 때문에 서버가 모든 뷰 준비하면 부하 가능성

따라서 리액트 같은 라이브러리나 프레임워크 사용해 뷰 렌더링을 사용자의 브라우저가 담당하게 하고 인터랙션 발생시 필요한 부분만 js 사용해 업데이트 해줌
새 데이터 필요시 서버 API 호출해 필요만 데이터만 새로 불러와 사용

싱글 페이지라고 해서 화면 한 종류만인것은 아님
SPA의 경우 서버에서 제공하는 페이지는 한 종류이지만 해당 페이지에서 로딩된 js와 현 사용자 브라우저 주소 상태에 따라 다양한 화면 보여줄 수 있음

  • 라우팅
    다른 주소에 다른 화면 보여주는 것
    리액트 자체에는 이 기능X
    but 브라우저의 API 직접 사용해 관리하거나 라이브러리 사용해 구현 가능

리액트 라우팅 라이브러리로 리액트 라우터(react-router), 리치 라우터(reach-router), Next.js 등이 있다

리액트 라우터는 클라이언트 사이드에서 이뤄지는 라우팅 아주 간단하게 구현 도와줌
서버 사이드 렌더링 돕는 컴포넌트도 제공

  • SPA 단점
    앱 규모 커지면 js파일 너무 커짐
    페이지 로딩 시 사용자가 실제로 방문하지 않을 수도 있는 페이지 스크립트도 불러오기 때문

리액트 라우터처럼 브라우저에서 js 사용해 라우팅 관리하는 것은 js 실행않는 일반 크롤러에서는 페이지 정보 제대로 수집 못한다는 단점
=> 서버 사이드 렌더링 통해 해결 가능

프로젝트에 리액트 라우터 적용시에는 BrowserRouter 컴포넌트 사용해 감싸면 됨
이 컴포넌트는 웹 앱에 html5의 history api를 사용해 페이지를 새로고침 하지 않고도 주소 변경하고
현 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있게 해줌

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById(‘root‘)
);
  • Route 컴포넌트로 특정 주소에 컴포넌트 연결
    Route 컴포넌트 사용해 사용자의 현재 경로에 따라 다른 컴포넌트를 보여줌
    어떤 규칙 가진 경로에 어떤 컴포넌트 보여줄지 정의할 수 있음

다음과 같은 식으로 사용한다.

<Route 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} /> {/*맨 처음에는 Home 컴포넌트 보여줌*/}
    <Route path = '/about' component = {About} /> {/* /about 주소에서는 About 컴포넌트 보여줌*/}
  </div>
  )
}

/about 경로를 들어가면 About 컴포넌트만 나오는것이 아니라 두 컴포넌트 모두 나타난다.
/about 경로가 / 규칙에도 일치하기 때문
이를 수정하려면 Home을 위한 Route 컴포넌트를 사용할 때 exact 라는 props를 true로 설정하면 됨

<Route path = '/' component = {Home} exact = {true}/>
  • Link 컴포넌트 사용해 다른 주소로 이동하기
    Link 컴포넌트는 클릭하면 다른 주소로 이동시켜주는 컴포넌트

일반 웹 앱에서는 a 태그 사용해 페이지 전환
but 리액트 라우터 사용시에는 a 태그 직접 사용하면 안됨
a 태그는 페이지 전환 과정에서 페이지를 새로 불러오기 때문에 앱이 들고있언 상태들을 모두 날려 버림
즉 렌더링된 컴포넌트들도 모두 사라지고 다시 처음부터 렌더링 해야 함

Link 컴포넌트 사용해 페이지 전환하면 페이지 새로 불러오지 않고 앱은 그대로 유지한 상태에서 html5 history API 사용해 페이지의 주소만 변경해줌
Link 컴포넌트 자체는 a 태그로 이뤄져 있지만 페이지 전환 방지 기능 내장되어 있음.

다음과 같은 식으로 사용한다.

<Link to = '주소'>내용</Link>

App 컴포넌트에서 '/', '/about' 경로로 이동하는 Link 컴포넌트

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}/> {/*맨 처음에는 Home 컴포넌트 보여줌*/}
    <Route path = '/about' component = {About} /> {/* /about 주소에서는 About 컴포넌트 보여줌*/}
  </div>
  )
}

export default App;


링크를 눌러 페이지를 전환할 수 있다.

  • Route 하나에 여러개의 path 설정하기
    Route 하나에 여러 path 지정하는 것은 리액트 라우터 v5 부터 적용된 기능
    이전 버전에서는 여러 path에 같은 컴포넌트 보여주려면 Route를 여러번 사용했다.
<Route path = '/about' component = {About} />
<Route path = '/info' component = {About} />

그 대신 path props를 배열로 설정해주면 여러 경로에서 같은 컴포넌트를 보여 줄 수 있다.

<Route path = {['/about', '/info']} component={About} /> {/*배열로 설정*/}
  • URL 파라미터쿼리
    페이지 주소를 정의할 때 유동적인 값을 전달해야 할 때가 있다.
    이는 파라미터와 쿼리로 나눌 수 있다
    • 파라미터 예시 : /profiles/gildong
    • 쿼리 예시 : /about?details=true

유동적인 값을 사용할 때 무엇을 사용할 지 규칙은 없지만
일반적으로 아이디나 이름 사용하여 조회시 파라미터, 키워드 검색이나 페이지에 필요힌 옵션 전달 시 쿼리를 사용한다.

  • URL 파라미터
    Profile 페이지에서 파라미터 사용해보기.
    /profile/gildong과 같은 형식으로 뒷부분에 유동적인 username 값을 넣어 줄 때 해당 값을 props로 받아와 조회하는 방법 알아보자.
// Profile.js
import React from 'react';

const data = {
    tom : { 
        name: Thomas,
        description: '리액트 개발자'
    },
    gildong : {
        name: '홍길동',
        description: '홍길동전 주인공'
    }
};

 // url 파라미터 사용시 라우트로 사용되는 컴포넌트에서
 // 받아오는 match라는 객체 안의 params값을 참조.
 // match 객체 안에는 현재 컴포넌트가 어떤 경로 규칙에
 // 의해 보이는지에 대한 정보 들어있음.
const Profile = ({match}) => {
    const {username} = match.params;
    const profile = data[username];
    if (!profile) {
        return <div>존재하지 않는 사용자입니다.</div>;
    }

    return (
        <div>
            <h2>
                {username}({profile.name})
            </h2>
            <p>{profile.description}</p>
        </div>
    )
}

export default Profile;

이제 App 컴포넌트에서 Profile 컴포넌트를 위한 라우트를 정의.
이번에 사용할 path 규칙은 /profiles/: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>
        <Link to = "/">!</Link>
      </li>

      <li>
        <Link to = '/about'>소개!</Link>
      </li>

      <li>
        <Link to = '/profile/tom'>tom 프로필</Link>
      </li>

      <li>
        <Link to = '/profile/gildong'>gildong 프로필</Link>
      </li>
    </ul>
    <hr />
    <Route path = '/' component = {Home} exact = {true}/> {/*맨 처음에는 Home 컴포넌트 보여줌*/}
    <Route path = {['/about', '/info']} component={About} /> {/*배열로 설정*/}
    <Route path = '/profile/:username' component={Profile} /> {/*match.params.username 값을 통해 현재 username 값 조회 가능*/}
  </div>
  )
}

export default App;

  • URL 쿼리
    About 페이지에서 쿼리를 받아오자.
    쿼리는 location 객체에 들어있는 search 값에서 조회할 수 있다.
    location 객체는 라우트로 사용된 컴포넌트에게 props로 전달되며 (match 처럼!) 웹 애플리케이션의 현재 주소에 대한 정보 갖고있음

location은 다음과 같은 형태.

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

위 location객체는 http://localhost:3000/about?detail=true 주소로 들어갔을 때의 값.

url 쿼리를 읽을때 위 객체에서 search값을 확인해야 한다.
문자열로 이뤄져있고 '?detail=true&another=1' 처럼 여러가지 값을 설정할 수 있다.
search에서 특정 값을 읽어오려면 문자열을 객체로 변환해줘야 한다.

쿼리 문자열 객체로 변환하는데 qs 라이브러리가 쓰인다.

import React from 'react';
import qs from 'qs';
// About.js
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>
    )
}
// 브라우저에서 http://localhost:3000/about?detail=true 주소로 들어가면 위 문장 출력됨.
export default About;

쿼리 문자열을 객체로 파싱하는 과정에서 결과값은 언제나 문자열이라는 점 주의하자.
?value=1 이나 ?value=true 처럼 숫자나 bool도 문자열로 받아짐.

  • 서브 라우트
    서브 라우트는 라우트 내부에 또 라우트를 정의하는 것.
    지금은 App에서 두 종류의 프로필 링크 보여주고 있다.
    이를 잘라내서 프로필 링크를 보여주는 Profiles 라우터 컴포넌트를 만들고,
    그 안에서 Profile 컴포넌트를 서브 라우트로 사용하도록 작성해보자.
import React from 'react';
import {Link, Route} from 'react-router-dom';
import Profile from './Profile';
// Profiles.js
const Profiles = () => {
    return (
        <div>
            <h3>사용자 목록:</h3>
            <ul>
                <li><Link to = '/profiles/tom'></Link></li>
                <li><Link to = '/profiles/gildong'>길동</Link></li>
            </ul>
            {/* component 대신 render라는 props 넣었다. 컴포넌트 자체 전달하는것이 아니라
            보여주고 싶은 jsx 넣을 수 있다. jsx에서 props 설정 시 값 생략하면 자동으로 true 설정됨
            현재 exact={true} 대신 exact만 써도 같은 의미 */}
            <Route path = '/profiles' exact render = {() => <div>사용자를 선택</div>} />
            <Route path = '/profiles/:username' component={Profile} />
        </div>
    )
}

export default Profiles;
// App.js
...
<li><Link to = '/profiles'>프로필</Link></li>
...
<Route path = '/profiles' component={Profiles} />


  • 리액트 라우터 부가 기능
  1. history
    history 객체는 라우터에 사용된 컴포넌트에 match, location과 함께 전달되는 props로
    컴포는테 내에 구현하는 메서드에 라우터 API 호출할 수 있다.
    ex) 특정 버튼 누를시 뒤로 가거나, 로그인 후 화면 전환 등등에 사용.

history를 사용하는 페이지를 만들어보자.

import React, {Component} from 'react';
// HistorySample.js
class HistorySample extends Component {
    // 뒤로 가기
    handleGoBack = () => {
        this.props.history.goBack();
    };

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

    componentDodMount() {
        // 페이지에 변화 생길때마다 정말 나갈것인지 질문
        this.unblock = this.props.history.block('Are you sure?');
    };

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


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

export default HistorySample;

App에서 /history 경로에 보이도록 설정.

...
import HistorySample from './HistorySample';
...
<li><Link to = '/history'>History 예제</Link></li>
...
<Route path = '/history' component={HistorySample} />


  1. withRouter
    withRouter 함수는 HoC(Higher-order Component)로 라우트로 사용된 컴포넌트가 아니어도
    math, location, history 객체에 접근할 수 있게 해줌.
import React from "react";
import { withRouter } from "react-router-dom";
// WithRouterSample.js
const WithRouterSample = (({location, match, history}) => {
    return (
        <div>
            <h4>location</h4>
            <textarea value={JSON.stringify(location, null, 2)}
            rows={7}
            readOnly={true} />
            <button onClick = {() => history.push('/')}>홈으로</button>
        </div>
    )
})
// withRouter 사용할때는 컴포넌트 내보낼 때 함수로 감싸 줌
export default withRouter(WithRouterSample);

이 컴포넌트를 Profiles 컴포넌트에 렌더링 해보자.

// Profiles.js
...
<div>
	<WithRouterSample />
...


그런데 match의 params 가 비어있다.
withRouter 는 현재 자신을 보여주고 있는 라우트 컴포넌트(현재 Profiles)를 기준으로 match가 전달됨.
Profiles를 위한 라우트를 설정할때 path="/profiles" 라고만 입력했으므로 username 파라미터를 읽어오지 목하는 상태.

WIthRouterSample 컴포넌트를 Profiles에서 지우고 Profile에 넣으면 match에 url 파라미터가 제대로 전달됨.

// Profile.js
...
<div>
	<WithRouterSample />
...


params.username이 제대로 출력된다.

  1. Switch
    Switch 컴포넌트는 여러 Route를 감싸고 그중 일치하는 하나만을 렌더링한다.
    모든 규칙에 안맞을때 Not Found 페이지도 구현할 수 있다.
import React from 'react';
import {Route, Link, Switch} from 'react-router-dom';
import About from './About';
import Home from './Home';
import Profiles from './Profiles';
import HistorySample from './HistorySample';
// App.js
const App = () => {
  return (
    <div>
      <ul>
        <li><Link to = '/'>!</Link></li>
        <li><Link to = '/about'>about!</Link></li>
        <li><Link to = '/about2'>about2!</Link></li>
        <li><Link to = '/profiles'>프로필</Link></li>
        <li><Link to = '/history'>History 예제</Link></li>
      </ul>
      <hr/>
      <Switch>
        <Route path = '/' component={Home} exact = {true}/>
        <Route path = {['/about', '/about2']} 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>
  )
}

export default App;

  1. NavLink
    Link와 비슷한 역할.
    현재 경로와 Link에서 사용하는 경로가 일치하는 경우 특정 스타일 작용할 수 있는 클래스
  • 정리
    리액트 라우터를 사용해 주소 경로에 따라 다양한 페이지 보여주는 방법 알아보았다.
    큰 규모 프로젝트를 진행하다보면 한가지 문제점이 발생한다.
    여러 컴포넌트, 로직, 함수들이 쌓이면 최종 js파일의 크기가 매우 커진다는 것.

위에서도 사용자가 /about 들어왔을때 당장 필요하지 않은 Profile 컴포넌트까지 불러옴.
다른 컴포넌드는 필요한 시점에만 불러오는것이 효율적.
=> 코드 스플리팅 통해 해결 가능

좋은 웹페이지 즐겨찾기