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

WooBuntu·2020년 9월 29일
0

SPA

  • 라우팅 : 다른 주소에 다른 뷰를 보여 주는 것

  • 전통적인 웹 페이지에서는 다른 뷰를 렌더링 할 때마다, 서버에서 html을 받아왔다.

  • 반면 SPA에서는 애플리케이션을 브라우저에 불러와서 실행시킨 이후, 뷰의 렌더링을 서버가 아닌 브라우저에서 자바스크립트로 제어한다.
    (SPA에서는 html파일이 하나이다)

  • 리액트 라이브러리에는 라우팅 기능이 내장되어 있지 않기에, 브라우저의 API를 직접 사용하거나 라이브러리를 사용한다.

  • react-router-dom 라이브러리는 클라이언트 사이드 라우팅뿐만 아니라, 서버 사이드 렌더링에도 라우팅을 도와주는 컴포넌트들을 제공한다.

SPA의 단점

  • 앱의 규모가 커지면 자바스크립트 파일이 너무 비대해진다.
    (페이지 로딩 시에 사용자가 실제로 방문하지 않을 수도 있는 페이지의 스크립트까지 전부 불러오기 때문)

    • 이는 코드 스플리팅을 통해 라우트별로 파일들을 나눠 트래픽과 로딩 속도를 개선할 수 있다.
  • 브라우저에서 자바스크립트를 사용하여 라우팅을 관리하는 것은 자바스크립트를 실행하지 않는 일반 크롤러에서는 페이지의 정보를 제대로 수집해가지 못한다.

  • 또한, 자바스크립트가 실행될 때까지 페이지가 비어 있기 때문에 자바스크립트 파일이 로딩되어 실행되는 짧은 시간 동안 흰 페이지가 나타날 수 있다

    • 위의 두 문제점은 서버 사이드 렌더링을 통해 해결이 가능하다.

프로젝트에 라우터 적용하기

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

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root'),
);
// ...
  • 프로젝트에 리액트 라우터를 적용할 때는 src/index.js파일에서 react-router-dom에 내장되어 있는 BrowserRouter컴포넌트를 사용하여 최상위 컴포넌트를 감싸면 된다.

  • BroswerRouter컴포넌트

    • 웹 애플리케이션에 HTML5의 History API를 사용하여 페이지를 새로고침하지 않고도 주소를 변경하고,

    • 현재 주소에 관련됭 정보를 props로 쉽게 조회하거나 사용할 수 있도록 해준다.

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

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

// ...
<Route path="주소" component={보여 줄 컴포넌트} />
// ...

주소 규칙 식별

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

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

export default App;
  • 위 예제에서 첫번째 Route컴포넌트에는 exact prop에 true값을 전달해주었다.

  • exact prop에 true값을 전달해주지 않으면 주소 규칙 상 "/"가 "/about"에 포함되기 때문에 "/about" 주소값에서 두 컴포넌트가 같이 렌더링되기 때문이다.

  • 리액트에서도 a태그를 사용할 수는 있으나, 이 경우 페이지를 전환하는 과정에서 페이지를 새로 불러오기 때문에 애플리케이션이 들고 있던 상태들을 모두 날려버리게 된다.
    (렌더링된 컴포넌트들도 모두 사라지고 처음부터 다시 렌더링하게 된다)

  • Link 컴포넌트는 페이지를 새로 불러오지 않고 애플리케이션은 그대로 유지한 상태에서 HTML5 History API를 사용하여 페이지의 주소만 변경해 준다.

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

const App = () => {
  return (
    <>
      <ul>
        <li>
          <Link to="/"></Link>
        </li>
        <li>
          <Link to="/about">소개</Link>
        </li>
      </ul>
      <Route path="/" component={Home} exact={true} />
      <Route path="/about" component={About} />
    </>
  );
};

export default App;

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

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

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

export default App;

URL 파라미터와 쿼리

  • 페이지 주소를 동적으로 정의해야 하는 경우

URL 파라미터

<Route path="/somePath/:someParam" component={someComponent} />
  • Route컴포넌트의 주소값에 "~주소값/:파라미터이름"과 같은 형식으로 설정해주면,
<Link to={`/somePath/${파라미터값}`}>다른 뷰</Link>
  • 이와 같이 동적으로 파라미터 값을 전달해줄 수 있다.
const someComponent = ({ match }) => {
  const { someParam } = match.params;
  // ...
}
  • 전달된 파라미터 값은 해당 컴포넌트의 match prop의 params로 접근하여 사용할 수 있다.

URL 쿼리

  • URL 파라미터 때와는 달리 쿼리의 경우 Route 컴포넌트의 주소값에는 변동이 없다.
<Link to={`/somePath?someQuery=${쿼리값}`}>다른 뷰</Link>
  • 이렇게 Link에 동적으로 쿼리값을 전해줄 수도 있고, 아니면 직접 주소값을 쿼리 형식에 맞춰 입력할 수도 있다.
// ...
import qs from "qs";
// ...

const someComponent = ({ location }) => {
  const query = qs.parse(location.search, {
    ignoreQueryPrefix: true // 쿼리 문자열 맨 앞의 ?를 생략
  });
  const { someQuery } = query;
  // ...
}
  • 전달된 쿼리 값은 해당 컴포넌트의 location prop의 search로 접근하여 사용할 수 있는데, 이 값이 쿼리 문자열이기 때문에 qs라이브러리를 이용하여 객체로 변환시켜주는 것이 일반적이다.

  • 주의할 점은, qs라이브러리를 통한 변환 과정에서 각 쿼리 값은 전부 문자열로 변환되므로 숫자 자료형이나 boolean 자료형을 쿼리 값으로 넘겨준 경우 이를 고려해줘야 한다는 것이다.

