---
# System prepended metadata

title: react auth route 만들기
tags: [tech sharing]

---

# react auth route 만들기

###### tags: `tech sharing`

react router를 사용할 때 route로 사용을 하는 컴포넌트가 두 개가 있으면 좋다고 생각했다. 

1. 일반적인 route 컴포넌트
2. 로그인 여부를 확인해서 로그인이 되어 있지 않으면 로그인 페이지로 리다이렉트 하는 컴포넌트

그래서 `AuthRoute`를 만들어서 다음과 같이 구현을 했다.

```javascript=
import React, { useEffect, useState } from 'react';
import { Route, useHistory } from 'react-router-dom';

import oauthAPI from '../apis/auth';

export default function AuthRoute({ path, exact = false, component }) {
  const [authenticated, setuthenticated] = useState(false);
  const history = useHistory();

  const authCheck = async () => {
    try {
      await oauthAPI.authCheck();
      setuthenticated(true);
    } catch (error) {
      history.replace('/signin');
      alert('인증 실패했습니다. 다시 시도해주세요');
    }
  };

  useEffect(() => {
    authCheck();
  }, []);

  return <Route exact={exact} path={path} component={authenticated && component} />;
}
```

그런데 App.jsx 단에서 이 `AuthRoute`를 여러번 사용을 하면 인증을 하는 oauthAPI를 여러번 호출을 하는 것은 아닐까 하는 생각이 들었다. `Route`에서는 path와 같은 컴포넌트만 렌더링을 한다고 가정을 해도 AuthRoute 내부에서는 useEffect를 사용해서 oatuhAPI를 호출하고 있었기 때문에 여러 AuthRoute 컴포넌트를 호출하면 각 AuthRoute 컴포넌트의 useEffect 함수에서 oauthAPI가 호출이 될 수도 있을 것 같았다.

실제로 테스트를 해보니 authCheck api는 한 번만 호출이 됐다.

![](https://i.imgur.com/Z95Nd3b.png)

일반적인 컴포넌트였으면 두 번 api가 호출되는 것이 맞을 것 같은데 왜 한 번만 호출이 될까? react router가 내부적으로 어떻게 돌아가는지 알면 좋을 것 같다.

# 내부 코드

## BrowserRouter

```jsx=
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
```

`BrowserRouter`에서는 결국 `history`를 사용한 `Router`인 것 같았다. 이 `history`는 확인을 해보니 npm 라이브러리 history 모듈이었다.

## Router
`BrowserRouter` 내부에서 사용되는 `Router`이다.

```jsx=
import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";

import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };

    // This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.
    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

  componentWillUnmount() {
    if (this.unlisten) {
      this.unlisten();
      this._isMounted = false;
      this._pendingLocation = null;
    }
  }

  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}
```

먼저 `if(!props.staticContext) ...` 부분은 StaticRouter와 상관있는 부분이라고 하니 넘어가고 보면 컴포넌트가 mount 되고 나서 `_pendingLocation`이 있으면 해당 location으로 state를 변경해주었다. 컴포넌트가 unmount 되기 전에는 `_pendingLocation`을 null로 바꿔주었다.

그리고 `RouterContext`로 history, location을 내려주었다. 결국 `BrowserRouter`는 history, location의 context provider 역할을 하는 것 같다.

## Route
[참고링크](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md)
`Route` 컴포넌트는 path가 현재 url과 일치할 때 어떤 UI를 렌더링하는 역할을 한다.

`Route`를 통해서 렌더링을 할 수 있는 방법은 세가지가 있다.

1. `<Route component>`
2. `<Route render>`
3. `<Route children>`

위 세가지 메소드는 모두 세가지 props를 받는다.

- match
- location
- history

### `<Route component>`
location이 match 되는 컴포넌트만 렌더링한다. `render`나 `children`을 사용할 때와는 다르게 router가 `React.createElement`를 사용한다. 이 말은 만약에 inline function을 component props로 주면 매 렌더링마다 새로운 엘리먼트를 만들어낸다는 의미이다. 따라서 inline 렌더링을 사용할 때는 `render`나 `childeren`을 사용하는 것이 좋다.

### `<Route render>`
inline 렌더링을 불필요한 remounting 없이 할 수 있다. `<Route component>`가 `<Route render>`보다 더 우선순위를 가지고 있기 때문에 같은 `Route`에서 사용하지 않는 것이 좋다.

### `<Route children>`
가끔 path가 일치하는지 여부와 관계없이 렌더링을 해야하는 경우가 있는데 이러한 경우에 `children` prop을 사용한다. 이 점을 제외하고는 모두 `<Route render>`와 동일하게 작동한다.

## Switch

location이 match 되는 첫번째  `<Route>`나 `<Redirect>`를 렌더링한다.
 
그냥 `<Route>`를 사용하는 것과 무엇이 다르냐면, 예를들어 다음과 같은 코드가 있다고 할 때,
 
```jsx=
import { Route } from "react-router";

let routes = (
  <div>
    <Route path="/about">
      <About />
    </Route>
    <Route path="/:user">
      <User />
    </Route>
    <Route>
      <NoMatch />
    </Route>
  </div>
);
```

url이 `/about`이면 `<About>`, `<User>`, `<NoMatch>`가 렌더링이 된다. 하지만 `Switch`와 함께 사용하면 첫번째로 일치하는 `<About>`만 렌더링이 된다.

결국에 `<AuthRoute>`를 만들어서 사용해도 됐던 이유는 `<Switch>` 안에 넣어서 사용을 했기 때문에 그런 것이 아닌가 싶다. 그런데 `<Switch>`는 자식 컴포넌트 중 path를 가지고 있는 것은 모두 확인을 하는 것인가 궁금함이 생겼다. [깃허브의 readme](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Switch.md)를 보면 `<Switch>`의 모든 children은 `<Route>`나 `<Redirect>`이어야 한다고 하는데 `<Route>`를 감싼 컴포넌트를 children으로 넣어도 되는 걸까?

```jsx=
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Switch> outside a <Router>");

          const location = this.props.location || context.location;

          let element, match;

          // We use React.Children.forEach instead of React.Children.toArray().find()
          // here because toArray adds keys to all child elements and we do not want
          // to trigger an unmount/remount for two <Route>s that render the same
          // component at different URLs.
          React.Children.forEach(this.props.children, child => {
            if (match == null && React.isValidElement(child)) {
              element = child;

              const path = child.props.path || child.props.from;

              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });

          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}
```

코드를 확인해보니 단순히 children을 가지고 리액트 엘리먼트인지만 확인한 후에 path를 가지고 확인을 하기 때문에 `<AuthRoute>`를 만들어서 사용을 해도 상관이 없을 것 같다.