이번 13장에서는 리액트 라우터로 SPA를 개발하는 방법에 대해서 알아보고자 합니다.
Single Page Application의 약자인 SPA는 한 페이지로 이루어진 애플리케이션을 의미합니다. 서버에서 사용자에게 제공하는 화면은 한 종류지만, 해당 페이지에서 로딩된 자바스크립트와 현재 사용자 브라우저의 주소 상태에 따라 다양한 화면 가질 수 있습니다. 이전에 만든 todo-list 웹 애플리케이션을 사례로 보면, 홈, 포스트 목록, 포스트 등 여려 화면을 볼 수 있었습니다.
사용자에게 보이는 화면을 서버가 준비하는 전통적인 웹 페이지와 달리, 리액트 같은 라이브러리 혹은 프레임워크를 사용하는 방법이 있습니다.
좀 더 구체적으로 이를 설명하면 아래와 같습니다.
이렇듯 전통적인 웹 페이지 구조에서 벗어나면 필요한 정보 가져오고 업데이트한다는 면에서 여러 이득을 얻을 수 있습니다.
리액트 라이브러리 자체에는 없지만, 라우팅을 이용해 브라우저의 API를 직접 사용해 관리하거나, 라우팅 라이브러리를 사용해 이를 API 관리를 더 쉽게 할 수 있습니다.
라우팅 : |
다른 주소에 다른 화면을 보여 주는 것 |
라우팅 라이브러리에는 리액트 라우터(react-router), 리치 라우터(reach-router), Next.js 등 여러 가지가 있습니다. 그중 본 교재는 가장 자주 사용되는 리액트 라우터를 사용합니다. 이는 클라이언트 사이드에서 이루어지는 라우팅을 아주 간단하게 구현하게 돕습니다. 또한 서버 사이드 렌더링을 돕는 컴포넌트를 제공합니다.
SPA를 이용해 개발할 경우 발생 혹은 예상되는 단점과 해결책은 아래와 같습니다.
리액트 라우터를 사용하는 웹 애플리케이션을 만드는 실습을 진행해 봅시다.
대략적인 순서는 다음과 같습니다:
프로젝트 생성 및 리액트 라우터 적용
-> 페이지 만들기
-> Route 컴포넌트로 특정 주소에 컴포넌트 연결
-> 라우트 이동
-> URL 파라미터와 쿼리 이해
-> 서브 라우트
-> 부가 기능
먼저 아래의 명령어를 입력해 리액트 라우터를 적용해볼 리액트 프로젝트를 새로 생성합니다.
$ yarn create react-app router-tutorial
그리고 이 프로젝트의 프로젝트 디렉터리에서
$ cd router-tutorial
react-router-dom이라는 리액트 라우터 라이브러리를 아래의 명령어를 이용해서 설치해 주세요
$ yarn add react-router-dom
조금 전에 설치한 라액트 라우터를 프로젝트에 적용할 때에는 BrowserRouter라는 컴포넌트를 사용하여 감싸면 됩니다.
크게 두 가지 기능을 하는데, 페이지 새로고침 없이 주소변경이 가능하고, 현주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있습니다.
라우트로 사용할 페이지 컴포넌트 두 개를 만듭니다.
먼저 아래의 코드와 같은 Home 컴포넌트는 사용자에게 보여주는 첫 화면입니다.
// Home.js 파일
import React from 'react';
const Home = () => {
return (
<div>
<h1>홈</h1>
<p>홈 화면 입니다! 가장 먼저 보여지는 페이지 입니다 >< </p>
</div>
);
};
export default Home;
두 번째는 About 페이지로, 웹 사이트를 소개하는 컴포넌트입니다.
// About.js 파일
import React from 'react';
const About = () => {
return (
<div>
<h1>소개</h1>
<p> 리액트 라우터 실습 예제 프로젝트입니다 >< </p>
</div>
);
};
export default About;
Route라는컴포넌트를 사용하여 사용자의 현재 경로에 따라 다른 컴포넌트를 보여 줄 수 있습니다. 조금 더 구체적으로 설명하면, 이를 이용해 어떤 규칙을 가진 경로에 어떤 컴포넌트를 보여 줄지 아래와 같은 코드를 통해 정의할 수 있습니다.
<Route path=“주소규칙“ component={보여 줄 컴포넌트} />
이어서 App.js 파일을 수정해 조금 전에 만든 Home과 About 컴포넌트를 Route 컴포넌트를 이용해 보이도록 합니다.
// App.js 파일
import React from 'react';
import { Route } 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;
코드를 성공적으로 작성했다면, 서버를 실행했을 때 'localhost:3000/'의 첫 화면으로 작성한 Home 컴포넌트가 나오고, 'localhost:3000/about'를 들어가면 기존의 Home 컴포넌트와 About 컴포넌트가 함께 나옵니다. 이는 App.js 파일에서 exact를 true 수정하면 됩니다.
// App.js 파일
import React from 'react';
import { Route } from 'react-router-dom';
import About from './About';
import Home from './Home';
const App = () => {
return (
<div>
<Route path="/" component={Home} exact={ture} /> // ** 수정한 부분 **
<Route path="/about" component={About} />
</div>
);
};
export default App;
다른 주소로 이동시키는 방법으로 컴포넌트 Link와 <a>를 대표적으로 꼽을 수 있습니다. 두 개는 차이를 가지는데, 이를 표로 보면 다음과 같습니다.
|
특징
|
|
Link
|
페이지 전환 : 페이지 새로 불러오지 않는다. 애플리케이션은 유지한 상태로 페이지 주소만 변경한다.
a 태그로 이루어져있지만, 페이지 전환 방지 기능이 있다. |
|
<Link to=“주소“>내용</Link>
|
||
a
|
페이지 전환 : 페이지 전체를 새로 불러온다.
|
|
<a>내용</a>
|
그래서 link 컴포넌트를 추가해 app.js 파일을 수정하면 아래와 같습니다.
// App.js 파일
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} exact={true} />
<Route path="/about" component={About} />
</div>
);
};
export default App;
li 태그에 의해서, 들여 써진 문단기호로 '홈'과 '소개' 페이지로 연결되는 것을 확인할 수 있습니다.
최신 버전의 리액트 라우터 v5부터 Route 하나에 여러 개의 path를 설정할 수 있습니다.
하지만 이전 버전에서 여러 Path에 같은 컴포넌트를 보여주고 싶다면 아래와 같이 코드를 작성하면 됩니다.
// App.js 파일
import React from 'react';
import { Route } from 'react-router-dom';
import About from './About';
import Home from './Home';
const App = () => {
return (
<div>
<Route path="/" component={Home} exact={true} />
<Route path="/about" component={About} />
<Route path="/info" component={About} /> // **추가 사항**
</div>
);
};
export default App;
혹은,
<Route path="/about" component={About} />
<Route path="/info" component={About} />
위의 두 코드를 하나로 합쳐 아래와 같이 작성할 수 있습니다.
<Route path={['/about', '/info']} component={About} />
|
특징
|
|
파라미터
|
/profiles/velopert
|
|
특정 아이디 혹은 이름을 사용해 조회
|
||
쿼리
|
/about?details=true
|
|
어떤 키워드 검색하거나 페이지에 필요한 옵션을 전달
|
profile이라는 컴포넌트를 만들어 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;
그리고 app.js 파일도 수정해 주는데, 위에 작성한 profile 컴포넌트를 위한 라우터를 정의하기 위해서입니다. username 값을 조회할 수 있다는 특징을 가집니다. 또 페이지 상단에 각 프로필 페이지로 이동하는 링크도 달기 위해 import문을 작성합니다.
// 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';
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} exact={true} />
<Route path={['/about', '/info']} component={About} />
<Route path="/profile/:username" component={Profile} />
</div>
);
};
export default App;
About 페이지에서 location 객체에 들어있는 search 값에서 쿼리를 조회합니다.
location의 형태 http://localhost:3000/about?detail=true 주소로 들어갈 때의 location 객체 |
{ “pathname”: “/about”,
“search”: “?detail=true”, “hash”: “ ” } |
URL 쿼리를 읽을 때는 위 객체가 지닌 값 중에서 search 값을 확인해야 하는데, 이건 문자열 형태로 되어 있습니다. URL 쿼리는? detail=true&another=1과 같이 문자열에 여러 가지 값을 설정해 줄 수 있습니다. search 값에서 특정 값을 읽어 오기 위해서는 이 문자열을 객체 형태로 변환해야 합니다.
쿼리 문자열을 객체로 변환할 때는 qs라는 라이브러리를 사용하는데, 아래의 코드를 이용해 라이브러리를 설치합니다.
$ yarn add 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;
쿼리를 사용할 때, 쿼리 문자열을 객체로 파싱하는 과정에서 결과 값은 언제나 문자열이다. value=1 혹은? value=true와 같이 숫자나 논리 자료형을 사용한다고 우리가 원하는 형태로 변환되는 것이 아니라, "1", "true"와 같이 문자열 형태로 받아집니다. 그렇기에 숫자를 원하면 parseInt 함수를 통해 숫자로 변환하고, 논리 자료형 값을 사용해야 하는 경우에는 "true" 문자열이랑 일치하는지 비교해 확인해야 한다.
서브 라우트는 라우트 내부에 또 라우트를 정의합니다. 라우트로 사용되고 있는 컴포넌트의 내부에 Route 컴포넌트를 또 사용하면 이를 의미할 수 있습니다.
기존의 App 컴포넌트에서는 두 종류의 프로필 링크를 보여 주었는데, 이를 잘라내서 프로필 링크를 보여 주는 Profiles라는 라우트 컴포넌트를 따로 만들고, 그 안에서 Profile 컴포넌트를 서브 라우트로 사용하도록 코드를 작성하면 아래와 같습니다.
// Profiles.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;
이 코드에서 첫 번째 Route 컴포넌트에는 component 대신 render라는 props를 넣어 주었습니다. 컴포넌트 자체를 전달하는 것이 아니라, 보여 주고 싶은 JSX를 넣어 줄 수 있습니다. 지금처럼 따로 컴포넌트를 만들기 애매한 상황에 사용해도 되고, 컴포넌트에 props를 별도로 넣어 주고 싶을 때도 사용할 수 있습니다.
JSX에서 props를 설정할 때 값을 생략하면 자동으로 true로 설정됩니다. 예를 들어 현재 Profile 컴포넌트의 첫 번째 Route에서 exact={true} 대신 그냥 exact라고만 적어서 exact={true}와 같은 의미입니다.
컴포넌트를 다 만들었다면 기존의 App 컴포넌트에 있던 프로필 링크를 지우고, Profiles 컴포넌트를 /profiles 경로에 연결시켜 주세요. 그리고 해당 경로로 이동하는 링크도 추가하세요.
// App.js
import React from 'react';
import { Route, Link } from 'react-router-dom';
import About from './About';
import Home from './Home';
import Profiles from './Profiles';
const App = () => {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profiles">프로필</Link>
</li>
</ul>
<hr />
<Route path="/" component={Home} exact={true} />
<Route path={['/about', '/info']} component={About} />
<Route path="/profiles" component={Profiles} />
</div>
);
};
export default App;
13.6.1 history
history 객체는 라우트로 사용된 컴포넌트에 match, location과 함께 전달되는 props 중 하나로, 이 객체를 통해 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있습니다. 예를 들어 특정 버튼을 눌렀을 때 뒤로 가거나, 로그인 후 화면을 전환하거나, 다른 페이지로 이탈하는 것을 방지해야 할 때 history를 활용합니다.
HistorySample이라는 컴포넌트를 만들면 아래의 코드와 같습니다.
// 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 에서 수정을 하면 다음과 같습니다.
// App.js 파일
import React from 'react';
import { Route, Link } from 'react-router-dom';
import About from './About';
import Home from './Home';
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="/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="/profiles" component={Profiles} />
<Route path="/history" component={HistorySample} />
</div>
);
};
export default App;
13.6.2 withRouter
withRouter 함수는 HoC(Higher-order Component)입니다. 라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체를 접근할 수 있게 해 줍니다.
WithRouterSample이라는 컴포넌트를 만들어서 withRouter 함수를 사용하면 아래의 코드와 같습니다.
//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)} //JSON에 들여쓰기가 적용
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);
이렇게 만든 파일을 profiles 컴포넌트에 랜더링하는 코드는 아래와 같습니다.
// Profiles.js 파일
import React from 'react';
import { Link, Route } from 'react-router-dom';
import Profile from './Profile';
import WithRouterSample from './WithRouterSample';
const Profiles = () => {
return (
<div>
// 생략
<WithRouterSample />
</div>
);
};
export default Profiles;
그런데 여기서 match 객체를 보면 params가 비어 있습니다. withRouter를 사용하면 현재 자신을 보여 주고 있는 라우트 컴포넌트(현재 Profiles)를 기준으로 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 Profile = ({ match }) => {
const { username } = match.params;
const profile = data[username];
if (!profile) {
return <div>존재하지 않는 사용자입니다.</div>;
}
return (
<div>
(…)
<WithRouterSample />
</div>
);
};
export default withRouter(Profile);
13.6.3 Switch
Switch 컴포넌트는 여러 Route를 감싸서 그중 일치하는 단 하나의 라우트만을 렌더링시켜 줍니다. Switch를 사용하면 모든 규칙과 일치하지 않을 때 보여 줄 Not Found 페이지도 구현할 수 있습니다.
// App.js 파일
import React from 'react';
import { Route, Link, Switch } from 'react-router-dom';
import About from './About';
import Home from './Home';
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="/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="/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;
이제 존재하지 않는 페이지인 http://localhost:3000/nowhere에 들어가면, '이 페이지는 존재하지 않습니다:'라는 문구를 확인할 수 있습니다.
13.6.4 NavLink
NavLink는 Link와 비슷합니다. 현재 경로와 Link에서 사용하는 경로가 일치하는 경우 특정 스타일 혹은 CSS 클래스를 적용할 수 있는 컴포넌트입니다. NavLink에서 링크가 활성화되었을 때의 스타일을 적용할 때는 activeStyle 값을, CSS 클래스를 적용할 때는 activeClassName 값을 props로 넣습니다.
Profiles에서 사용하고 있는 컴포넌트에서 Link 대신 NavLink를 사용하게 하고, 현재 선택되어 있는 경우 검정색 배경에 흰색 글씨로 스타일을 보여 주게끔 코드를 수정해 보겠습니다.
// Profiles.js 파일
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;
이번에는 리액트 라우터가 무엇인지, 이를 사용하는 방법은 무엇인지부터 시작해 주소 경로에 따라 다양한 페이지를 보여 주는 방법까지 공부해 보았습니다. 본 과정에서 실습한 것처럼 작은 규모의 프로젝트는 괜찮지만, 큰 규모의 프로젝트를 진행하다 보면 한 가지 문제를 마주할 수도 있습니다. 자바스크립트 파일에 웹 브라우저에서 사용할 컴포넌트, 상태 관리를 하는 로직, 그 외 여러 기능을 구현하는 함수들이 점점 쌓이면서 그 크기가 지나치게 커질 수 있다는 점입니다. 이를 해결해 주는 기술이 바로 코드 스플리팅으로 추후 자세히 다룹니다.
문제 | 답 | |
1 | 한 페이지로 이루어진 애플리케이션을 ( )이라고 한다. | SPA |
2 | ( )는 다른 주소에 다른 화면을 보여주는 것을 의미한다. | 라우터 |
3 | 라우터 등을 사용하더라도, 자바스크립트 파일이 커지는 등 문제가 생길 수 있는데, 이를 위한 해결책으로 ( )이 있다. | 코드 스플리팅 |
4 | 서버에서 사용자에게 보여줄 화면을 준비하는 전통적인 구조를 벗어나, 리액트 같은 라이브러리 호은 프레임워크를 사용하면 ( )와 같은 장점이 있다. | - 과도한 트래픽으로 인한 오류 감소 및 속도 저하 예방 - 서버 부하 감소 등 |
5 | Link 컴포넌트는 다른 주소로 이동시키는 방법이다. 이것이 <a>와 다른 점은 ( )이다. | - 페이지 전체 새로 불러오는 것이 아닌, 페이지 주소만 변경 - 페이지 전환 방지 기능 등 |
6 | URL 파라미터의 특징은 ( A )이라면, 쿼리의 특징은 ( B )이다. | A : 특정 아이디 혹은 이름을 사용해 조회 B : 어떤 티워드를 검색하거나 페이지에 필요한 옵션을 전달 |
7 | 쿼리 문자열을 객체로 파싱하는 과정에서 결과 값은 언제나 ( )이다. | 문자열 |
8 | Route 컴포넌트를 이용하는 통상적인 코드 구조를 적어라. | <Route path=“주소규칙“ component={보여 줄 컴포넌트} /> |
9 | 'http://localhost:3000/history?detail=false' 주소로 들어갈 때의 location 형태를 적어라 | { “pathname”: “/history”,
“search”: “?detail=false”, “hash”: “ ” } |
Corner React1
Editor: 라마
[리액트 스타터1] 17장. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 (0) | 2023.01.12 |
---|---|
[리액트 스타터1] 16장. 리덕스 라이브러리 이해하기 (0) | 2023.01.12 |
[리액트 스타터1] 12장. immer를 사용하여 더 쉽게 불변성 유지하기 (1) | 2022.12.29 |
[리액트 스타터1] 11장. 컴포넌트 성능 최적화 (0) | 2022.12.29 |
[리액트 스타터1] 10장 : 일정 관리 웹 애플리케이션 만들기 (0) | 2022.12.22 |