서브 라우트

  • 라우트 내부에 또 라우트를 정의하는 것
    (=라우트로 사용되고 있는 컴포넌트의 내부에 Route 컴포넌트를 또 사용하는 것)

  • 아래의 예제는 라우트 내부에 또 라우트를 설정한 예시이다.

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/woobuntu">woobuntu</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 prop대신 render prop을 전달했는데, 위와 같이 따로 컴포넌트를 만들기 애매한 상황이나 컴포넌트에 props를 별도로 넣어 주고 싶을 때도 사용할 수 있다.

리액트 라우터 부가 기능

history

  • 라우트로 사용된 컴포넌트에는 match, location외에도 history라는 객체가 prop으로 전달된다.

  • 이 객체를 통해 컴포넌트 내의 메서드에서 라우터 API를 호출할 수 있다.

    • 뒤로 가기
    • 로그인 후 화면 전환
    • 다른 페이지로의 이탈 방지
  • 아래는 클래스형 컴포넌트에서의 history객체 사용 예제이다.

import React, { Component } from 'react';

class HistoryClass extends Component {
  handleGoBack = () => {
    this.props.history.goBack();
  };

  handleGoHome = () => {
    this.props.history.push('/');
  };

  // 컴포넌트가 마운트된 뒤 컴포넌트 인스턴스의 unblock property에 함수 설정
  componentDidMount() {
    this.unblock = this.props.history.block('정말 떠나시겠습니까?');
  }

  // 컴포넌트가 언마운트되기 직전에 컴포넌트 인스턴스에 unblock property가 있다면
  // 해당 property를 함수로써 호출
  componentWillUnmount() {
    if (this.unblock) {
      this.unblock();
    }
  }
  render() {
    return (
      <div>
        <button onClick={this.handleGoBack}>뒤로</button>
        <button onClick={this.handleGoHome}>홈으로</button>
      </div>
    );
  }
}

export default HistoryClass;
  • 책에는 클래스형 컴포넌트에서의 사용 예제만 나와 있어서, 함수형 컴포넌트에서의 사용 예제를 직접 짜봤다.
import React, { useEffect, useRef } from 'react';

const HistoryFunction = ({ history }) => {
  const handleGoBack = () => {
    history.goBack();
  };

  const handleGoHome = () => {
    history.push('/');
  };

  let unblock = useRef(undefined);

  function stopAsking() {
    if (unblock.current) {
      unblock.current();
    }
  }

  useEffect(() => {
    unblock.current = history.block('정말 떠나시겠습니까?');
    return stopAsking;
  }, [history]);

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

export default HistoryFunction;
  1. 함수형 컴포넌트에서 this 바인딩은 최대한 지양하는 편이 좋다고 생각하기 때문에, unblock을 지역 변수로 삼아 렉시컬 스코프를 활용했다.
    (애초에 이 접근부터 틀린 건 아닌지 모르겠다)

  2. useEffect의 dependency로 빈 배열을 전달해주어야 콜백 내부의 코드가 마운트된 이후에만 실행되며, 뒷정리 함수는 언마운트 직전에 실행된다.

    • dependency에 특정 값을 전달하면 해당 값의 업데이트 직전에 뒷정리 함수가 실행되고, 업데이트 시점에 콜백 내부의 코드가 실행된다.

    • 다만 history객체가 업데이트 되는 경우는 라우트를 통해 해당 컴포넌트에 들어갈 때와 나올 때이므로, 이 경우엔 dependency에 history가 있는 것이 문제가 되지 않는다고 판단했다

  3. 함수형 컴포넌트던, 클래스형 컴포넌트던 'history.goBack()'에는 history.block이 작동하지 않는데 이유를 모르겠다.

withRouter

  • withRouter 함수는 HoC(Higher-order Component)이다.
    (무슨 말일까)

  • 라우트로 사용된 컴포넌트가 아니어도 match, location, history prop에 접근할 수 있게 해준다.

// ...
import { withRouter } from "react-router-dom";
// ...

const someComponent = ({ location, match, history }) => {
  //...
}

export default withRouter(someComponent);
  • 다만 withRouter를 사용하면 현재 자신(someComponent)를 보여 주고 있는 라우트 컴포넌트를 기준으로 match가 전달된다는 것에 유의해야 한다.

Switch

  • 우리가 흔히 아는 조건문 switch로 생각해도 무방하다

  • 여러 Route를 감싸서 그 중 현재 주소값과 일치하는 단 하나의 라우트만을 렌더링시킨다.

  • 모든 규칙과 일치하지 않을 때 보여줄 Not Found 페이지의 구현에도 사용할 수 있다.

// ...
import { Switch } from "react-router-dom";
// ...

const someComponent = () => {
  return (
    <>
      // ...
      <Switch>
        // Route 컴포넌트들 나열
        <Route
  	  render={({ location }) => (/* 404페이지에 사용할 JSX */)} />
      </Switch>
    </>
  );
};
  • NavLink는 현재 주소값과 Link의 주소값이 일치하는 경우 특정 스타일 혹은 CSS를 적용할 수 있는 컴포넌트이다.

    • activeStyle : 링크가 활성화되었을 때의 스타일

    • activeClassName : 링크가 활성화되었을 때의 CSS클래스를 props로 전달하는 용도

// ...
import { NavLink } from "react-router-dom";
// ...

const someComponent = () => {
  const activeStyle = {
    // 활성화되었을 때의 style
  };
  return (
    <>
      // ...
      <NavLink activeStyle={activeStyle} to="주소값">
    	뭐시기
      </NavLink>
      // ...
    </>
  );
};

0개의 댓글