WEB🔨/React

리액트를 다루는 기술 요약#7

최문경 블로그 2021. 6. 26. 14:27

해당 시리즈는 제목 그대로 김민준님의 '리액트를 다루는 기술'을 요약한 글입니다.

 

목차

13장 리액트 라우터로 SPA 개발하기

 

 

13장 리액트 라우터로 SPA 개발하기

13.1 SPA란?

단일 페이지 애플리케이션(Single Page Application, SPA)는 모던 웹의 패러다임이다. SPA는 기본적으로 단일 페이지로 구성되며 기존의 서버 사이드 렌더링과 비교할 때, 배포가 간단하며 네이티브 앱과 유사한 사용자 경험을 제공할 수 있다는 장점이 있다.

link tag를 사용하는 전통적인 웹 방식은 새로운 페이지 요청 시마다 정적 리소스가 다운로드되고 전체 페이지를 다시 렌더링하는 방식을 사용하므로 새로고침이 발생되어 사용성이 좋지 않다. 그리고 변경이 필요없는 부분를 포함하여 전체 페이지를 갱신하므로 비효율적이다.

SPA는 기본적으로 웹 애플리케이션에 필요한 모든 정적 리소스를 최초에 한번 다운로드한다. 이후 새로운 페이지 요청 시, 페이지 갱신에 필요한 데이터만을 전달받아 페이지를 갱신하므로 전체적인 트래픽을 감소할 수 있고, 전체 페이지를 다시 렌더링하지 않고 변경되는 부분만을 갱신하므로 새로고침이 발생하지 않아 네이티브 앱과 유사한 사용자 경험을 제공할 수 있다.

모바일의 사용이 증가하고 있는 현 시점에 트래픽의 감소와 속도, 사용성, 반응성의 향상은 매우 중요한 이슈이다. SPA의 핵심 가치는 사용자 경험(UX) 향상에 있으며 부가적으로 애플리케이션 속도의 향상도 기대할 수 있어서 모바일 퍼스트(Mobile First) 전략에 부합한다. - PoiemaWeb (https://poiemaweb.com/js-spa#1-spa-single-page-application)

다른 주소에 다른 화면을 보여 주는 것을 라우팅이라고 하는데, 리액트 라우팅 라이브러리는 리액트 라우터(react-router), 리치 라우터(reach-router), Next.js 등 여러 가지가 있습니다. 이 책에서는 그중 역사가 가장 길고 사용 빈도가 높은 리액트 라우터를 사용하겠습니다.

 

SPA의 단점은 앱의 규모가 커지면 자바스크립트 파일이 너무 커지는데 이것은 페이지 로딩 시 사용자가 실제로 방문하지 않을 수도 있는 페이지의 스크립트도 불러오기 때문입니다. 하지만, 나중에 배울 코드 스플리팅을 사용하면 라우트별로 파일들을 나누어서 트래픽과 로딩 속도를 개선할 수 있습니다.

 

그리고 SPA는 서버 사이드 렌더링 방식이 아닌 자바스크립트 기반 비동기 모델(클라이언트 사이드 렌더링 방식)이기 때문에 자바스크립트를 실행하지 않는 일반 크롤러에서는 페이지의 정보를 제대로 수집해 가지 못합니다. 따라서, SEO(검색 엔진 최적화)에서 문제점이 생기는데 나중에 배우게 될 서버 사이드 렌더링을 통해 해결이 가능합니다.

 

 

13.2 프로젝트 준비 및 기본적인 사용법

흐름

  1. 프로젝트 생성 및 리액트 라우터 적용
  2. 페이지 만들기
  3. Route 컴포넌트로 특정 주소에 컴포넌트 연결
  4. 라우트 이동하기
  5. URL 파라미터와 쿼리 이해하기
  6. 서브 라우트
  7. 부가 기능 알아보기

 

13.2.1. yarn add react-router-dom

 

 

13.2.2. 프로젝트에 라우터 적용

src/index.js 파일에서 react-router-dom 에 내장되어 있는 BrowserRouter라는 컴포넌트로 <App />을 감싸면 됨. 이 컴포넌트는 웹 애플리케이션에 HTML5의 History API를 사용하여 페이지를 새로고침하지 않고도 주소를 변경하고, 현재 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있도록 해줌.

 

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';

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

 

 

13.2.3. 페이지 만들기

import React from 'react';

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
      <p>
        Lorem ipsum.......
      </p>
    </div>
  );
};

