단일 페이지 애플리케이션(Single Page Application)으로 서버에서 필요한 데이터만 비동기로 받아와서 현재 화면에 다시 렌더링하는 방식이다. 이는 하나의 페이지로 사용자의 요청을 즉시 생성하여 더 빠르게 화면 전환을 처리할 수 있어 많이 사용되는 방식이다.
라우팅은 하나 이상의 네트워크 안에서 데이터를 보낼 때 최적의 경로를 선택하는 과정으로 전화, 전자, 교통 등 여러 종류의 네트워크에서 사용된다. 라우팅의 경로 선택은 특수한 네트워크 하드웨어 라우터에 의해 이루어진다. 라우터는 로컬 네트워크 연결을 설정하는데에 사용되어 데이터가 목적지에 도달하도록 돕는다.
SPA방식이 널리 퍼지게 되면서 이러한 라우팅 개념이 빠질 수 없게 되었다. 라우팅으로 인해 사용자의 요청이 있을 때마다 서버가 아닌 URL 경로 이름에따라 콘텐츠가 동적으로 표현된다.
history.pushState
API를 활용하여 하나의 페이지에서 다른 페이지를 다시 로드하지 않고 URL을 탐색 할 수 있다. history API는 pushState
와 window.popstate
이벤트를 이용하여 새로운 데이터를 전달하기 위한 URL을 지정 할 수 있다. hitory는 사용자가 페이지에서 어디로 이동하였는지(뒤로가기, 앞으로가기의 popstate, URL이동 등)의 기록이라 할 수 있다.
window.addEventListener("popstate", )
history.pushState()
어제 저녁에 있었던 민태강사님의 라우팅 강의를 보고 history와 popstate를 새롭게 알게되었고 직접 구현해보는 라우팅 로직을 보면서 여러번의 복습이 필요할 것 같다라는 생각이 들어 복습차원에서 시작해보았다.
npm init vite@latest
npm install
<body>
<h1>sumin's study</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/download">Download</a></li>
</ul>
</nav>
<div id="app"></div>
<script type="module" src="main.js"></script>
</body>
HTML에 들어가는 기본적인 nav태그는 URL을 지정해주기 위한 a태그의 목록으로 구성되어있고 그 밑에는 각각의 목록이 눌러지면 라우팅을 이용한 화면이 렌더링 되기 위한 구역이 들어갈 div태그를 넣어두었다. div태그는 id가 app인 속성을 포함한다.
function init() {
const a = document.querySelector("a")
a.addEventListener("click", navPage)
}
function navPage(event) {
event.preventDefault() // 주소창이 새로고침 되는 것을 방지
const a = event.target.closest("a") // 모든 a 태그를 찾아서 타겟해줘
if (a && a.href) {
const path = a.getAttribute("href")
history.pushState(null, null, path)
route()
}
}
document.addEventListener("DOMContentLoaded", init)
돔이 렌더되면 init( )함수를 실행시키고 함수 내부에 a태그가 클릭되면 navPage( )함수가 실행되도록 작성하였다. navPage( )함수 내부는 a와 a의 값인 href 가 참이면 history.pushState
를 사용하여 href의 값, 즉 이동한 기록을 강제로 넣어주고 route( )함수를 실행시킨다. 기록이 저장되지 않으면 URL전환 시(popstate) 새로고침이 발생되어 이것을 방지한 방어코드라고 볼 수 있다. 앞으로 이동한 기록만 남기고 싶다면 history.forward()
, 뒤로가기 기록만 남기고 싶다면 history.back()
을 사용 할 수 있다. 하지만 SPA에서는 전체 페이지를 다시 로딩하지 않는데 back( ), forword( )를 호출하면 해당 페이지를 리로드하기때문에 잘 사용되지 않는다.
history.pushState( data, title, url )
- data : 상태 값을 나타내는 것으로 브라우저에서 앞/뒤로 갈때 넘겨줄 데이터
- title : 변경할 브라우저 제목
- URL : history에 담길 새로운 url, 브라우저 주소창에 입력되는 path
function route() {
const path = location.pathname;
const app = document.querySelector("#app");
switch (path) {
case "/":
app.innerHTML = `
<h1>Home</h1>
<div>메인 화면입니다.</div>
`
break
case "/about":
app.innerHTML = `
<h1>About</h1>
<div>무엇에 대해 알려드릴까요?</div>
`
break
case "/download":
app.innerHTML = `
<h1>Download</h1>
<div>무료로 다운로드 가능합니다.</div>
`
break
}
}
route( )함수는 이벤트 발생 시 동작되는 함수로 URL이 바뀔때마다 id가 app인 div태그에 해당 URL과 맞는 HTML 값을 넣어주도록 작성하였다.
export function download() {
return `
<h1>Download</h1>
<div>무료로 다운로드 가능합니다.</div>
`
}
route( )함수에 각각의 URL의 화면이 렌더되는 html을 다 작성해준다면 엄청 복잡하고 길어질 것이다. 그렇기 때문에 html에 넣는 download화면은 download.js파일에서 작성을 해주었고 route( )함수가 있는 main.js파일에서 해당 함수를 import하여 아래와 같이 작성해주었다.
case "/download":
app.innerHTML = download();
break;
이렇게 하면 라우팅기능을 담은 main.js만 깔끔하게 관리 할 수 있다.
import { download } from "./download"
// 화면이 렌더되면 실행
const app = () => {
init()
route()
}
// 핸들러 함수
function init() {
window.addEventListener("popstate", route)
document.body.addEventListener("click", navPage)
}
// 페이지 전환 기록 저장
function navPage(event) {
event.preventDefault()
const a = event.target.closest("a")
if (a && a.href) {
const path = a.getAttribute("href")
history.pushState(null, null, path)
route()
}
}
// 페이지 전환
function route() {
const path = location.pathname
const app = document.querySelector("#app")
switch (path) {
case "/":
app.innerHTML = `
<h1>Home</h1>
<div>메인 화면입니다.</div>
`
break
case "/about":
app.innerHTML = `
<h1>About</h1>
<div>무엇에 대해 알려드릴까요?</div>
`
break
case "/download":
app.innerHTML = download()
break
}
}
document.addEventListener("DOMContentLoaded", app)
몇가지 바뀐 점이 있다면 각각의 핸들러가 담긴 함수에 route( )를 실행시키고 있어 app( )함수를 만들어 해당 함수 내부에 돔이 렌더되면 핸들러가 담긴 init( )함수와 route( )함수가 실행되도록 해주었다.
init( )함수에서 popstate 이벤트 발생시 route( )가 실행되도록 추가로 넣어줗었고, a태그가 아닌 body태그에 핸들러를 적용하였는데 이유는 새롭게 추가된 a링크가 바디에서 이벤트 처리 되어 HTML내부에 추가하더라도 이벤트 처리를 해야하는 번거로움을 없애기 위해서이다.