SPA(Single Page Application)는 페이지가 단일인 어플리케이션을 뜻한다. 보통의 웹 페이지는 사용자가 페이지를 이동할 때 마다 페이지가 새로고침되고 페이지를 로딩할 때 마다 서버로부터 리소스를 받아 렌더링 한다. 그렇기 때문에 로딩시간이 지연된다는 단점이 생긴다.
그러나 SPA는 브라우저에 어플리케이션을 로드하고 이후에는 렌더링이 필요한 부분의 데이터만 불러와 동적으로 보여줘 사용자에게 매력적인 경험을 선사할 수 있다.
그러나 처음 한번 어플리케이션을 로딩할 때 파일의 규모가 크면 브라우저에 빈 화면이 보일 수 있는 단점이 있다. 또 자바스크립트를 실행하지 않는 크롤러(Web Crawler)에서는 페이지의 정보를 제대로 받아가지 못하는 잠재적인 단점도 존재한다.
이번에 다뤄볼 react-router-dom
은 서드파티 라이브러리로 공식적으로 리액트에서 지원하는 기능이 아니지만 SPA를 쉽게 작성하게 해준다. 여러화면으로 구성된 웹 어플리케이션을 만들고 싶다면 react-router-dom
을 사용해 보길 권장한다.
선호하는 패키지 관리자를 사용하여 react-router-dom
을 설치한다.
$npm install react-router-dom
$yarn add react-router-dom
라우터 적용은 index.js에서 BrowserRouter
라는 컴포넌트를 사용한다.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
// * App 을 BrowserRouter 로 감싸준다
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
다음으로 라우트로 사용할 페이지 컴포넌트를 작성한다. 웹페이지에 처음 들어왔을 때 먼저 보여줄 main 페이지 사용자의 정보를 보여주는 user 페이지를 작성한다.
// Main.js 컴포넌트
const Main = ()=>{
return (
<div>
<h1>메인 페이지</h1>
<hr/>
<p>메인 페이지 내용 입니다.</p>
</div>
);
};
// User.js 컴포넌트
const User = () =>{
return (
<div>
<h1>사용자 페이지</h1>
<hr/>
<p>사용자의 정보를 보여주는 페이지 입니다.</p>
</div>
);
}
이제 페이지 컴포넌트를 작성했으니 Route
컴포넌트를 사용하여 주소에 따라 보여줄 컴포넌트를 지정할 수 있다.
import React from 'react';
import { Route } from 'react-router-dom';
import Main from './Main';
import User from './User';
const App = () => {
return (
<div>
<Route path='/' exact={true} component={Main}/>
<Route path='/user' component={User}/>
</div>
);
};
export default App;
Route 사용방법
<Route path="주소" component={보여줄 컴포넌트}/>
주소의 값이 정확히 일치할 때 컴포넌트를 보여 주려면exact
값을 true 로 설정한다.
위와 같이 작성했을 때 /
경로로 진입했을 때 메인페이지가 보이고 /user
경로로 진입시 사용자 페이지가 출력된다.
리액트 라우터에서는 a
태그를 사용할 수 없는 대신 Link
컴포넌트를 사용하여 다른 주소로 이동할 수 있다.
만약
a
태그를 사용하여 주소를 변경하면 리액트 앱에서 가지고 있던 상태들 초기화 되며 컴포넌트 또한 다시 렌더링 되므로Link
컴포넌트를 사용해야한다.
위의 예제를 사용하여 브라우저의 주소창이 아닌 Link
컴포넌트를 사용하여 페이지 이동을 하도록 코드를 작성하면 아래와 같다.
import React from 'react';
import { Route, Link } from 'react-router-dom';
import Main from './Main';
import User from './User';
const App = () => {
return (
<div>
<ul>
<li><Link to="/">메인 페이지</Link></li>
<li><Link to="/user">사용자 페이지</Link></li>
</ul>
<Route path='/' exact={true} component={Main}/>
<Route path='/user' component={User}/>
</div>
);
};
export default App;
지금까지 작성한 예제코드 실행 결과는 아래에서 확인할 수 있다.
페이지 전환시 보여질 컴포넌트에 인자를 전달해야 하는 경우 파라미터와 쿼리를 사용하여 전달할 수 있다.
파라미터 :
/profile/kimcoding
쿼리 :/about?details=true
보편적으로 파라미터는 특정 아이디나 이름을 가지고 조회할 때 사용하고, 쿼리는 어떤 키워드를 검색하거나 요청할 때 필요한 옵션을 전달할 때 사용한다.
파라미터를 사용하여 인자를 받아 해당하는 값에 맞는 정보를 표출하는 profile
컴포넌트를 작성하면 다음과 같다.
import React from 'react';
const userProfile = {
kimcoding:{
name:"김코딩",
desc:"내 이름은 김코딩 개발자죠."
},
parkhacker:{
name:"박해커",
desc:"안녕하시오, 나는 박해커요",
}
};
const Profile ({ match }) =>{
// 리액트 라우트 컴포넌트는 props에 match, location, history가 포함되어 있다.
// 파라미터를 받아올 때는 match를 사용한다.
const { userid } = match.params;
const user = userProfile[userid];
return user ? (
<div>
<h2>
{userid} - {user.name}
</h2>
<p>{user.desc}</p>
</div>
) : (
<div>존재하지 않는 사용자 입니다.</div>
);
}
export default Profile;
리액트 라우트는 props
에 match, location, history가 포함되어 있다. 그중 파라미터를 사용할 때는 match
의 params
값을 사용하여 사용할 수 있다. 아래 그림과 같이 주소창에 파라미터값을 줘서 확인할 수 있다.
이번에는 쿼리를 사용하여 값을 받아오는 컴포넌트를 작성해 본다. 쿼리는 라우트 컴포넌트에서 전달되는 location
객체에 있는 search
를 사용하여 읽어올 수 있다.
// location 예시 데이터
{
key: 'ac3df4',
pathname: '/somewhere',
search: '?some=search-string', // 쿼리로 입력받은 값
hash: '#howdy',
state: {
[userDefined]: true
}
}
위의 search
값은 문자열 형태로 되어있어서 이를 사용하기 위해서는 값을 직접변화해줘야 하지만 qs 라이브러리를 사용하면 간편하게 값으로 변환 가능하다.
search
에서 입력받은 detail
값이 "true"일 때 상세정보를 표시하는 컴포넌트를 작성하면 다음과 같이 작성할 수 있다.
import React from 'react';
import qs from 'qs';
const About = ({ location }) =>{
const query = qs.parse(location.search, {
// 첫 단어 ? 를 무시하는 옵션
ignoreQueryPrefix: true
});
const detail = query.detail === 'true';
return (
<div>
<h2>페이지 소개</h2>
<p>사용자 정보를 보여주는 페이지 입니다.</p>
{detail && <p>상세 정보를 표시합니다.</p>}
</div>
);
}
export default About;
이번에는 라우트안에 라우트를 포함시켜 서브라우트를 만들어 보자. ProfileList
컴포넌트를 작성하여 Profile
컴포넌트를 서브라우트로 사용하는 코드를 작성한다.
import React from 'react';
import { Link, Route } from 'react-router-dom';
import Profile from './Profile';
const ProfileList = () =>{
return (
<div>
<h3>사용자 목록</h3>
<ul>
<li>
<Link to="/profilelist/kimcoding">김코딩</Link>
</li>
<li>
<Link to="/profilelist/parkhacker">박해커</Link>
</li>
</ul>
<Route
path="/profilelist"
exact={true}
render={()=><div>사용자를 선택해 주세요</div>} />
<Route path="/profilelist/:userid" component={Profile} />
</div>
);
}
export ProfileList;
첫 번째 Route
에서 render
값을 사용했는데, 이값을 통해서 인라인으로 JSX를 사용할 수 있으며 라우트의 props
를 전달해 줄 수 있다. 이제 위 컴포넌트를 App
컴포넌트에 포함시켜 ProfileList
를 선택할 수 있게 작성한 결과는 다음과 같다.
라우트로 만들어진 컴포넌트는 match
, location
, history
객체를 props
로 가지고 있다. 이중 history
를 사용하여 메서드에서 라우터접근에 관한기능들을 사용할 수 있다. 예를들어 뒤로가기나 특정 경로로 이동 및 이탈방지 같은기능을 사용할 수 있다.
history 요소
- length - (number) 히스토리 스택에 쌓인 요소의 개수
- action - (string) 현재 액션상태 (PUSH, REPLACE, POP)
- location - (object) 현재 location 객체값
- push(path, [state]) - (function) 지정한 경로를 히스토리 스택에 추가
- replace(path, [state]) - (function) 현재경로를 지정한 경로로 대체한다
- go(n) - (function) 입력된 값만큼 히스토리 스택의 포인터를 이동시킨다
- goBack() - (function) go(-1) 과 똑같이 동작 : 뒤로가기
- goForward() - (function) go(1) 과 똑같이 동작 : 앞으로 가기
- block(prompt) - (function) 현재경로를 이동하기 전에 입력받은 프롬프트를 실행시켜 페이지 이탈을 방지한다
- listen - location의 변화를 감지했을 때 특정한 동작을 하기위해 사용한다(참고).
history
객체를 사용하여 간단한 예제를 만들어 보면 다음과 같다.
import React, { useEffect } from 'react';
const HistorySample = ({ history })=>{
const goBack = ()=>{
history.goBack();
};
const goMain = () =>{
history.push('/');
};
useEffect(()=>{
console.log(history);
const unblock = history.block('현재 페이지를 떠나시겠습니까 ?');
return ()=>{
unblock();
};
},[history]);
return (
<div>
<button onClick={goBack}>뒤로가기</button>
<button onClick={goMain}>메인 페이지로</button>
</div>
);
}
export default HistorySample;
각 버튼 클릭 이벤트에 history
객체를 사용하여 라우트를 제어하고 useEffect
를 사용하여 주소를 이동할 때 알람메시지를 표출하여 사용자의 선택에 따라서 라우트 변경을 제어할 수 있다. 위 예제 코드를 적용시킨 코드는 다음과 같다.
withRouter
를 사용하면 라우트 컴포넌트가 아닌 컴포넌트도 부모 라우터의 match
, location
, history
를 전달 받을 수 있게 된다.
import React from 'react';
import { withRouter } from 'react-router-dom';
const WithRouterSample = ({match, location, history}) =>{
return (
<div>
<h3>match</h3>
<textarea value={JSON.stringify(match, null, 2)} readOnly/>
<h3>location</h3>
<textarea value={JSON.stringify(location, null, 2)} readOnly/>
<button onClick={()=> history.push('/')}>메인으로</button>
</div>
);
}
export default withRouter(WithRouterSample);
위 컴포넌트를 ProfileList
에 적용시키면 다음과 같다.
import React from "react";
import { Link, Route } from "react-router-dom";
import Profile from "./Profile";
import WithRouterSample from "./WithRouterSample";
const ProfileList = () => {
return (
<div>
<h3>사용자 목록</h3>
<ul>
<li>
<Link to="/profilelist/kimcoding">김코딩</Link>
</li>
<li>
<Link to="/profilelist/parkhacker">박해커</Link>
</li>
</ul>
<Route
path="/profilelist"
exact={true}
render={() => <div>사용자를 선택해 주세요</div>}
/>
<Route path="/profilelist/:userid" component={Profile} />
<WithRouterSample />
</div>
);
};
export default ProfileList;
위 코드를 실행시킨 결과 실행시켜 확인해 보면 WithRouterSample
의 부모컴포넌트의 props를 전달받아 보여준다는 것을 알 수 있다.
NavLink
에 주어진 경로가 일치하는 경우 특정 클래스나 스타일을 줄수있는 Link
컴포넌트history.block
의 컴포넌트 버전react-router-dom
을 사용하여 SPA를 쉽게 구현할 수 있다.