export default Home;
import React from 'react';

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        Lorem ipsum.......
      </p>
    </div>
  );
};

export default About;

 

 

13.2.4. Route 컴포넌트로 특정 주소에 컴포넌트 연결

Route라는 컴포넌트를 사용하여 사용자의 현재 경로에 따라 다른 컴포넌트를 보여 줄 수 있다.

<Route path="주소규칙" component={보여 줄 컴포넌트} />
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './Home';
import About from './About';

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

export default App;

 



그런데 /about 경로로 들어갔을 때는 Home과 About 둘 다 나타나는 것을 볼 수 있습니다.

/about 경로가 / 규칙에도 일치하기 때문에 그렇습니다.

Home의 Route에 exact라는 props를 true로 설정하면 됩니다.

 

import React from 'react';
import { Route } from 'react-router-dom';
import Home from './Home';
import About from './About';

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

export default App;


 

 

13.2.5. Link 컴포넌트를 사용하여 다른 주소로 이동하기

Link 컴포넌트는 클릭하면 다른 주소로 이동시켜 주는 컴포넌트입니다. Link 컴포넌트를 사용하여 페이지를 전환하면, 페이지를 새로 불러오지 않고 애플리케이션은 그대로 유지한 상태에서 HTML5 History API를 사용하여 페이지의 주소만 변경해 줍니다. Link 컴포넌트 자체는 a 태그로 이루어져 있지만, 페이지 전환을 방지하는 기능이 내장되어 있습니다.

 

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

function App() {
  return (
    <div>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
      <Route path="/" component={Home} exact />
      <Route path="/about" component={About} />
    </div>
  );
}

export default App;

 

 

13.3 Route 하나에 여러 개의 path 설정하기

아래와 같이 path props를 배열로 설정해 주면 여러 경로에서 같은 컴포넌트를 보여 줄 수 있습니다.

import React from 'react';
import { Route } from 'react-router-dom';
import Home from './Home';
import About from './About';

function App() {
  return (
    <div>
      <Route path="/" component={Home} exact />
      <Route path={['/about', '/info']} component={About} />
    </div>
  );
}

export default App;

 

 

13.4 URL 파라미터와 쿼리

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

  • 파라미터 예시: /profile/velopert
  • 쿼리 예시: /about?details=true

일반적으로 파라미터는 특정 아이디 혹은 이름을 사용하여 조회할 때 사용하고, 쿼리는 어떤 키워드를 검색하거나 페이지에 필요한 옵션을 전달할 때 사용합니다.

 

 

13.4.1 URL 파라미터

URL 파라미터를 사용할 때는 아래와 같이 라우트로 사용되는 컴포넌트에서 받아 오는 match라는 객체 안의 params 값을 참조합니다. 

import React from 'react';

const data = {
  velopert: {
    name: '김민준',
    desc: '리액트를 좋아하는 개발자',
  },
  gildong: {
    name: '홍길동',
    desc: '고전 소설 홍길동전의 주인공',
  },
};

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.desc}</p>
    </div>
  );
};

export default Profile;
import React from 'react';
import { Route, Link } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Profile from './Profile';

function App() {
  return (
    <div>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/profile/velopert">velopert 프로필</Link>
        </li>
        <li>
          <Link to="/profile/gildong">gildong 프로필</Link>
        </li>
      </ul>
      <Route path="/" component={Home} exact />
      <Route path={['/about', '/info']} component={About} />
      <Route path="/profile/:username" component={Profile} />
    </div>
  );
}

