-
[React] React-Router를 활용해 원하는 페이지로 이동하기Front-end/React 2020. 11. 9. 01:55
React-Router
리액트 라우터는 여러 페이지들을 연결시켜주는 역할을 하는 친구입니다. 리액트 자체에서 지원해주는 기술은 아니지만 리액트에서 라우팅을 한다면 이 서드파티를 사용하는 것이 국룰로 자리잡았습니다. 그만큼 간편하고 성능이 뛰어나단 소리겠죠?
이전에 순수 Javascript로 SPA를 구현해보면서 동작 방식을 이해 했었는데요, 이 글을 읽기 전 한 번 읽고 보시면 조금 더 이해는데 있어 좋을 것 같습니다. :)
구현할 예제는 헤더를 가지고 있는 페이지 2개와 헤더를 가지지 않는 페이지 1개를 이용해 각 상황을 고려한 라우팅을 해보려고 합니다.
핵심적인 내용을 살펴보면서 구현해보겠습니다.
❗️ 이 글은 개인적으로 학습한 정보와 지식을 토대로 작성된 글입니다. 혹시 잘못된 부분이 있거나 수정사항이 있다면 말씀해주시면 반영하겠습니다. 🙏
프로젝트 구조
프로젝트 세팅
CRA를 활용하여 프로젝트를 생성해줍니다.
npx create-react-app react-router-study
리액트 라우터를 사용하기 위해 패키지를 설치합니다.
npm i react-router-dom
컴포넌트 구현
라우팅에 필요한 컴포넌트들을 구현하겠습니다.
src/component/Header.jsx
import React from 'react'; function Header() { return ( <header className='header'> <strong>Header</strong> <ul> <li> <button>홈</button> </li> <li> <a href=''>프로필</a> </li> </ul> </header> ); } export default Header;
src/pages/SignPage.jsx
import React from 'react'; function SignPage() { return <div>로그인 페이지</div>; } export default SignPage;
src/pages/MainPage.jsx
import React from 'react'; import Header from '../components/Header'; function MainPage() { return ( <> <Header /> <div>메인 페이지</div>; </> ); } export default MainPage;
src/pages/ProfilePage.jsx
import React from 'react'; import Header from '../components/Header'; function ProfilePage() { return ( <> <Header /> <div>프로필 페이지</div>; </> ); } export default ProfilePage;
index.css
* { box-sizing: border-box; margin: 0; padding: 0; } ul { list-style: none; } .header { display: flex; justify-content: center; padding: 20px 0 20px 0; border-bottom: 1px solid black; } .header strong { margin: 0 20px 0 0; } .header ul { display: flex; } .header ul li { margin: 0 15px 0 0; } .header ul li:last-child { margin: 0; } .sign, .main, .profile { margin: 30px 0; text-align: center; }
위와 같이 세팅하면 아래와 같은 화면이 나타납니다.
메인 페이지와 프로필 페이지에서는 각각 헤더를 가지고 있으며 로그인 페이지에는 현재 헤더를 가지고 있지 않은 상태입니다.
위 네가지 컴포넌트를 활용해서 상황에 맞게 라우팅을 해보겠습니다.
React-Router 사용하기
본격적으로 리액트 라우터를 사용해보겠습니다.
App.js
BrowserRouter, HashRouter
아까 npm으로 추가한 react-router-dom에 BrowserRouter, HashRouter를 활용해 라우팅을 할 수 있는데요, 이 둘의 차이는 BrowserRouter의 경우는 페이지 그 자체를 이동시키는 방식이며, HashRouter는 #을 포함시키고 해당 정보에 접근하는 방식의 차이입니다. 현재 예제는 BrowserRouter 방식으로 해보겠습니다.
import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import SignPage from './pages/SignPage'; import ProfilePage from './pages/ProfilePage'; import MainPage from './pages/MainPage'; function App() { return ( <> <Router> <SignPage /> <MainPage /> <ProfilePage /> </Router> </> ); } export default App;
위와 같이 해주면 페이지 그 자체를 이동할 컴포넌트들을 선언해 준 것입니다.
Router
Route는 페이지 하나를 통제하며 path, exact, component, render를 다룰 수 있게 해줍니다. 위 4가지는 어떤 기능을 가지고 있는지 먼저 간략하게 보면,
- path: 페이지를 이동할 경로를 선언합니다. 만약 선언하지 않으면 기존에 선언된 경로 이외에 접근시 라우팅
- exact: path가 정확하게 일치할 때만 이동하도록 선언합니다. (default true)
- component: 어떤 페이지인지 선언합니다.
- render: 커스터마이징이 가능한 컴포넌트를 함수 형태로 선언합니다.
import React from 'react'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import SignPage from './pages/SignPage'; import MainPage from './pages/MainPage'; import ProfilePage from './pages/ProfilePage'; function App() { return ( <> <Router> <Route path='/' component={SignPage} /> <Route path='/main' component={MainPage} /> <Route path='/profile' component={ProfilePage} /> </Router> </> ); } export default App;
각 path에 맞는 페이지를 선언해주고 /main을 입력하여 주면 아래와 같은 화면이 렌더링 됩니다.
.
.
.
로그인 페이지와 동시에 메인페이지가 렌더링 됐죠?
이는 라우터에 path가 include되어있는 모든 라우트를 가져오기 때문인데요, 현재 3개의 라우트는 모두 /라는 path를 포함하고 있어서 /를 포함하고 있는 어떠한 페이지를 접근하든지 /는 항상 포함 되기 때문에 그렇습니다.
그래서 위에 4가지 속성 중 하나인 exact를 사용해서 '/' 경로만 포함된 컴포넌트를 출력해주면 될 것 같습니다.
한 번 다시 해볼까요?
import React from 'react'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import SignPage from './pages/SignPage'; import MainPage from './pages/MainPage'; import ProfilePage from './pages/ProfilePage'; function App() { return ( <> <Router> <Route path='/' exact component={SignPage} /> <Route path='/main' component={MainPage} /> <Route path='/profile' component={ProfilePage} /> </Router> </> ); } export default App;
.
.
.
정상적으로 잘 출력이 되네요!
그런데 만약 exact를 사용하지 않고 경로 매칭을 하려면 어떻게 해야할까요?
바로 Switch 컴포넌트를 사용하는 방법입니다.
Switch
Switch는 라우트들을 묶어서 선택된 라우트 첫 번째 하나만을 렌더링 하게끔 도와줍니다.
import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import SignPage from './pages/SignPage'; import MainPage from './pages/MainPage'; import ProfilePage from './pages/ProfilePage'; function App() { return ( <> <Router> <Switch> <Route path='/' component={SignPage} /> <Route path='/main' component={MainPage} /> <Route path='/profile' component={ProfilePage} /> </Switch> </Router> </> ); } export default App;
위처럼 라우트들을 묶어주면 선택된 컴포넌트 단 하나만을 출력해주기 때문에 편리하게 사용할 수 있습니다.
그런데 여기서 뭔가 이상합니다..
.
.
.
분명 선택된 하나만 렌더링 해준다고 해서 /main에 접근해봤더니... SignPage가 출력되는 대참사가 벌어집니다. 😭
이는 Switch의 특성을 잘 이해하면 충분히 풀리는 문제인데요 바로 위에 설명했듯 선택된 첫 번째 라우트가 렌더링 된다는 것인데요 이 말인 즉슨 경로가 /인 라우트가 맨 상위에 와있다면 당연히 어떤 페이지로 접근을 하던지 상관 없이 무조건 SignPage가 렌더링 될 것입니다.
그래서 이럴 땐 맨 마지막으로 보내 가장 나중에 찾을 수 있도록 해주는 경우가 있습니다.
import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import SignPage from './pages/SignPage'; import MainPage from './pages/MainPage'; import ProfilePage from './pages/ProfilePage'; function App() { return ( <> <Router> <Switch> <Route path='/main' component={MainPage} /> <Route path='/profile' component={ProfilePage} /> <Route path='/' component={SignPage} /> </Switch> </Router> </> ); } export default App;
/ 경로의 라우트를 가장 하단에 넣고 다시 한 번 테스트를 해보면
.
.
.
오 이제 아주 잘 되는군요!
여기서 느낀 부분은 바로 라우트를 선언하는 순서도 개발자가 신경써주어야 한다는 것입니다... 😡
이렇게 개발자가 설정한 경로만으로 사용자가 접근해주면 좋을텐데 대부분의 서비스들은 그렇지 못합니다.
그래서 설정한 외의 경로에는 에러페이지 처리를 해주어야 하는데요, 이번엔 render props를 활용해서 직접 커스텀 하여 처리해보겠습니다.
Render
Render는 개발자가 개발한 컴포넌트 파일을 바로 선언하는 것이 아닌 직접 커스터마이징을 하여 렌더링 할 수 있는 것을 말합니다.
함수 형태로 선언해줍니다.
import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import SignPage from './pages/SignPage'; import MainPage from './pages/MainPage'; import ProfilePage from './pages/ProfilePage'; function App() { return ( <> <Router> <Switch> <Route path='/main' component={MainPage} /> <Route path='/profile' component={ProfilePage} /> <Route path='/' component={SignPage} /> <Route render={() => <div className='error'>에러 페이지</div>} /> </Switch> </Router> </> ); } export default App;
기존에 선언된 경로 이외에 접근을 할 때 에러페이지를 보고 싶기 때문에 path는 선언하지 않았으며, 단순한 에러 페이지를 보여주는 박스를 하나 선언하였습니다.
이제 임의의 URL을 입력하면 에러 페이지가 뜰 것 같은 기대감을 품고 한 번 실행해 보겠습니다.
.
.
.
역시나 세상은 녹록지 않습니다. 바로 라우트의 순서 문제가 여기서도 발생한 것이죠...
에러 라우트를 먼저 선언하자니 로그인 페이지 전에 에러 페이지가 렌더링 될 것이고... 로그인 페이지를 먼저 선언하자니 /가 포함되면 로그인 페이지가 렌더링 될 것이고... 😱
그래서 보통은 Switch를 사용하고 있어도 / 경로에는 exact를 선언하여 처리를 해주는게 좋긴 하지만, 우리는 Switch를 사용하고 있기 때문에 해당 방법 외에 방법을 알아보자면..
import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import SignPage from './pages/SignPage'; import MainPage from './pages/MainPage'; import ProfilePage from './pages/ProfilePage'; function App() { return ( <> <Router> <Switch> <Route path='/main/profile' component={ProfilePage} /> <Route path='/main' component={MainPage} /> <Route path='/sign' component={SignPage} /> <Route render={() => <div className='error'>에러 페이지</div>} /> </Switch> </Router> </> ); } export default App;
위처럼 경로 뎁스를 늘려서 중복이 되지 않게 하는 방법이 있습니다.
그러면 한 번 테스트를 해볼까요?
.
.
.
모든 path에 맞지 않기 때문에 render로 커스텀했던 라우트가 렌더링 된 것을 볼 수 있습니다.
자 이제 우리가 헤더에 만들어놓은 버튼과 a태그를 활용하여 이동 시켜보겠습니다.
Location, History
우리는 라우트로 페이지를 선언하게 되면 기본적으로 가지고 있는 props가 있습니다. 그 중 가장 유용하게 사용되는 것이 바로 location과 history인데요, 이를 활용하여 특정 페이지의 예외 처리를 한다거나 history 함수를 사용하여 페이지를 이동시킬 수 있습니다.
import React from 'react'; import Header from '../components/Header'; function MainPage({ location, history }) { console.log(history); console.log(location); return ( <> <Header /> <div className='main'>메인 페이지</div> </> ); } export default MainPage;
메인 페이지 컴포넌트에서 location과 history를 받았습니다. 이 두개를 활용하여 헤더 컴포넌트에서 예외 처리와 버튼을 조작할 예정입니다. 콘솔을 찍어보면 아래와 같이 나오게 됩니다.
첫 번째 콘솔은 history며 지원하는 여러가지 함수들을 해당 컴포넌트에서 사용할 수 있게 됩니다.
두 번째 콘솔은 location이며 각각 프로퍼티 별로 값이 있습니다. 현재는 /main에 접근해 있기 때문에 해당 pathname을 가진 상태입니다.
이걸 모든 컴포넌트의 헤더에 props로 넘겨줍니다.
자 이제 본격적으로 예외 처리를 해볼텐데요
로그인 페이지에서만 헤더가 안보이고 나머지 페이지에서는 헤더가 나타나는 요구사항이 있다고 가정하겠습니다.
그러면 로그인 컴포넌트에 헤더 컴포넌트를 추가해줍니다.
.
.
.
아까 location에 pathname이란 프로퍼티가 있었죠?
pathname은 현재 접근해있는 URL의 경로를 나타내 줍니다. 즉, 만약 /sign으로 접근을 해 있다면 return을 해주지 않는다면 헤더가 사라지겠죠? 한 번 구현해 보겠습니다.
import React from 'react'; function Header({ location, history }) { if (location.pathname === '/sign') { return null; } return ( <header className='header'> <strong>Header</strong> <ul> <li> <button>홈</button> </li> <li> <a href=''>프로필</a> </li> </ul> </header> ); } export default Header;
경로가 /sign일 경우에 null을 리턴해주었습니다. 과연 처리가 됐을까요?
.
.
.
크으... 잘 처리가 됐네요!
이제 본격적으로 헤더에서 버튼과 a태그를 처리해보겠습니다.
아까 history prop을 같이 넘겨 받았었고 거기에는 여러가지 함수가 있지만 대표적으로는 push를 통해 페이지를 이동시킵니다.
즉, 버튼을 클릭 했을 때 push를 해서 해당 페이지로 이동시킨다는 개념이죠
한 번 구현해 보겠습니다.
import React from 'react'; function Header({ location, history }) { if (location.pathname === '/sign') { return null; } return ( <header className='header'> <strong>Header</strong> <ul> <li> <button onClick={() => history.push('/main')}>홈</button> </li> <li> <a href=''>프로필</a> </li> </ul> </header> ); } export default Header;
button에 onClick을 주어 /main 경로로 push해주게 되면!
.
.
.
기가 막히게 잘 됩니다!!!
이제 a태그를 통해 페이지를 이동하는 방법에 대해 알아보겠습니다.
Link
Link는 기존에 사용되던 a태그와 굉장히 흡사한데요, a태그를 사용하면 페이지가 깜빡이면서 리프레시 되지만 Link를 사용하게 되면 새로운 페이지를 불러오는 것이 아닌 화면 전환을 시켜줍니다.
import React from 'react'; import { Link } from 'react-router-dom'; function Header({ location, history }) { if (location.pathname === '/sign') { return null; } return ( <header className='header'> <strong>Header</strong> <ul> <li> <button onClick={() => history.push('/main')}>홈</button> </li> <li> <Link to='/main/profile'>프로필</Link> </li> </ul> </header> ); } export default Header;
Link를 import 해준 뒤 a태그 대신 Link를 선언하는데요, Link는 href가 아닌 to를 사용하여 경로를 선언해줍니다.
페이지가 잘 그려지나 볼까요?
.
.
.
아주 잘 되네여!!
그런데 여기서 문제가 있습니다.
현재 페이지는 총 3개라서 큰 무리가 없지만, 만약 계속적으로 많은 페이지가 생기게 되고 그에 맞게 헤더가 추가된 페이지에 계속 있어야한다면 지금처럼 헤더를 가진 컴포넌트는 항상 location과 history props를 가지고 있게 될 것입니다...🥶
그래서 지금까지 했던 개념으로 리팩토링을 한 번 해볼까 합니다.
아까 Switch를 다루어보면서 공부한 내용이 있는데요 바로 Switch는 가장 첫 번째의 단 하나만 렌더링을 해준다는 것이죠,
그렇다면 Switch에 헤더를 포함시키지 않는다면, 헤더가 필요한 컴포넌트들과 URL을 중복시킨다면 지속적으로 가지고 올 수 있지 않을까요?
헤더 컴포넌트에 있는 location으로 처리한 예외 처리를 삭제해줍니다.
또한 기존에 각 페이지에서 받았던 props와 헤더 컴포넌트도 전부 삭제한 후 한 번 구현해 보겠습니다.
import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import SignPage from './pages/SignPage'; import MainPage from './pages/MainPage'; import ProfilePage from './pages/ProfilePage'; import Header from './components/Header'; function App() { return ( <> <Router> <Route path='/main' component={Header} /> <Switch> <Route path='/main/profile' component={ProfilePage} /> <Route path='/main' component={MainPage} /> <Route path='/sign' component={SignPage} /> <Route render={() => <div className='error'>에러 페이지</div>} /> </Switch> </Router> </> ); } export default App;
헤더 컴포넌트를 그려주는 대신, 헤더가 포함된 페이지와 path를 같이 선언했습니다. 그렇게 되면 Switch와는 별개로 path에 걸리기 때문에 렌더링이 될 것이고, Switch에서는 하나의 라우트만 그려질테니 /main이 포함된 프로필, 메인 페이지만 헤더가 포함된 채로 렌더링 될 것입니다.
과연.... 잘 될까요..?
.
.
.
잘 처리가 되서 렌더링 되고 있네요!
지금까지 React-Router에 대해서 공부해보았습니다. 이 외에 더 많은 기능들이 있으니 공식 홈페이지에서 참고하시면 되겠습니다. :)
'Front-end > React' 카테고리의 다른 글
[React] Props 알아보기 (0) 2020.11.29 [React] JSX 알아보기 (0) 2020.11.29 [React] 컴포넌트 알아보기 (0) 2020.11.29 [React] useEffect Hooks 알아보기 (0) 2020.07.01 [React] useState hooks 알아보기 (0) 2020.07.01