Single-Page-Application(SPA)은 페이지 이동 시 새로고침 되지 않고 빠르게 이동하는 페이지 기법을 일컫는 말로 React와 CSR이 유행할 때 성행했던 기법이다. 대표적으로는 페이스북, 인스타그램이 많이 사용하는 기법으로 알려져있다.
React는 이런 SPA 기법을 기본적으로 제공하는 라이브러리이다. 많은 개발자들이 자사 서비스에 도입했고 학습하였고 나 또한 React를 학습하며 비교적 손쉽게 프로그래밍을 할 수 있었다.
하지만, React를 슬슬 대체하기 시작하는 Next를 보며 React에 너무나 익숙해진 나는 살짝 걱정이 되기 시작했다. 물론 둘의 개발방식은 거의 흡사하다. React를 할 줄 알면 Next는 금방 배울 수 있다. 하지만, 아예 React / Next를 대체하는 라이브러리가 나온다면? Vue 혹은 다른 프레임워크나 바닐라 자바스크립트가 다시 유행한다면? 내 근간이 흔들릴 것이라고 생각이 들었다.
따라서, 왜 이 라이브러리 / 프레임워크를 쓰는지 역으로 불편함을 겪으면서 원리를 파악해보려고 한다.
SPA의 가장 큰 특징은 하나의 페이지처럼 동작하는 것인데, 이 문장에 답이 숨어있었다. 정말로 한 페이지만 보여주는 것이다. 실제로 cra
로 리액트 프로젝트를 새로 만들었을 때 프로젝트 폴더를 살펴보면 index.html
로 하나의 페이지밖에 없다. 모든 컨텐츠들은 #root
에 들어가는것이다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
#root
안에 들어가는 자바스크립트들은 모두 모듈화 / 컴포넌츠화 되서 렌더링하게 된다.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App /> // <== 여기에 컴포넌츠들을 추가
</React.StrictMode>,
document.getElementById('root') // <== #root를 참조
);
reportWebVitals();
바닐라 자바스크립트는 페이지를 개별로 만들어서 페이지를 이동할 때마다 보여주지만,
SPA는 페이지를 이동할때 예전 컨텐츠를 지우고 새롭게 작성한다. 컨텐츠는 달라지지만, 보이는 것은 화이트보드처럼 하나이기 때문에 SPA라고 불리울 수 있는 것이다. 아마, 이 개념을 만든 사람은 정말로 페이지가 하나이기 때문에 이러한 정의를 내린 것 같다.
본격적으로 만드려면 리액트의 index.html처럼 메인으로 보여줄 html페이지를 만들어주고 그 안에서 동작할 모듈을 넣어준다. 이 모듈은 컨텐츠를 지우고 불러오는 과정을 하는 Router
이다.
<!-- index.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPA</title>
</head>
<body>
<div id="root">
<script type="module" src="/Router.js"></script>
</div>
</body>
</html>
그리고 Router.js를 정의해준다.
//Router.js
const routes = [
{
path: "/",
location: "mainpage",
},
{
path: "/mypage",
location: "mypage",
},
];
const App = () => {
const pageMatches = routes.map((route) => {
return {
route: route,
isMatch: window.location.pathname === route.path,
};
});
let match = pageMatches.find((pageMatch) => pageMatch.isMatch);
console.log(match);
};
App();
routes
배열의 객체들중 path
가 window.location.pathname
와 일치하다면 pageMatches
에서 true
를 가지게 되고 그 객체는 match
에 할당되어 콘솔에 찍히게 될 것이다.
잘 찍혀있는것이 확인되었다. 이로써 현재 페이지와 일치하는 페이지의 정보를 불러 올 수 있게 되었다. 그러나 다른 페이지로 직접 url주소를 변경할 경우 해당 html을 불러온다. 이는 바로 http요청을 보내서 실제 html페이지를 요구한다. 다른 방법을 사용해야한다.
위에 언급된 내용을 조작할 수 있는것이 History API이다. 여기서 pushState
메서드를 이용해 실제로는 해당 페이지를 방문하지 않지만, url주소에 삽입함으로써 해당 페이지를 방문한것처럼 보이게 할 것이다.
<!-- index.html -->
<body>
<button class="mainbutton">Mainpage</button>
<button class="mybutton">Mypage</button>
<div id="root">
<script type="module" src="/Router.js"></script>
</div>
</body>
//Router.js
...
const changeUrl = (requestedUrl) => {
history.pushState(null, null, requestedUrl);
Router();
};
window.addEventListener("click", (e) => {
if (e.target.classList.contains("mainbutton")) {
changeUrl("/");
} else if (e.target.classList.contains("mybutton")) {
changeUrl("/mypage");
}
});
html에 버튼을 추가해서 해당 버튼을 누를때 해당 페이지로 이동하게끔 만들었다.
그 다음, Router.js에서 history.pushState()
를 통해 매개변수로 받은 문자열로 url을 바꾸게끔 만들었고 버튼을 눌렀을 때 해당 페이지를 렌더( Router()
)하게끔 구현했다.
mypage버튼을 누르면 url이 잘바뀌고 페이지의 위치가 mypage인 것도 확인되었다!
직접적으로 http요청을 하지않고 history.pushState()
을 이용해 간접적으로 url을 바꾸고 바뀐 url을 router가 감지하고 해당 페이지의 데이터를 로드하게끔 하는것이 SPA의 방식이라고 정리할 수 있겠다! 물론 React 라이브러리는 이것보다 복잡하고 섬세할테지만, 근본적인 동작방식과 실제로 구현해보면서 React보다 불편한 점을 체감하면서 정말 많은 공부가 되었다.