export default App;

 

 

13.4.2 URL 쿼리

쿼리는 location 객체에 들어 있는 search 값에서 조회할 수 있습니다.

 

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

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

URL 쿼리를 읽을 때는 위 객체가 지닌 값 중에서 search 값을 확인해야 합니다. 이 값은 문자열 형태로 되어 있는데 search 값에서 특정 값을 읽어 오기 위해서는 이 문자열을 객체 형태로 변환해야 합니다.

 

qs라는 라이브러리를 사용해 쿼리 문자열을 객체로 변환할 수 있습니다.

 

yarn add qs

 

import React from 'react';
import qs from 'qs';

const About = ({ location }) => {
  console.log(location); // { "pathname": "/about", "search": "?detail=true", "hash": "" }
  const query = qs.parse(location.search, {
    ignoreQueryPrefix: true,
  });
  console.log(query); // { detail: "true" }
  const showDetail = query.detail === 'true';
  return (
    <div>
      <h1>About</h1>
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsum
        aspernatur quas, voluptatum perspiciatis itaque aliquam alias fuga
        laudantium. Et ut nam molestiae quisquam quam nesciunt nihil nisi sequi
        asperiores quibusdam!
      </p>
      {showDetail && <p>detail 값이 true입니다!</p>}
    </div>
  );
};

export default About;

 

 

13.5 서브 라우트

서브 라우트는 라우트 내부에 또 라우트를 정의하는 것을 의미합니다. 기존의 App 컴포넌트에서는 두 종류의 프로필 링크를 보여 주었는데요. 이를 잘라내서 프로필 링크를 보여 주는 Profiles라는 라우트 컴포넌트를 따로 만들고, 그 안에서 Profile 컴포넌트를 서브 라우트로 사용하도록 코드를 작성해봅시다.

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/velopert">velopert 프로필</Link>
        </li>
        <li>
          <Link to="/profiles/gildong">gildong 프로필</Link>
        </li>
      </ul>
      <Route
        path="/profiles"
        exact
        render={() => <div>사용자를 선택해 주세요.</div>}
      />

      <Route path="/profiles/:username" component={Profile} />
    </div>
  );
};

export default Profiles;

첫 번째 Route 컴포넌트에는 component 대신 render 라는 props를 넣어주었는 데, 지금처럼 따로 컴포넌트를 만들기 애매한 상황에 사용할 수 있습니다.

 

import React from 'react';
import { Route, Link } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Profiles from './Profiles';

function App() {
  return (
    <div>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/profiles">프로필</Link>
        </li>
      </ul>
      <Route path="/" component={Home} exact />
      <Route path={['/about', '/info']} component={About} />
      <Route path="/profiles" component={Profiles} />
    </div>
  );
}

export default App;

 

 

13.6 리액트 라우터 부가 기능

13.6.1 history

history 객체는 match, location과 함께 전달되는 props 중 하나로, 라우터 API를 호출할 수 있습니다. 예를 들어, 특정 버튼을 눌렀을 때 뒤로 가거나, 로그인 후 화면을 전환하거나, 다른 페이지로 이탈하는 것을 방지해야할 때 history를 활용합니다.

import React, { Component } from 'react';

export default 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>
    );
  }
}
import React from 'react';
import { Route, Link } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Profiles from './Profiles';
import HistorySample from './HistorySample';

function App() {
  return (
    <div>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/profiles">프로필</Link>
        </li>
        <li>
          <Link to="/history">History 예제</Link>
        </li>
      </ul>
      <Route path="/" component={Home} exact />
      <Route path={['/about', '/info']} component={About} />
      <Route path="/profiles" component={Profiles} />
      <Route path="/history" component={HistorySample} />
    </div>
  );
}

