# 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>`를 만들어서 사용을 해도 상관이 없을 것 같다.