# 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는 한 번만 호출이 됐다.

일반적인 컴포넌트였으면 두 번 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>`를 만들어서 사용을 해도 상관이 없을 것 같다.