router에 대해서 공부하기 이전에 공부할 환경을 만들어준다.
src 폴더 안에 pages 폴더를 만들고 그 안에 Home, Profile, About이라는 jsx 파일 세가지를 준비한다. url을 localhost:3000/about
이라고 입력한다면 About.jsx 화면이 보이도록 할 예정이다.
App.js에서 각각의 컴포넌트를 불러오고 각 컴포넌트는 다음과 같이 작성해준다.
// App.js
import React from 'react';
import './App.css';
import Home from './pages/Home'
import About from './pages/About';
import Profile from './pages/Profile';
function App() {
return (
<Home />
<Profile />
<About />
);
}
export default App;
// Home.jsx
import React from "react";
export default function Home() {
return (
<div>
<h2>Home 페이지 입니다.</h2>
</div>
);
}
About과 Profile도 Home과 똑같은 방식으로 적어서 준비해준다.
이제, react-router-dom으로부터 라이브러리를 가져올 것이다. 가져온 라이브러리가 대문자로 시작하면, 클래스이거나 컴포넌트이다. 소문자로 시작하면, 함수이거나 변수이거나 객체이다.
// App.js
import { BrowserRouter } from 'react-router-dom';
BrowserRouter는 대문자로 시작하니 컴포넌트이다. 이 컴포넌트는 남이 만든 컴포넌트를 내가 가져온 것이다. 따라서 사용법을 숙지해야 한다. 컴포넌트 사용법은 곧 props를 어떻게 설정하느냐를 의미한다. props는 children이거나 내가 직접 지정하는 것 두 가지 방법이 있다.
가져온 컴포넌트는 내가 바꿔서 보여주려고 하는 컴포넌트들을 감싼다.
// App.js
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import './App.css';
import Home from './pages/Home'
import About from './pages/About';
import Profile from './pages/Profile';
function App() {
return (
<BrowserRouter>
<Home />
<Profile />
<About />
</BrowserRouter>
);
}
export default App;
이렇게 감싼다면 Home, Profile, About이 children이 된다. BrowserRouter 컴포넌트 사용방법에 따라서 라우팅의 결과물이 달라진다. children의 이름을 Route라고 바꿔준다. Route는 BrowserRouter처럼 가져온다.
Route의 props를 정해줘야 하는데, 첫 번째 props의 이름은 path이다. path는 localhost:3000
뒤에 뭐가 붙냐를 말한다. 두 번째 props의 이름은 component인데 값으로는 보여주고 싶은 페이지의 컴포넌트 이름이 들어간다.
// App.js
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import './App.css';
import Home from './pages/Home'
import About from './pages/About';
import Profile from './pages/Profile';
function App() {
return (
<BrowserRouter>
<Route path="/" component={Home}/>
<Route path="/profile" component={Profile}/>
<Route path="/about" component={About}/>
</BrowserRouter>
);
}
export default App;
localhost:3000으로 가면 Home.jsx에 해당하는 화면이 보인다. 그런데, localhost:3000/profile
로 들어가보면 예상과 다른 화면이 나온다.
내가 원한 화면은 profile만 나오는 화면인데 home 화면까지 같이 나온다. about으로 들어가도 같은 결과가 나온다.
브라우저 url과 path prop에 적힌 조건을 포함하여 랜더링한다. url에 /profile
이라고 적히면, path 조건이 "/"인 것과 "/profile"인 것이 모두 일치한다고 나오게 된다.(react-router-dom의 매칭 알고리즘에 의해서)
그렇다면 항상 정확하게 같은 것만 보여주길 원하면 exact
를 추가하면 된다.
<Route path="/" exact component={Home}/>
여기까지 배웠다면, 정해진 경로는 다 만들 수 있다. 그렇다면 정해지지 않은 경로는 어떻게 처리할까?
정해지지 않은 경로는 Dynamic Routing이라고 한다. 정해지지 않은 경로는 어떤 것을 말하는 걸까? 이는 localhost:3000/profile/3
과 같은 요청을 의미한다. 이렇게 요청이 들어온다면, 이 url을 읽어서 3을 꺼내온 다음 api 요청을 보낼 수도 있고 데이터베이스로부터 3에 해당하는 정보를 요청할 수도 있다. 그런데, 우리는 아직 api 요청을 할 줄 모르니까 어떻게 url에서 3을 꺼내올 수 있는지만 배워보자.
지금 현재, localhost:3000/profile/3
이렇게 요청하면 profile 화면이 나온다. 그런데, 이는 원하는 결과가 아니므로 profile에 exact 설정을 해준다.
항상 exact 설정이 필요한 것은 아니다. 만약, 기존 profile 화면에 추가해서 보여주고 싶은게 맞다면 exact를 빼주는 것이 맞다.
그리고, profile/3을 받아줄 route를 설정해줘야 한다.
// App.js
function App() {
return (
<BrowserRouter>
<Route path="/" exact component={Home} />
<Route path="/profile" component={Profile} />
<Route path="/profile/:id" component={Profile} />
<Route path="/about" component={About} />
</BrowserRouter>
);
}
profile/3에 해당하는 페이지를 만들지 않아서 일단 profile을 넣었다. 이렇게 하면 같은 화면처럼 보이지만 엄밀히 다른 인스턴스이다. 그럼, id가 들어오면 profile: id페이지 입니다. 라고 뜨게 설정해주자.
path="/profile/:id"
의 :id를 꺼내쓰는 것은 component={Profile}
의 Profile 컴포넌트가 꺼내쓴다.
다짜고짜 props 넣어주고 로그를 찍어본다.
// Profile.jsx
import React from "react";
export default function Profile(props) {
console.log(props);
return (
<div>
<h2>Profile 페이지 입니다.</h2>
</div>
);
}
이렇게 된 이유는 누군가가
<Profile history={{}} location={{}} match={{}} />
이와 같은 행동을 한 것이다. 누가 이렇게 했을까? 바로 Profile을 사용하고 있는 컴포넌트가 추가해줬을 것이다.
App.js의 Route라는 컴포넌트가 component라고 하는 props로 Profile을 받아서 history, location, match라는 props를 끼워넣고 렌더를 한 것이다.(중요!)
profile의 props인 match를 살펴보면 id 정보가 담겨있다. 이 아이디가 undefined라면 그냥 Profile을, 아이디가 존재한다면 profile: id페이지 입니다. 페이지를 띄우도록 해보자.
// Profile.jsx
import React from "react";
export default function Profile(props) {
const id = props.match.params.id;
if (id === undefined) {
return (
<div>
<h2>Profile 페이지 입니다.</h2>
</div>
);
}
return (
<div>
<h2>Profile : {id} 페이지 입니다.</h2>
</div>
);
}
같은 컴포넌트이지만 props에 따라서 다른 화면을 렌더링할 수 있다.
Dynamic Routing에서 localhost:3000/about?id=3
처럼 요청하는 경우도 있다. ?id=3
을 Query String이라 한다.
query string은 params처럼 라우트 설정을 따로 해줄 필요는 없다. 왜냐하면, localhost:3000/about?id=3
이것의 실제 경로는 localhost:3000/about
이것과 같아서이기도 하고 query string은 옵션으로 사용하는 경우가 많기 때문이다.
query string도 한번 꺼내와보자.
// About.jsx
// src/pages/About.jsx
import React from "react";
export default function About(props) {
console.log(props);
return (
<div>
<h2>About 페이지 입니다.</h2>
</div>
);
}
query 정보는 location 객체의 search안에 담겨있다. 이번에는 아이디값만 들어있는게 아니라 ?id=3
으로 통째로 담겨있다.
<아이디만 가져오는 방법>
// About.jsx
import React from "react";
export default function About(props) {
const search = props.location.search;
const searchParams = new URLSearchParams(search);
console.log(searchParams.get('id'));
return (
<div>
<h2>About 페이지 입니다.</h2>
</div>
);
}
하지만, 이 방법은 IE 지원이 전혀 안된다.
$ npm i query-string
// About.jsx
import React from "react";
import qs from 'query-string';
export default function About(props) {
const search = props.location.search;
const queryString = qs.parse(search);
console.log(queryString.id);
return (
<div>
<h2>About 페이지 입니다.</h2>
</div>
);
}
위의 params처럼 id 없으면 그냥 about, 있으면 about: id 렌더링
// About.jsx
import React from "react";
import qs from 'query-string';
export default function About(props) {
const search = props.location.search;
const { id } = qs.parse(search);
return (
<div>
<h2>About : {id === undefined || <span>{id}</span>} 페이지 입니다.</h2>
</div>
);
}
지금까지 우리의 라우팅 방식은 다음과 같다.
function App() {
return (
<BrowserRouter>
<Route path="/" exact component={Home} />
<Route path="/profile" exact component={Profile} />
<Route path="/profile/:id" component={Profile} />
<Route path="/about" component={About} />
</BrowserRouter>
);
}
path가 "/"이면, Home
path가 "/profile"이면 Profile
각각을 병렬로 평가한다. 그런데 switch를 사용하면 다른 방법으로 평가할 수 있다.
path가 "/"이면, Home 아니면 밑으로 내려간다. 그래서, path가 "/profile"이라면 Profile로, 아니라면 밑으로 내려간다.
이런 방식으로 평가된다.
마찬가지로 import 해서 사용할 수 있다.
import { BrowserRouter, Route, Switch } from 'react-router-dom';
switch 문의 특징
만약에 각 비교문이 포함 관계에 있다면 순서에 따라 달라진다.
예를 들어) /안에 /about가 포함된다.(/가 더 큰 개념)
그렇다면, /를 먼저 간 후에는 /about에 갈 수 없다. 따라서 /about을 먼저 검사한 후, /를 가야한다.(작은 것을 먼저 검사한후 밑으로 갈수록 큰 것을 검사해야 잘 걸러진다.)
따라서 switch는 순서가 매우 중요하다.
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/about" component={About} />
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/" exact component={Home} />
</Switch>
</BrowserRouter>
);
}
exact가 없더라도 잘 동작한다.(/에는 반드시 exact를 해줘야 한다. 모든 path에는 /가 들어가므로)
switch를 이용해 404 not found를 설정해보자.
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/about" component={About} />
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
}
// NotFound.jsx
import React from 'react';
export default function NotFound() {
return (
<h1>404 Not Found</h1>
);
}
먼저 error page 만들어준다.
// ErrorPage.jsx
import React from 'react';
export default function ErrorPage() {
return (
<div>
<h1>으악 에러당</h1>
</div>
);
}
$ npm i react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
// App.js
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorPage}>
<BrowserRouter>
<Switch>
<Route path="/about" component={About} />
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/" component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
</ErrorBoundary>
);
}
Error Boundary가 에러를 감지해서 ErrorPage.jsx를 열어준다. 자기 자신의 에러를 알 수 없고 자식들의 에러만 찾을 수 있으므로 항상 컴포넌트 최상단에 위치해야 한다.
참고로, 에러 바운더리는 소스코드에서의 에러만 발견할 수 있다. 예를 들어, 서버에 잘못된 정보를 요청했다거나 하는 에러는 찾을 수 없고, 코드에 오류가 있다거나 문법이 잘못된 경우의 에러만 찾을 수 있다.
Not Found는 에러가 아니다. path에 매칭되는 주소가 없을 경우 보여주는 페이지일 뿐이다.
지금까지 url을 입력하면 라우트를 이동하는 방법이었다. 그런데, 실제로 유저들이 url에 직접 입력해서 페이지를 이동하는 경우는 거의 없다. 브라우저에 보이는 목록을 눌러서 페이지를 이동하는 경우가 대다수다. 이번에는 JSX 링크로 페이지를 이동하는 방법을 알아보자.
지금까지 우리는 페이지를 이동하는 경우 <a>
태그를 사용했다. 그런데, a태그를 누르게 되면 서버에 페이지를 요청하게 된다. 그런데, 서버에 요청을 하게 되면 react를 사용하는 의미가 없어지게 된다. 서버에는 최초에 한번만 요청하고 그 이후로는 요청을 하지 않아야 한다.
따라서 어떻게 동작해야 하냐면, 눌렀을 때 서버에 요청하지 말고 해당 페이지만 렌더링 되도록 경로만 바꿔주도록 해야한다. 그게 바로 Link이다.
// Link 불러오기
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorPage}>
<BrowserRouter>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/profile">Profile</Link>
</li>
<li>
<Link to="/profile/3">Profile : 3</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/about?id=5">About : 5</Link>
</li>
</ul>
</div>
<Switch>
<Route path="/about" component={About} />
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/" component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
</ErrorBoundary>
);
}
Link를 사용하면 로딩하지 않고, 서버에 요청하지 않고 화면만 전환된다. Link는 브라우저의 api를 통해서 주소를 몰래 바꾼다. 그렇다는 것은 주소창의 url을 control할 수 있어야 한다. control하는 객체의 이름은 history이다. 이 history를 조작해 주소창을 바꾼다.
사용자를 속이는 것이다. 주소창을 바꾸고, 주소창에 맞는 라우팅만 보이도록 처리해준다.
JSX로만 라우트 이동하진 않는다. JS로 라우팅 이동하는 방법도 알아보자.
// Login.jsx 생성
import React from 'react';
export default function Login() {
return (
<div>
<h1>Login</h1>
<button>로그인 버튼</button>
</div>
);
}
로그인 페이지도 app.js에 라우트 추가해준다.
<li>
<Link to="/login">Login</Link>
</li>
<Route path="/login" component={Login} />
이제, Login 페이지에서 로그인 버튼을 누르면 2초 후에 페이지 이동 하는 것을 구현하고 싶다.
// Login.jsx
export default function Login() {
return (
<div>
<h1>Login</h1>
<button onClick={() => {
setTimeout(() => {
window.location.assign('/');
}, 2000);
}} >로그인 버튼</button>
</div>
);
}
원래 자바스크립트 방식이라면 location assign을 사용할 수 있다. 그런데, 이 방법도 마찬가지로 로딩이 된다. 로딩이 된다는 것은 서버에 요청하는 것을 의미한다. 이 방법은 원하는 방법이 아니다.
Login 컴포넌트의 props를 받아서 보자.
match를 보면 isExact라는 프로퍼티가 있다. path에 있는 것과 현재 url이 맞는 지 확인해서 맞으면 true가 나온다.
history는 주소창의 history를 말한다.(history api라고 부른다.) 여기 Push라는것이 있는데 우리가 가려고 하는 페이지를 Push 해주면 된다.
export default function Login(props) {
return (
<div>
<h1>Login</h1>
<button onClick={() => {
setTimeout(() => {
props.history.push('/');
}, 2000);
}} >로그인 버튼</button>
</div>
);
}
만약, 로그인 했을 때만 볼 수 있는 페이지가 있다면 반드시 로그인을 한 상태에서만 접근할 수 있어야 한다. 로그인을 하지 않았다면 로그인 페이지로 이동해야 한다. 이때 사용할 수 있는게 바로 Redirect 이다.
Redirect 컴포넌트를 사용하게 위해 import 해오자.
// App.js
import { BrowserRouter, Route, Switch, Link, Redirect } from 'react-router-dom';
Redirect는 어디선가 렌더되면 이동(Redirect)된다.
// App.js
const isLogin = true;
<Route path="/login" render={(props) => {
console.log(props); // history, loaction, match
if (isLogin) {
return <Redirect to="/" />
}
}}/>
path에 login을 입력해도 다시 홈으로 돌아온다. 라우트에 적는 것 말고 컴포넌트에도 똑같이 사용할 수 있다.