📗목차
SPA는 Single Page Application(싱글 페이지 애플리케이션)의 약어이다.
말 그대로 한 개의 페이지로 이루어진 애플리케이션이라는 의미이다.
전통적인 웹 페이지는 여러 페이지로 구성되어 있다.
기존에는 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아 오고, 페이지를 로딩할 때마다 서버에서 리소스를 전달받아 해석한 뒤 화면에 보여줬다. 이렇게 사용자에게 보이는 화면은 서버 측에서 준비했다. 사전에 html 파일을 만들어서 제공하거나, 데이터에 따라 유동적인 html을 생성해 주는 템플릿 엔진을 사용하기도 했다.
요즘은 웹에서 제공되는 정보가 정말 많기 때문에 새로운 화면을 보여줘야 할 때마다 서버 측에서 모든 뷰를 준비한다면 성능상의 문제가 발생할 수 있다. 예를 들면 트래픽이 너무 많이 나올 수도 있고, 사용자가 몰려 서버에 높은 부하가 쉽게 걸릴 수도 있다. 속도와 트래픽 측면에서는 캐싱과 압축을 해서 서비스를 제공하면 어느 정도 최적화될 수 있겠지만 사용자의 인터렉션이 자주 발생하는 모던 웹 애플리케이션에는 적당하지 않을 수도 있다. 애플리케이션 내에서 화면 전환이 일어날 때마다 html을 계속 서버에 새로 요청하면 사용자의 인터페이스에서 사용하고 있던 상태를 유지하는 것도 번거롭고, 바뀌지 않는 부분까지 새로 불러와서 보여줘야 하기 때문에 불필요한 로딩이 있어서 비효율적이다.
그래서 리액트 같은 라이브러리 혹은 프레임워크를 사용하여 뷰 렌더링을 사용자의 브라우저가 담당하도록 하고, 우선 애플리케이션을 브라우저에 불러와서 실행시킨 후에 사용자와의 인터렉션이 발생하면 필요한 부분만 자바스크립트를 사용하여 업데이트 해준다. 만약 새로운 데이터가 필요하다면 서버 API를 호출하여 필요한 데이터만 새로 불러와 애플리케이션에서 사용할 수도 있다.
싱글 페이지라고 해서 화면이 한 종류일까? 그렇진 않다. 예를 들어 블로그를 개발한다면 홈, 포스트 목록, 포스트, 글쓰기 등의 화면이 있다. SPA의 경우 서버에서 사용자에게 제공하는 페이지는 한 종류이지만, 해당 페이지에서 로딩된 자바스크립트와 현재 사용자 브라우저의 주소 상태에 따라 다양한 화면을 보여 줄 수 있다.
다른 주소에 다른 화면을 보여 주는 것을 라우팅이라고 한다. 리액트 라이브러리 자체에 이 기능이 내장되어 있지는 않다. 그 댓니 브라우저의 API를 직접 사용하여 이를 관리하거나, 라이브러리를 사용하여 이 작업을 더욱 쉽게 구현할 수 있다.
리액트 라우팅 라이브러리는 리액트 라우터(react-router), 리치 라우터(reach-router), Next.js 등 여러 가지가 있다. 우리는 역사가 가장 길고 사용 빈도가 가장 높은 리액트 라우터를 사용할 것이다.
리액트 라우터는 클라이언트 사이드에서 이루어지는 라우팅을 아주 간단하게 구현할 수 있도록 해준다. 더 나아가서 나중에 서버 사이드 렌더링을 할 때도 라우팅을 도와주는 컴포넌트들을 제공해준다.
페이지 로딩 시 사용자가 실제로 방문하지 않을 수도 있는 페이지의 스크립트도 불러오기 때문이다. 그렇지만 이는 코드 스플리팅을 사용하면 라우트별로 파일들을 나누어서 트래픽과 로딩 속도를 개선할 수 있다.
그렇기 때문에 구글, 네이버, 다음 같은 검색 엔진의 검색 결과에 페이지가 잘 나타나지 않을 수도 있다. 구글 검색 엔진에서 사용하는 크롤러의 경우 자바스크립트를 실행해 주는 기능이 탑재되어 있지만, 크롤링하는 모든 페이지에서 자바스크립트를 실행하고 있지는 않다(2019년 기준).
이러한 문제점들은 나중에 배울 서버 사이드 렌더링을 통해 모두 해결할 수 있다.
새 프로젝트를 생성하여 터미널에 다음 명령어를 입력하자.
npm i react-router-dom
index.js 파일에서 react-router-dom에 내장되어 있는 BrowserRouter라는 컴포넌트를 사용하여 감싸면 된다. 이 컴포넌트는 웹 애플리케이션에 HTML5의 History API를 사용하여 페이지를 새로고침하지 않고도 주소를 변경하고, 현재 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있도록 해준다.
src/index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import './index.css'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <App /> </BrowserRouter> );
이제 라우트로 사용할 페이지 컴포넌트를 만들 차례이다. 사용자가 웹 사이트에 들어왔을 때 맨 처음 보여 줄 Home 컴포넌트와 웹 사이트를 소개하는 About 컴포넌트를 만들어 보자.
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;
Route라는 컴포넌트를 사용하여 사용자의 현재 경로에 따라 다른 컴포넌트를 보여주겠다. Route 컴포넌트를 사용하면 어떤 규칙을 가진 경로에 어떤 컴포넌트를 보여줄지 정의할 수 있다.
<Route path="주소규칙" element={<컴포넌트명 />} />
App.js
import React from 'react'; import { Routes, Route } from 'react-router-dom'; import About from './About'; import Home from './Home'; const App = () => { return ( <div> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </div> ); }; export default App;
위의 코드는 react-router-dom v6
기준으로 수정한 코드이다.
책에서는 react-router-dom v5
기준이라 아래 코드처럼 입력하라 나와 있다.
return (
<div>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
</div>
);
다음은 localhost:3000/about 경로를 입력해보자.
Link 컴포넌트는 클릭하면 다른 주소로 이동시켜 주는 컴포넌트이다.
Link 컴포넌트를 사용하여 페이지를 전환하면, 페이지를 새로 불러오지 않고 애플리케이션은 그대로 유지한 상태에서 HTML5 History API를 사용하여 페이지의 주소만 변경해준다. Link 컴포넌트 자체는 a 태그로 이루어져 있지만, 페이지 전환을 방지하는 기능이 내장되어 있다.
일반 웹 애플리케이션에서는 a 태그를 사용하여 페이지를 전환하는데, 리액트 라우터를 사용할 때는 이 태그를 직접 사용하면 안 된다.
이 태그는 페이지를 전환하는 과정에서 페이지를 새로 불러오기 때문에 애플리케이션이 들고 있던 상태들을 모두 날려 버리게 된다. 렌더링된 컴포넌트들도 모두 사라지고 다시 처음부터 렌더링하게 된다.
Link 컴포넌트는 다음과 같이 사용한다.
<Link to="주소">내용</Link>
이제 App 컴포넌트에서 "/", "/about" 경로로 이동하는 Link 컴포넌트를 만들어 보자.
import React from 'react';
import { Routes, 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 />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
);
};
export default App;
페이지 상단에 있는 링크를 눌러 보자. 페이지 전환이 잘 되는가?
페이지 주소를 정의할 때 가끔은 유동적인 값을 전달해야 할 때도 있다. 이는 파라미터와 쿼리로 나눌 수 있다.
/profiles/oeanb
/about?details=true
유동적인 값을 사용해야 하는 상황에서 파라미터를 써야 할지 쿼리를 써야 할지 정할 때, 무조건 따라야 하는 규칙은 없다. 다만 일반적으로 파라미터는 특정 아이디 혹은 이름을 사용하여 조회할 때 사용하고, 쿼리는 우리가 어떤 키워드를 검색하거나 페이지에 필요한 옵션을 전달할 때 사용한다.
Profile 페이지에서 파라미터를 사용해본다. /profile/oenab
와 같은 형식으로 뒷부분에 유동적인 username 값을 넣어 줄 때 해당 값을 props로 받아 와서 조회하는 방법을 알아보자.
Profile이라는 컴포넌트를 다음과 같이 만들어 보자.
import React from 'react';
import { useParams } from 'react-router-dom';
const data = {
oeanb: {
name: '호박이와칼림바',
description: '리액트를 좋아하는 개발자'
},
gildong: {
name: '홍길동',
description: '고전 소설 홍길동전의 주인공'
}
};
const Profile = () => {
const { username } = useParams();
const profile = data[username];
if(!profile) {
return <div>존재하지 않는 사용자입니다.</div>;
}
return (
<div>
<h3>
{username}({profile.name})
</h3>
<p>{profile.description}</p>
</div>
);
};
export default Profile;
React에서 라우터 사용 시 파라미터 정보를 가져오기 위해서 useParams를 사용하는데,
useParams는 URL에 포함되어있는 Key, Value 형식의 객체를 반환해주는 역할을 한다.
이제 App 컴포넌트에 Profile 컴포넌트를 위한 라우트를 정의해보아라. 이번에 사용할 path 규칙에는 /profiles/:username
이라고 넣어주면 된다.
라우트를 정의하고 나서 상단에 각 프로필 페이지로 이동하는 링크도 추가해보자.
import React from 'react';
import { Routes, 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="/profiles/oeanb">oeanb 프로필</Link>
</li>
<li>
<Link to="/profiles/gildong">gildong 프로필</Link>
</li>
</ul>
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path={'/about'} element={<About />} />
<Route path={'/profiles/:username'} element={<Profile />} />
</Routes>
</div>
);
};
export default App;
이번에는 About 페이지에서 쿼리를 받아올 것이다. 쿼리는 location 객체에 들어 있는 search 값에서 조회할 수 있다.
location의 형태는 다음과 같다.
{
"pathname": "/about",
"search": "?detail=true",
"hash": ""
}
location 객체는 라우트로 사용된 컴포넌트에게 props로 전달되며, 웹 애플리케이션의 현재 주소에 대한 정보를 지니고 있다.
위 location 객체는 http://localhost:3000/about?detail=true 주소로 들어갔을 때의 값이다.
URL 쿼리를 읽을 때는 위 객체가 지닌 값 중에서 search 값을 확인해야 한다. 이 값은 문자열 형태로 되어 있다. URL 쿼리는 ?detail=true&another=1과 같이 문자열에 여러 가지 값을 설정해 줄 수 있다. search 값에서 특정 값을 읽어 오기 위해서는 이 문자열을 객체 형태로 변환해줘야 한다.
쿼리 문자열을 객체로 변환할 때는 qs라는 라이브러리를 사용한다.
npm을 사용하여 해당 라이브러리를 설치해보자.
npm i qs
현재 페이지의 위치 정보를 가져오기 위해 useLocation Hook
를 사용할 것이다.
location 변수에다 location 객체를 넣고, About 컴포넌트에서 location.search 값에 있는 detail이 true인지 아닌지에 따라 추가 정보를 보여 주도록 만들어 보자.
About.js
import React, { useEffect } from 'react'; import qs from 'qs'; import { useLocation } from 'react-router-dom'; const About = () => { const location = useLocation(); useEffect(() => { console.log(location); // 콘솔에 location 객체 출력 }, [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;
http://localhost:3000/about?detail=true 이 주소로 들어가면 아래와 같은 화면이 나오는가?
서브 라우트는 라우트 내부에 또 라우트를 정의하는 것을 의미한다. 라우트로 사용되고 있는 컴포넌트의 내부에 Route 컴포넌트를 또 사용하는 방식이다.
서브 라우트를 직접 만들어 보자. 기존의 App 컴포넌트에서는 두 종류의 프로필 링크를 보여 줬는데, 이를 잘라내서 프로필 링크를 보여 주는 Profiles라는 라우트 컴포넌트를 따로 만들고, 그 안에서 Profile 컴포넌트를 서브 라우트로 사용하도록 코드를 작성해보겠다.
우선 Profiles라는 컴포넌트를 만들자.
import React from 'react';
import { Link, Routes, Route } from 'react-router-dom';
import Profile from './Profile';
const Profiles = () => {
return (
<div>
<h3>사용자 목록</h3>
<ul>
<li>
<Link to="/profiles/oeanb">oeanb</Link>
</li>
<li>
<Link to="/profiles/gildong">gildong</Link>
</li>
</ul>
<Routes>
<Route path="/" element="사용자를 선택해 주세요." />
<Route path=":username" element={<Profile />} />
</Routes>
</div>
);
};
export default Profiles;
이 코드에서 첫 번째 Route 컴포넌트에는 element 대신 render라는 props를 넣어 주었다.
컴포넌트 자체를 전달하는 것이 아니라, 보여 주고 싶은 JSX를 넣어 줄 수 있다. 지금처럼 따로 컴포넌트를 만들기 애매한 상황에 사용해도 되고, 컴포넌트에 props를 별도로 넣어주고 싶을 때도 사용할 수 있다.
컴포넌트를 다 만들었다면 기존의 App 컴포넌트에 있던 프로필 링크를 지우고, Profiles 컴포넌트를 /profiles 경로에 연결시키자. 그리고 해당 경로로 이동하는 링크도 추가하자.
App.js
import React from 'react'; import { Routes, 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 /> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="profiles/*" element={<Profiles />} /> </Routes> </div> ); }; export default App;
useNavigate Hook을 실행하면 페이지 이동을 할 수 있게 해주는 함수를 반환한다.
반환하는 함수를 navigate라는 변수에 저장 후 navigate의 인자로 설정한 path값을 넘겨주면 해당 경로로 이동할 수 있다.
페이지 전환 시 추가로 처리해야 하는 로직이 있을 경우 useNavigate 사용
useNavigate를 활용한 NavigationSample 컴포넌트를 만들어 보자.
import React from 'react';
import { useNavigate } from 'react-router-dom';
const NavigationSample = () => {
const navigate = useNavigate();
const onGoBack = () => navigate(-1); // 뒤로 가기
const onGoHome = () => navigate('/'); // 홈으로 이동
return (
<div>
<button onClick={onGoBack}>뒤로</button>
<button onClick={onGoHome}>홈으로</button>
</div>
);
}
export default NavigationSample;
그 다음 NavigationSample 컴포넌트를 App에 렌더링 해보자.
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import About from './About';
import Home from './Home';
import Profiles from './Profiles';
import NavigationSample from './NavigationSample';
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="/navigate">Navigate 예제</Link>
</li>
</ul>
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="profiles/*" element={<Profiles />} />
<Route path="/navigate" element={<NavigationSample />} />
</Routes>
</div>
);
};
export default App;
코드를 저장하고 나면 아래와 같이 결과가 나온다.
withRouter 함수는 HoC(Higher-order Component)이었다.
라우틀 사용된 컴포넌트가 아니어도 match(현재는 → useParams), location(현재는 → useLocation), history(현재는 → useNavigate) 객체를 접근할 수 있게 해줬다.
하지만, withRouter는 react-router-dom이 v6으로 업그레이드 된 이후로 사라졌기 때문에 이를 대신하는 코드를 작성해보록 하겠다.
import React from 'react';
import { useLocation, useParams, useNavigate } from 'react-router-dom';
const WithRouterSample = () => {
const location = useLocation();
const params = useParams();
const navigate = useNavigate();
return (
<div>
<h4>location</h4>
<textarea
value={JSON.stringify(location, null, 2)}
rows={8}
readOnly={true}
/>
<h4>params</h4>
<textarea
value={JSON.stringify(params, null, 2)}
rows={4}
readOnly={true}
/>
<button onClick={() => navigate('/')}>홈으로</button>
</div>
);
};
export default WithRouterSample;
JSON.stringfy의 두 번째 파라미터와 세 번째 파라미터를 위와 같이 null, 2로 설정해주면 JSON에 들여쓰기가 적용된 상태로 문자열이 만들어진다.
다 만들었다면 이 컴포넌트를 Profiles 컴포넌트에 렌더링해보자.
import React from 'react';
import { Link, Routes, Route } from 'react-router-dom';
import Profile from './Profile';
import WithRouterSample from './WithRouterSample';
const Profiles = () => {
return (
<div>
(...)
<WithRouterSample />
</div>
);
};
export default Profiles;
코드를 저장하고 실행하면 다음과 같은 결과가 나온다.
현재 Profiles를 위한 라우트를 설정할 때는 path="/profiles"라고만 입력했으므로 username 파라미터를 읽어 오지 못 한 상태이다.
이번에는 Profiles에서 WithRouterSample 컴포넌트를 지우고 Profile 컴포넌트에 넣어보자.
import React from 'react';
import { useParams } from 'react-router-dom';
import WithRouterSample from './WithRouterSample';
(...)
const Profile = () => {
const { username } = useParams();
const profile = data[username];
if(!profile) {
return <div>존재하지 않는 사용자입니다.</div>;
}
return (
<div>
(...)
<WithRouterSample />
</div>
);
};
export default Profile;
이번에는 params.username을 제대로 보여 준다.
여러 라우트 중 단 하나의 라우트만 렌더링 시키는 방법에 대해서 알아보자.
모든 규칙과 일치하지 않을 때 보여 줄 Not Found 페이지를 구현해볼 것이다.
import React from 'react';
import { Routes, Route, Link, useLocation } from 'react-router-dom';
import About from './About';
import Home from './Home';
import Profiles from './Profiles';
import NavigationSample from './NavigationSample';
const App = () => {
const location = useLocation();
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profiles">프로필</Link>
</li>
<li>
<Link to="/navigate">Navigate 예제</Link>
</li>
</ul>
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="profiles/*" element={<Profiles />} />
<Route path="/navigate" element={<NavigationSample />} />
<Route path="/*" element={
<div>
<h1>존재하지 않는 페이지입니다.</h1>
{location.pathname}
</div>
}
/>
</Routes>
</div>
);
};
export default App;
location 객체 참고
location 객체의 pathname을 출력하기 위해 useLocation Hook 사용했다.
이제 존재하지 않는 페이지인 https://localhost:3000/nowhere에 들어가보자.
NavLink는 Link와 비슷하다. 현재 경로와 Link에서 사용하는 경로가 일치하는 경우 특정 스타일 혹은 CSS 클래스를 적용할 수 있는 컴포넌트이다.
Profiles에서 사용하고 있는 컴포넌트에서 Link 대신 NavLink를 사용하게 하고, 현재 선택되어 있는 경우 파란색 글씨로 선택되지 않았을 경우 흰색 글씨로 스타일을 보여 주게끔 코드를 수정해보자.
import React from 'react';
import { NavLink, Routes, Route } from 'react-router-dom';
import Profile from './Profile';
const Profiles = () => {
return (
<div>
<h3>사용자 목록</h3>
<ul>
<li>
<NavLink
to="/profiles/oeanb"
style={({ isActive }) => ({ color: isActive ? 'blue' : 'white' })}
>
<b>oeanb</b>
</NavLink>
</li>
<li>
<NavLink
to="/profiles/gildong"
className={({ isActive }) => 'nav-link' + (isActive ? ' activated' : '')}
>
<b>gildong</b>
</NavLink>
</li>
</ul>
<Routes>
<Route path="/" elemet="사용자를 선택해 주세요." />
<Route path=":username" element={<Profile />} />
</Routes>
</div>
);
};
export default Profiles;
oeanb를 선택했을 때
![]()
gildong을 선택했을 때
![]()
리액트 라우터를 사용하여 주소 경로에 따라 다양한 페이지를 보여주는 방법을 알아보았다. 큰 규모의 프로젝트를 진행하다 보면 한 가지 문제가 발생한다. 바로 웹 브라우저에서 사용할 컴포넌트, 상태 관리를 하는 로직, 그 외 여러 기능을 구현하는 함수들이 점점 쌓이면서 최종 결과물인 자바스크립트 파일이 매우 커진다는 점이다.
예를 들어 방금 만든 프로젝트는 About 페이지에 사용자가 들어왔을 때 지금 당장 필요하지 않은 Profile, NavigationSample 등의 컴포넌트의 코드까지 함께 불러온다. 다른 컴포넌트는 다른 페이지를 방문하는 등의 필요한 시점에 불러오면 더 효율적일 것이다. 이를 해결해주는 기술이 바로 코드 스플리팅인데 이는 추후에 다룰 예정이다.
참고문헌
김민준,「리액트를 다루는 기술 :실무에서 알아야 할 기술은 따로 있다!」, 길벗, 개정판[실은 2판] 2019 (개정판)