[create-react-app] pages 폴더 구조대로 라우팅
June 02, 2020 - [create-react-app, CRA, pages, route]
Nextjs 를 사용할 때 라우팅은 기본적으로 src/pages
폴더 구조를 그대로 따른다. CRA 만을 이용해서 리액트 프로젝트를 생성하면 라우팅을 직접 하나씩 설정해 주어야 하는데 path 에 따라 해당 화면을 일일이 매핑하는 일은 귀찮은 일이 아닐 수 없다.
pages 폴더 구조대로 라우팅을 조금 더 편하게 할 수는 없을까 고민하다가 Nextjs 에서의 라우팅 동작과 비슷하게 구현을 해보았다.
Basic concept
dynamic import 를 이용해 라우팅 path 에 따라 해당 화면을 동적으로 로드하여 렌더링한다.
폴더구조
src/
pages/
login/
sign-in.js
sing-up.js
my-info.js
404.js
AsyncComponent.js
Routes.js
라우팅 테이블
라우팅 대상은 아래와 같고 이는 Routes.js
에서 정의한다.
path | component |
---|---|
/login/sign-in | /src/pages/login/sign-in.js |
/login/sign-up | /src/pages/login/sign-up.js |
/my-info | /src/pages/my-info.js |
/blablabla | /src/pages/404.js |
Goal
before) 일반적인 라우팅 정의
// Routes.js
import React, {useEffect} from 'react'
import {BrowserRouter, Switch, Route} from 'react-router-dom'
import SignIn from './pages/login/sign-in'import SignUp from './pages/login/sign-up'import MyInfo from './pages/my-info'import NotFound from './pages/404'
export default function Routes() {
return (
<BrowserRouter>
<Switch> <Route exact path='/login/sign-in' component={SignIn} /> <Route exact path='/login/sign-up' component={SignUp} /> <Route exact path='/my-info' component={MyInfo} /> <Route path='/' component={NotFound} /> </Switch> </BrowserRouter>
)
}
after) 라우팅 path 에 따라 동적으로 컴포넌트를 매핑
// Routes.js
import React, {useEffect} from 'react'
import {BrowserRouter, Route} from 'react-router-dom'
export default function Routes() {
return (
<BrowserRouter>
<Route path='/' render={({history, location}) => ( <AsyncComponent path={location.pathname} onNotFound={() => history.push('/404')} /> )} /> </BrowserRouter>
)
}
AsyncComponent
구현방법
동적으로 컴포넌트를 로드하는 AsyncComponent
를 정의
// AsyncComponent.js
import React, {useEffect, useState} from 'react'
export default function AsyncComponent(props) {
const [Component, setComponent] = useState(null)
useEffect(() => {
let cleanedUp = false
import('./pages' + props.path)
.then(module => {
if (cleanedUp) {
return
}
setComponent(() => module.default)
})
.catch(e => {
if (cleanedUp) {
return
}
setComponent(null)
if (e.message.startsWith('Cannot find module')) {
if (typeof props.onNotFound === 'function') {
props.onNotFound()
}
}
})
return () => {
setComponent(null)
cleanedUp = true
}
}, [props.path])
return Component ? <Component {...props} /> : props.loading || 'Loading..'
}
- 위
AsyncComponent
컴포넌트는 동적으로 로드할 리액트 컴포넌트 자체를 상태로서 정의하여 사용하고 있다.- hook을 이용할 때 함수 자체를 상태로 사용하고자 할 경우에는 해당 함수를 리턴하는 함수를
setComponent
에 인자로 전달해야만 의도했던 데로 동적으로 로드된 리액트 컴포넌트가Component
상태에 세팅된다.setComponent
는 함수를 인자로 받을 경우 내부적으로 해당 함수를 호출하고 해당 함수의 리턴값을 상태로서 사용하기 때문이다(useState
의 인자로 전달되는 초기상태도 마찬가지).- 그리고 이는 클래스 컴포넌트를 로드하는 경우에도 마찬가지이다. 자바스크립트에서 클래스의 typeof 결과는
'function'
으로 평가되기 때문이다.useState
,setComponet
는 아마 내부적으로 전달된 인자의 타입 확인을 위해서typeof
연산을 이용하는 것 같다.import()
는 동적으로 한번 로드한 컴포넌트를 내부적으로 캐시하기 때문에 이후 동적 모듈 로드시 네트워크 요청이 다시 발생하지는 않는다.
TL;DR;
상세 내용은 관심없고 결과만 빠르게 적용하고 싶다면 아래 모듈을 이용한다.
https://www.npmjs.com/package/react-dynamic-route