HTML 파일들로 페이지가 구성
url이 파일 경로와 이름이 된다
url 상 파일명이 없을 경우 index.html을 찾으려 한다
정적인 파일을 웹 서버로 제공하는 방식뿐만 아니라 PHP, Java, Node.js 등을 이용해 동적으로 HTML을 생성해서 제공하는 방식이다.
서버가 HTML을 만들어서 제공하고 이후 동작을 JS를 통해 하다보니 렌더링 시점이 꼬이는 문제가 있다. 그러다 보니 서버 렌더링 시점에서 구현했던 것을 클라이언트에서 동적으로 렌더링 해야한다면 중복해서 구현이 필요하기도 했다.
UI를 생성하는 부분을 템플릿화하여 서버, 클라이언트에서 같이 쓸 수 있는 방안도 나오긴 했지만 환경에 따라 동작이 100% 일치하지 않는 문제가 발생하기도 했고, 궁극적으로 유저와의 상호작용 처리는 클라이언트 사이드에서 이루어져야 하기 때문에 모든 처리를 서버에서 할 수 가 없다.
위와 같은 흐름 속에서 서버는 API만 처리하고 아예 모든 렌더링을 하나의 HTML 페이지로 클라이언트에서 다루는 방식이다. 이 경우 웹 애플리케이션에서는 요청에 따른 데이터만 JSON 등으로 내려주고 별도의 클라이언트 애플리케이션을 구성한다.
클라이언트에선 html 파일은 index.html 하나만 존재하며 클라이언트로 오는 모든 url 요청을 index.html로 돌리며 이후 동작은 url을 보고 어떤 페이지를 그릴지 동적으로 처리합니다.
이전 방식에서는 페이지를 이동할 때마다 페이지의 모든 내용을 새로 불러왔어야 했지만 SPA 내에선 렌더링만 다시 동적으로 하므로 처음 로딩 이후에는 네트워크 부담이 줄어들어 성능 개선에 효과가 있습니다.
url 맨 뒤에 #을 이용해 처리하는 방식입니다
#은 같은 페이지 내의 요소를 가리킬 때 많이 사용되었습니다.
다음과 같이 모두 같은 페이지( index.html => localhost:5000/ )에서 불러오며, 뒤에 붙은 #은 location.hash를 통해 가져올 수 있습니다.
localhost:5000/
localhost:5000/#list
localhost:5000/#qna
hash를 통해 어떤 페이지를 렌더링 할 지를 정하는 로직을 구현하는 방식으로 아래와 같이 hashchange 이벤트를 통해 라우팅 기능을 구현할 수 있습니다.
hashbang으로 url을 다룰 때 querystring을 다루기에 불편한점이 단점으로 부각되었고 이를 개선하여 url을 다루기 쉽도록 제공해주는 API입니다.
브라우저에서 페이지 로딩을 하면, 세션 히스토리를 갖습니다.
세션 히스토리는 페이지를 이동할 때마다 쌓이게 되며, 이를 통해 뒤로가기 시 이전 페이지로 가거나 뒤로 간 이후 다시 앞으로 가는 동작들이 가능한 것 입니다.
// 뒤로가기
window.histroy.back();
// 앞으로가기
window.histroy.forward();
// 특정 위치로 가기
window.history.go(-1);
// window.history.back()과 동일하기 한 페이지 뒤로 이동합니다.
window.history.go(1);
// window.history.forward()와 동일하게 한 페이지 앞으로 이동합니다.
window.history.go(-2);
// 두 페이지 이전으로 이동합니다.
console.log(window.history.length);
// 현재 페이지를 기준으로 앞, 뒤 페이지가 없다면 1이 출력됩니다.
위와 같이 브라우저의 히스토리 기록을 이동할 수 있고 그리고 length 속성을 이용하여 히스토리가 몇 페이지로 되어 있는지 확인 할 수 있습니다.
pushState, replaceState 두 개의 함수로 화면 이동 없이 현재 url을 업데이트 할 수 있습니다.
pushState
: 세션 히스토리에 새 url 상태를 쌓는다.
replaceState
: 세션 히스토리에 새 url 상태를 쌓지 않고, 현재 url을 대체한다.
hashbang의 hashchange 이벤트 처럼, popstate 이벤트로 세션 히스토리의 변경을 감지할 수 있다.
일반 url 형식을 따르기 때문에 querystring도 자유롭게 다룰 수 있다.
history.pushState(state, title, url);
history.replaceState(state, title, url);
state
: history.state에서 꺼내쓸 수 있는 값으로 가장 최신의 값을 가리킨다.
title
: 변경될 페이지의 title을 가리키는 값인 것 같지만 거의 대부분의 브라우저에서 지원하지 않고 보통 빈 string을 넣는다.
url
: 세션 히스토리에 새로 넣을 url 이며 a 태그나 location.href로 url을 변경하는 것 과 다르게 url이 변경된다고 해서 화면이 리로드 되거나 그러지 않는다. ./url
과 같이 상대경로도 지정가능하며 절대경로 역시 가능하다.
<!doctype html>
<html lang="ko">
<head>
<title>History API Sample</title>
<meta charset="utf-8">
<style>
section {margin: 20px 0;}
</style>
</head>
<body>
<section>
<h2>push state</h2>
<button id="push-state1">pushState1</button>
<button id="push-state2">pushState2</button>
<button id="push-state3">pushState3</button>
</section>
<section>
<h2>replace state</h2>
<button id="replace-state">replaceState</button>
</section>
<section>
<h2>history state</h2>
<span id="state"></span>
</section>
</body>
</html>
// 현재의 history state 값을 출력합니다.
const currentHistoryState = () => {
document.getElementById('state').innerText = JSON.stringify(history.state);
};
currentHistoryState();
// pushState()
document.getElementById('push-state1').addEventListener('click', () => {
history.pushState({data: 'pushState1'}, '', '/push-state1');
currentHistoryState();
});
document.getElementById('push-state2').addEventListener('click', () => {
history.pushState({data: 'pushState2'}, '', '/push-state2');
currentHistoryState();
});
document.getElementById('push-state3').addEventListener('click', () => {
history.pushState({data: 'pushState3'}, '', '/push-state3');
currentHistoryState();
});
// replaceState()
document.getElementById('replace-state').addEventListener('click', () => {
history.replaceState({data: 'replaceState'}, '', '/replace-state');
currentHistoryState();
});
// 브라우저의 뒤로가기 / 앞으로가기를 누르면 history state 값을 확인하여 출력합니다.
window.addEventListener('popstate', () => {
currentHistoryState();
});
push state 버튼들을 누르면 화면을 새로 그리지 않고 URL이 파라미터로 보낸 URL 값으로 변경되고 히스토리 엔트리에 추가됩니다. 그래서 뒤로가기나 앞으로가기를 누르면 이전의 URL로 다시 화면을 새로 그리지 않고 이동할 수 있습니다.
replace state 버튼은 새로운 엔트리를 추가하지 않고 현재의 엔트리 값을 변경하기 때문에 뒤로가기로 이전 URL로 이동할 수 없습니다.
pushState()
pushState1 클릭 -> URL이 /push-state1 로 변경 -> pushState2 클릭 -> URL이 /push-state2 로 변경 -> 브라우저의 뒤로가기 누르면 -> URL이 /push-state1 로 변경
replaceState()
pushState1 클릭 -> URL이 /push-state1 로 변경 -> pushState2 클릭 -> URL이 /push-state2 로 변경 -> replace-state 클릭 -> URL이 /replace-state 로 변경 -> 브라우저의 뒤로가기 누르면 -> URL이 /push-state1 로 변경
hitory api를 사용할 경우 hashbang과는 다르게 a 태그를 그대로 쓰면 의도한대로 동작하지 않습니다.
이는 a 태그를 클릭했을 때 기본 동작은 href에 연결된 url로 이동을 하는데, history api에서 SPA 처리를 하려면 이동을 하는게 아니라 해당 url에 대해 현재와 바뀌는 요소만 다시 렌더링을 해야하기 때문입니다.