export default App;


 

13.6.2 withRouter

withRouter 함수는 HoC(Higher-order Component)입니다. 라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체에 접근할 수 있게 해 줍니다. (현재 자신을 보여 주고 있는 라우트 컴포넌트를 기준으로 전달)

 

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

const WithRouterSample = ({ location, match, history }) => {
  return (
    <div>
      <h3>location</h3>
      <textarea value={JSON.stringify(location, null, 2)} rows="7" readOnly />
      <h3>match</h3>
      <textarea value={JSON.stringify(match, null, 2)} rows="7" readOnly />
      <button onClick={() => history.push('/')}>홈으로</button>
    </div>
  );
};

export default withRouter(WithRouterSample);

JSON.stringify의 두 번째 파라미터와 세 번째 파라미터를 null, 2로 설정해 주면 JSON에 들여쓰기가 적용된 상태로 문자열이 만들어집니다.

 

import React from 'react';
import { withRouter } from 'react-router-dom';
import WithRouterSample from './WithRouterSample';

( ... )

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

export default withRouter(Profile);


 

13.6.3 Switch

Switch 컴포넌트는 여러 Route를 감싸서 그중 일치하는 단 하나의 라우트만을 렌더링시켜 줍니다. Switch를 사용하면 모든 규칙과 일치하지 않을 때 보여 줄 Not Found 페이지도 구현할 수 있습니다.

 

import React from 'react';
import { Route, Link, Switch, withRouter } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Profiles from './Profiles';
import HistorySample from './HistorySample';

function App({ location }) {
  return (
    <div>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/profiles">프로필</Link>
        </li>
        <li>
          <Link to="/history">History 예제</Link>
        </li>
      </ul>
      <Switch>
        <Route path="/" component={Home} exact />
        <Route path={['/about', '/info']} component={About} />
        <Route path="/profiles" component={Profiles} />
        <Route path="/history" component={HistorySample} />
        <Route
          render={() => (
            <div>
              <h2>이 페이지는 존재하지 않습니다.</h2>
              <p>{location.pathname}</p>
            </div>
          )}
        />
      </Switch>
    </div>
  );
}

export default withRouter(App);

 

 

13.6.4 NavLink

Link와 비슷한데 경로가 일치할 경우 특정 스타일 혹은 CSS 클래스를 적용할 수 있는 컴포넌트입니다.

링크가 활성화되었을 때의 스타일을 적용할 때는 activeStyle 값을, CSS 클래스를 적용할 때는 activeClassName 값을 props로 넣어주면 됩니다.

 

import React from 'react';
import { NavLink, Route } from 'react-router-dom';
import Profile from './Profile';

const Profiles = () => {
  const activeStyle = {
    background: 'black',
    color: 'white',
  };
  return (
    <div>
      <h3>사용자 목록: </h3>
      <ul>
        <li>
          <NavLink activeStyle={activeStyle} to="/profiles/velopert">
            velopert 프로필
          </NavLink>
        </li>
        <li>
          <NavLink activeStyle={activeStyle} to="/profiles/gildong">
            gildong 프로필
          </NavLink>
        </li>
      </ul>
      <Route
        path="/profiles"
        exact
        render={() => <div>사용자를 선택해 주세요.</div>}
      />

      <Route path="/profiles/:username" component={Profile} />
    </div>
  );
};

export default Profiles;

 

 

정리

방금 만든 프로젝트는 사용자가 /about 페이지에 들어왔을 때 지금 당장 필요하지 않은 Profile 컴포넌트까지 불러옵니다. 라우트에 따라 필요한 컴포넌트만 불러오고, 다른 컴포넌트는 다른 페이지를 방문하는 등의 필요한 시점에 불러오면 더 효율적이지 않을까요? 이를 해결해주는 기술이 바로 코드 스플리팅입니다. 이에 대해서는 19장에서 다루겠습니다.