[리액트를 다루는 기술]13장 리액트 라우터로 SPA 개발하기
1. SPA란?
SPA(Single Page Application)
한 개의 페이지로 이루어진 애플리케이션이라는 의미
1) 전통적인 웹 페이지와 SPA 비교
- 전통적인 웹 페이지
- 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아온다.
- 페이지를 로딩할 때마다 서버에서 리소르를 전달받아 해석한 뒤 화면에 보여준다.
- 사용자에게 보이는 화면을 서버 측에서 준비한다.
- 사전에 html파일을 만들어 제공하거나, 데이터에 따라 유동적인 html을 생성해 주는 템플릿 엔진을 사용한다.
- SPA
먼저, SPA를 사용하는 이유에 대해 알아보자!
1. 웹에서 제공되는 정보가 많아 새로운 화면을 보여 줘야 할 때마다 서버 측에서 모든 뷰를 준비한다면 성능상의 문제가 발생할 수 있다. (트래픽 ↑, 서버 과부하)
2. html을 서버에 요청할 때, 인터페이스에서 사용하고 있던 상태를 유지하는 것도 번거롭고, 바뀌지 않는 부분까지 새로 불러와 보여 주어야 하기 때문에 불필요한 로딩이 있어 비효율적이다.
- 리액트 같은 라이브러리 혹은 프레임워크를 사용하여 뷰 렌더링을 브라우저가 담당하도록 한다.
- 애플리케이션을 브라우저에 불러와서 실행시킨 후, 사용자와의 인터랙션이 발생하면 필요한 부분만 자바스크립트를 사용하여 업데이트 한다.
- 새로운 데이터가 필요하면 서버 API를 호출하여 필요한 데이터만 새로 불러와 애플리케이션에서 사용한다.
2) SPA에서 다양한 화면 보여주기
SPA는 서버에서 사용자에게 제공하는 페이지는 한 종류이지만,
해당 페이지에서 로딩된 자바스크립트와 함께, 사용자와 브라우저의 주소 상태에 따라
다양한 화면을 보여 줄 수 있다.
→ 이것이 바로 라우팅!
- 리액트 라우팅 라이브러리 종류
- 리액트 라우터(react-router)
- 리치 라우터(reach-router)
- Next.js
- 리액트 라우터 특징
- 클라이언트 사이드에서 이루어지는 라우팅을 아주 간단하게 구현할 수 있도록 한다.
- 서버 사이드 렌더링을 할 때, 라우팅을 도와주는 컴포넌트들을 제공해준다.
3) SPA 단점
- 앱의 규모가 커지면 자바스크립트 파일이 너무 커진다.
- (해결법) 코드 스플리팅을 사용해 라우트 별로 파일들을 나누어서 트랙픽과 로딩 속도를 개선할 수 있다.
- 브라우저에서 자바스크립트를 사용하여 라우팅을 관리하면, 자바스크립트를 실행하지 않는 일반 크롤러에서는 페이지의 정보를 제대로 수집해 가지 못한다는 잠재적인 단점이 있다.
- 자바스크립트가 실행될 때까지 페이지가 비어있기 때문에 자바스크립트 파일이 로딩되어 실행되는 짧은 시간 동안 흰 페이지가 나타날 수 있다.
- (해결법) 서버 사이드 렌더링
2. 프로젝트 준비 및 기본적인 사용법
1) 프로젝트 생성 및 리액트 라우터 적용
- 프로젝트 생성하기
$ yarn create react-app router-tutorial
- 라우터 설치하기
$ yarn add react-router-dom@5
2) 프로젝트에 라우터 적용
<적용법>
src/index.js 파일에서 react-router-dom에 내장되어 있는 BrowserRouter라는 컴포넌트를 사용하여 감싼다.
- BrowserRouter 컴포넌트는 웹 애플리케이션 HTML5의 History API를 사용하여 페이지를 새로고침하지 않고도 주소를 변경한다.
- 또한, 현재 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있게 해준다.
src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
3) 페이지 만들기
라우트로 사용할 페이지 컴포넌트 만들기
Home.js
import React from "react";
const Home = () => {
return (
<div>
<h1>홈</h1>
<p>홈, 그 페이지는 가장 먼저 보여지는 페이지.</p>
</div>
);
};
export default Home;
About.js
import React from "react";
const About = () => {
return (
<div>
<h1>소개</h1>
<p>이 프로젝트는 리액트 라우터 기초를 실습해 보는 예제 프로젝트입니다.</p>
</div>
);
};
export default About;
4) Route 컴포넌트로 특정 주소에 컴포넌트 연결
<Route 컴포넌트 사용방식>
<Route path=”주소 규칙” component={보여 줄 컴포넌트} />
App.js
import React from "react";
import { Route, Routes } from "react-router-dom";
import About from "./About";
import Home from "./Home";
const App = () => {
return (
<div>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
</div>
);
};
export default App;
<결과보기>
- 위와 같이 /about으로 이동했지만 About 컴포넌트만 보이는 것이 아닌 Home 컴포넌트도 같이 보인다.
- (해결법) /about 경로가 /규칙에도 일치해졌기 때문인데, exact라는 props를 true로 설정하면 컴포넌트 하나만 나오게 할 수 있다.
-
<Route path="/" component={Home} exact={true} />
5) Link 컴포넌트를 사용하여 다른 주소로 이동하기
- Link 컴포넌트
- 클릭하면 다른 주소로 이동시켜 주는 컴포넌트
- 페이지를 새로불러오지 않고 그대로 유지한 상태에서 HTML5 History API를 사용하여 페이지의 주소만 변경해 준다.
- Link 컴포넌트 사용법
<Link to =”주소”>내용</Link>
- Link 컴포넌트 적용하기
import React from "react";
import { Route, Link } from "react-router-dom";
import About from "./About";
import Home from "./Home";
const App = () => {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
</ul>
<hr />
<Route path="/" component={Home} />
<Route path="/about" component={About} />
</div>
);
};
export default App;
3. Route 하나에 여러 개의 path 설정하기
import React from "react";
import { Route, Link } from "react-router-dom";
import About from "./About";
import Home from "./Home";
const App = () => {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
</ul>
<hr />
<Route path="/" component={Home} />
<Route path={["/about", "/info"]} component={About} />
</div>
);
};
export default App;
- /info로 접속해도 About 컴포넌트를 볼 수 있다.
4. URL 파라미터와 쿼리
페이지 주소를 정의할 때 유동적인 값을 전달해야 할 경우 파라미터나 쿼리를 사용한다.
- 파라미터 예시 : /profile/velopert
- 쿼리 예시 : /about?details=true
일반적으로 파라미터는 특정 아이디 혹은 이름을 사용하여 조회할 때 사용하고
쿼리는 어떤 키워드를 검색하거나 페이지에 필요한 옵션을 전달할 때 사용한다.
1) URL 파라미터
/profile/velopert와 같은 형식으로
뒷부분에 유동적인 username 값을 넣어 줄 때
해당 값을 props로 받아 와 조회하는 방법을 알아보자
Profile.js
import React from "react";
const data = {
velopert: {
name: "김민준",
description: "리액트를 좋아하는 개발자",
},
gildong: {
name: "홍길동",
description: "고전 소설 홍길동전의 주인공",
},
};
const Profile = ({ match }) => {
const { username } = match.params;
const profile = data[username];
if (!profile) {
return <div>존재하지 않는 사용자입니다.</div>;
}
return (
<div>
<h3>
{username}({profile.name})
</h3>
<p>{profile.description}</p>
</div>
);
};
export default Profile;
- URL 파라미터를 사용할 때 라우트로 사용되는 컴포넌트에서 받아오는 match라는 객체 안의 params 값을 참조한다.
- match 객체 안에는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 들어 있다.
App 컴포넌트에서 Profile 컴포넌트를 위한 라우터 정의하기
import React from "react";
import { Route, Link } from "react-router-dom";
import About from "./About";
import Home from "./Home";
import Profile from "./Profile";
const App = () => {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profile/velopert">velopert 프로필</Link>
</li>
<li>
<Link to="/profile/gildong">gildong 프로필</Link>
</li>
</ul>
<hr />
<Route path="/" component={Home} />
<Route path={["/about", "/info"]} component={About} />
<Route path="/profile/:username" component={Profile} />
</div>
);
};
export default App;
2) URL 쿼리
About 페이지에서 쿼리를 받아오기
쿼리는 location 객체에 들어 있는 search 값에서 조회 가능하며
location 객체는 라우트로 사용된 컴포넌트에게 props로 전달된다.
location의 형태
{
"pathname": "about",
"search": "?detail=true",
"hash": ""
}
▲ http://localhost:3000/about?detail=true 주소로 들어갔을 때의 location 객체의 값
URL 쿼리의 특징
- URL 쿼리를 읽을 때는 search 값을 확인해야 한다.
- search 값은 문자열 형태로 되어있다.
- URL 쿼리는 문자열에 여러 가지 값을 설정해 줄 수 있다.
- search 값에서 특정 값을 읽어 오기 위해서는 문자열을 객체 형태로 변환해 주어야 한다.
- 이를 위해 qs라이브러리 설치하기
$yarn add qs
- 이를 위해 qs라이브러리 설치하기
About 컴포넌트에서 location.search값에 있는 detail이 true인지 아닌지에 따라 추가 정보를 보여 주도록 만들어보자
About.js
import React from "react";
import qs from "qs";
const About = ({ location }) => {
const query = qs.parse(location.search, {
ignoreQueryPrefix: true, // 이 설정을 통해 문자열 맨 앞의 ?를 생략합니다.
});
const showDetail = query.detail === "true"; // 쿼리의 파싱 결과 값은 문자열입니다.
return (
<div>
<h1>소개</h1>
<p>이 프로젝트는 리액트 라우터 기초를 실습해 보는 예제 프로젝트입니다.</p>
{showDetail && <p>detail 값을 true로 설정하셨군요!</p>}
</div>
);
};
export default About;
- 쿼리를 사용할 때 쿼리 문자열을 객체로 파싱하는 과정에서 결과값은 언제나 문자열이기 때문에 비교할 때에도 “true”와 같이 정확하게 문자열을 비교하도록 한다.
5. 서브 라우트
라우트 내부에 또 다른 라우트를 정의하는 것
APP 컴포넌트에서 두 종류의 프로필 링크를 보여 줬는데,
이를 잘라내서 프로필 링크를 보여 주는 Profiles라는 라우트 컴포넌트를 따로 만들고,
그 안에서 Profile 컴포넌트를 서브 라우트로 사용하도록 한다.
Profile.js
import React from 'react';
import { Link, Route } from 'react-router-dom';
import Profile from './Profile';
const Profiles = () => {
return (
<div>
<h3>사용자 목록:</h3>
<ul>
<li>
<Link to="/profiles/velopert">velopert</Link>
</li>
<li>
<Link to="/profiles/gildong">gildong</Link>
</li>
</ul>
<Route
path="/profiles"
exact
render={() => <div>사용자를 선택해 주세요.</div>}
/>
<Route path="/profiles/:username" component={Profile} />
</div>
);
};
export default Profiles;
첫번째 라우터의 render props
→ 컴포넌트 자체를 전달하는 것이 아닌 보여주고 싶은 JSX를 넣어 줄 수 있다.
App.js
import React from "react";
import { Route, Link } from "react-router-dom";
import About from "./About";
import Home from "./Home";
import Profile from "./Profile";
import Profiles from "./Profiles";
const App = () => {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profile/velopert">velopert 프로필</Link>
</li>
<li>
<Link to="/profile/gildong">gildong 프로필</Link>
</li>
<li>
<Link to="/profiles">프로필</Link>
</li>
</ul>
<hr />
<Route path="/" component={Home} exact={true} />
<Route path={["/about", "/info"]} component={About} />
<Route path="/profile/:username" component={Profile} />
<Route path="/profiles" component={Profiles} />
</div>
);
};
export default App;
6. 리액트 라우터 부가 기능
1) history
- 라우트로 사용된 컴포넌트에 match, location과 함께 전달되는 props 중 하나
- history 객체를 통해 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있다.
- 특정 버튼을 눌러 뒤로가기, 로그인 후 화면 전환, 다른 페이지로 이탈 방지 등에 쓰인다.
HistorySample.js
import React, { Component } from "react";
class HistorySample extends Component {
// 뒤로 가기
handleGoBack = () => {
this.props.history.goBack();
};
// 홈으로 이동
handleGoHome = () => {
this.props.history.push("/");
};
componentDidMount() {
// 이것을 설정하고 나면 페이지에 변화가 생기려고 할 때마다 정말 나갈 것인지를 질문함
this.unblock = this.props.history.block("정말 떠나실 건가요?");
}
componentWillUnmount() {
// 컴포넌트가 언마운트되면 질문을 멈춤
if (this.unblock) {
this.unblock();
}
}
render() {
return (
<div>
<button onClick={this.handleGoBack}>뒤로</button>
<button onClick={this.handleGoHome}>홈으로</button>
</div>
);
}
}
export default HistorySample;
App.js
import React from "react";
import { Route, Link } from "react-router-dom";
import About from "./About";
import Home from "./Home";
import Profile from "./Profile";
import Profiles from "./Profiles";
import HistorySample from "./HistorySample";
const App = () => {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profile/velopert">velopert 프로필</Link>
</li>
<li>
<Link to="/profile/gildong">gildong 프로필</Link>
</li>
<li>
<Link to="/profiles">프로필</Link>
</li>
<li>
<Link to="history">History 예제</Link>
</li>
</ul>
<hr />
<Route path="/" component={Home} exact={true} />
<Route path={["/about", "/info"]} component={About} />
<Route path="/profile/:username" component={Profile} />
<Route path="/profiles" component={Profiles} />
<Route path="/history" component={HistorySample} />
</div>
);
};
export default App;
2) withRouter
withRouter함수는 HoC(Higher-order Component)이다.
라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체를 접근할 수 있게 해 준다.
WithRouterSample.js
import React from ‘react‘;
import { withRouter } from ‘react-router-dom‘;
const WithRouterSample = ({ location, match, history }) => {
return (
<div>
<h4>location</h4>
<textarea
value={JSON.stringify(location, null, 2)}
rows={7}
readOnly={true}
/>
<h4>match</h4>
<textarea
value={JSON.stringify(match, null, 2)}
rows={7}
readOnly={true}
/>
<button onClick={() => history.push(‘/‘)}>홈으로</button>
</div>
);
};
export default withRouter(WithRouterSample);
- withRouter를 사용하면 현재 자신을 보여 주고 있는 라우트 컴포넌트(현재 Profile)를 기준으로 match가 전달된다.
- Profiles를 위한 라우트를 설정할 때 path=”/profiles”라고만 입력했기 때문에 username 파라미터를 읽어 오지 못하는 상태이다.
- WithRouterSample 컴포넌트를 Profiles에서 지우고 Profile 컴포넌트에 넣으면 match쪽에 URL파라미터가 제대로 보이게 된다.
Profile.js
import React from "react";
import { withRouter } from "react-router-dom";
import WithRouterSample from "./WithRouterSample";
const data = {
velopert: {
name: "김민준",
description: "리액트를 좋아하는 개발자",
},
gildong: {
name: "홍길동",
description: "고전 소설 홍길동전의 주인공",
},
};
const Profile = ({ match }) => {
const { username } = match.params;
const profile = data[username];
if (!profile) {
return <div>존재하지 않는 사용자입니다.</div>;
}
return (
<div>
<h3>
{username}({profile.name})
</h3>
<p>{profile.description}</p>
<WithRouterSample />
</div>
);
};
export default Profile;
3) Switch
- 여러 Route를 감싸서 그 중 일치하는 단 하나의 라우트만을 렌더링 시켜 준다.
- Switch를 사용해 모든 규칙과 일치하지 않을 때 보여 줄 Not Found 페이지도 구현할 수 있다.
import React from "react";
import { Route, Link, Switch } from "react-router-dom";
import About from "./About";
import Home from "./Home";
import Profile from "./Profile";
import Profiles from "./Profiles";
import HistorySample from "./HistorySample";
const App = () => {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profile/velopert">velopert 프로필</Link>
</li>
<li>
<Link to="/profile/gildong">gildong 프로필</Link>
</li>
<li>
<Link to="/profiles">프로필</Link>
</li>
<li>
<Link to="history">History 예제</Link>
</li>
</ul>
<hr />
<Switch>
<Route path="/" component={Home} exact={true} />
<Route path={["/about", "/info"]} component={About} />
<Route path="/profile/:username" component={Profile} />
<Route path="/profiles" component={Profiles} />
<Route path="/history" component={HistorySample} />
<Route
// path를 따로 정의하지 않으면 모든 상황에 렌더링됨
render={({ location }) => (
<div>
<h2>이 페이지는 존재하지 않습니다:</h2>
<p>{location.pathname}</p>
</div>
)}
/>
</Switch>
</div>
);
};
export default App;
4) NavLink
- NavLink는 Link와 비슷하다.
- 현재 경로와 Link에서 사용하는 경로가 일치하는 경우 특정 스타일 혹은 CSS클래스를 적용할 수 있는 컴포넌트이다.
import React from "react";
import { NavLink, Route } from "react-router-dom";
import Profile from "./Profile";
const Profiles = () => {
const activeStyle = {
background: "black",
color: "white",
};
return (
<div>
<h3>사용자 목록:</h3>
<ul>
<li>
<NavLink activeStyle={activeStyle} to="/profiles/velopert" active>
velopert
</NavLink>
</li>
<li>
<NavLink activeStyle={activeStyle} to="/profiles/gildong">
gildong
</NavLink>
</li>
</ul>
(…)
</div>
);
};
export default Profiles;
퀴즈!
Q1) 다른 주소에 다른 화면을 보여주는 것을 뭐라고 할까요
라우팅
Q2) URL에 유동적인 값을 전달할 때 사용하는 2가지를 적어주세요
파라미터, 쿼리
Q3) 라우트의 부가기능 중 라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체를 접근할 수 있게 해주는 함수는 무엇일까요
withRouter