바닐라 자바스크립트로 여러 페이지를 라우팅 해야 하는 규모의 프로젝트를 연습 삼아 만들어보고 있다. 해당 과정에서 SPA 애플리케이션의 라우팅 하기 위한 방법을 몰라 애를 많이 먹었다. 그 과정에서 알게 된 몇몇 내용들을 정리 차 남겨둔다.
라우팅과 SPA
규모가 어느 정도 있는 웹 서비스는 하나의 도메인에도 n개 이상의 페이지(화면 전환)가 존재한다. 그래서 어떠한 경로로 페이지에 접근했는지에 따라 클라이언트에 그려지는 화면이 달라지도록 처리해주는 게 필요하다.
예를 들어, 어떠한 도메인에 ./
라는 경로로 바로 들어오면 메인 페이지가 나오고, ./login
이라는 경로를 붙여서 들어오면 로그인 페이지를 보여주고, ./signup
이라는 경로를 붙여서 들어오면 회원가입 페이지가 보이도록 처리해 주는 식이다(패스 네임은 개발한 사람이 지정하기 나름이다). 또한, 애플리케이션을 사용하면서의 화면 전환을 기록하여 뒤로 가기-앞으로 가기 등의 페이지 전환도 가능해야 한다.
이렇게, 접근 경로에 따라 서로 다른 페이지를 보여주도록 처리해주는 걸 '라우팅(Routing)'이다. 영문 표현 Route에는 '경로를 정하다'라는 뜻이 있다. 뜻에 걸맞게 페이지에 들어온 패스에 따라 보여줘야 하는 화면의 경로를 정해주는 것이 라우팅이다.
가장 쉽게 라우팅을 처리하는 방법은 URL의 다른 패스로 접근한 경우마다 서버에서 그에 맞는 서로 다른 정적 파일을 내려주는 것이다. 그러면 URL을 통한 서버 요청, 서버에서 정적 파일인 HTML, CSS, JavaScript 파일 전달, 전달받은 파일들을 파싱 해서 클라이언트에서 그려준다는 웹의 메커니즘대로 라우팅을 (상대적으로) 수월하게 처리될 수 있다.
하지만, SPA는 화면 전환 시 반드시 서버에 새로운 정적 파일들을 요청하진 않고, 변화가 있는 부분의 데이터만 서버에서 받아와 부분적으로 렌더링한다. 그렇다 보니, SPA에서의 라우팅은 클라이언트에서 처리한다(서버에 정적 파일 요청을 다시 보내지 않는다). 클라이언트에서 나름의 방식으로 유저의 유입 경로에 따라 필요한 화면에 렌더링 될 수 있도록 처리해 주는 것이다.
SPA 개발에 사용되는 다양한 라이브러리/프레임워크(React.js, Vue.js 등)들은 라우팅을 처리하기 위한 별도의 도구들을 제공한다. 해당 도구를 잘 사용하면 수월하게 라우팅을 처리할 수 있다. 하지만, 바닐라 자바스크립트만으로 프로젝트를 할 땐 별도의 라우팅 라이브러리가 없기 때문에 직접 수동으로 라우팅을 구현해주어야 한다.
History API
바닐라 자바스크립트로 구현하는 SPA 애플리케이션에서 라우팅을 구현하는 하나의 방법은 History API를 사용하는 것이다. History API는 브라우저에서 제공해주는 기능이다. 브라우저의 세션 히스토리(특정 세션 조회 기간 동안 어떤 작업을 수행했는지의 기록)에 대한 접근을 제공하고, 그를 이용해 URL 패스 변경, URL 패스에 따른 페이지 라우팅 로직 처리가 가능하다.
History API는 브라우저에서 제공하는 history
객체를 통해 사용이 가능하다(Node.js 환경에선 사용이 불가능하다). 브라우저에 history
를 쳐보면 History
를 프로토타입으로 하는 객체가 확인된다. 그리고, history
객체가 갖는 프로퍼티들도 확인된다.
먼저 pushState
와 replaceState
라는 메서드가 보인다. 둘 다 History API에서 관리하는 세션 기록(방문한 URL 기록)을 관리하는 역할을 한다. 이름 그대로 pushState
는 세션 기록을 추가하고, replaceState
은 세션 기록을 변경한다.
pushState
와 replaceState
는 동일한 세 개의 인자를 갖는다. 확인을 위해 pushState
를 사용해보겠다.
history.pushState({page: 1}, null, 'page=1');
위와 같이 작성하면 현재 보고 있는 브라우저 세션의 URL 패스 뒷부분에 page=1
이라는 패스가 추가된다. 실제론 어떠한 화면의 변환 로직도 처리되지 않았지만 history.pushState
메서드를 통해 세션 정보를 추가해 줬기 때문에 페이지가 변환된 것으로 간주하고 URL을 바꿔주는 것이다.
예시를 통해 쉽게 알 수 있듯이, pushState
와 replaceState
의 세 번째 인자에는 추가할 패스 경로를 문자열 형태로 전달하면 된다. 그리고, 두 번째 전달한 위치엔 title이라는 역할을 하는 값을 전달하면 된다는데, 사실 이 값은 어떤 역할을 해주는지 정확하게 파악이 어렵다. 다만, 현재는 사파리를 제외한 모든 브라우저에서 두 번째 값으로 전달되는 title 값을 사용하지 않다고 하니, 그냥 빈 값을 전달해 주면 된다(MDN 문서에선 빈 문자열을 전달하는 걸 추천하고 있다).
그리고, 첫 번째 전달한 인자는 State를 나타낸다. history
객체의 속성으로 있던 state
값에 전달한 값이 저장된다. 참고로, 첫 번째 인자인 state
에는 직렬화(Serialize) 할 수 있는 모든 값이 올 수 있다.
새로운 세션 정보를 history
객체에 전달하면 state
의 값이 바뀌는 것으로 보아 해당 값은 history
객체가 현재 바라보고 있는 세션을 의미하는 것 같다.
사실 History API에는 제공된 더 다양한 기능들이 있다. 관련해선 아래에 실제 사용 사례를 통해 살펴보도록 하겠다.
History API 활용하기
먼저, 페이지 라우팅을 처리해줘야 하는 상황을 통해 History API가 해주는 여러 역할과 좀 더 상세한 활용 방법에 대해 살펴보자.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p id="text">현재 페이지: 0페이지</p>
<button id="go-forward--btn">go forward</button>
<button id="go-backward-btn">go backward</button>
<script>
const $text = document.getElementById("text");
const $goForwardBtn = document.getElementById("go-forward--btn");
const $goBackwardBtn = document.getElementById("go-backward-btn");
let idx = 0; // 화면 전환을 위한 표시하기 위한 인덱스
// innerText 변경 템플릿
const template = ({ idx }) => {
return `현재 페이지: ${idx}페이지`;
};
$goForwardBtn.addEventListener("click", (e) => {
idx += 1;
history.pushState({ page: idx }, null, `?page=${idx}`);
$text.innerText = template({ idx });
});
$goBackwardBtn.addEventListener("click", (e) => {
idx -= 1;
history.pushState({ page: idx }, null, `?page=${idx}`);
$text.innerText = template({ idx });
});
</script>
</body>
</html>
위의 예제대로 코드를 작성하고 버튼을 조작해 보면 화면에 표시되는 텍스트가 idx
값에 따라 변화한다. 그리고, 화면에 표시되는 URL 뒤에 패스도 변경된다.
History API를 통해 가장 간단한 라우팅을 구현해 봤다. 물론, 완벽하진 않다. 뒤로 가기 페이지를 눌렀을 때 URL의 패스는 변경되지만 화면에 표시되는 내용은 변경되지 않은 것이다(패스에 따라 보이는 화면이 달라지지 않았으니, 라우팅이 되지 않은 것이다).
보통 브라우저의 세션 탐색 기록(저장된 세션 접근 누적 기록)으로 인해 현재의 세션 기록이 변경될 때(뒤로 가기, 앞으로 가기 등) 브라우저는 popstate
이벤트를 발생시킨다. 하지만, history.popState
와 history.replaceState
는 popstate
이벤트를 감지하지 않는다. 만약에 뒤로 가기나 앞으로 가기 동작이 발생될 시 이를 감지하고 새로운 화면을 렌더 해주기 위해선 브라우저의 전역 객체인 window
에 이벤트 리스너를 부착해 popstate
이벤트를 감지하고, 이벤트 발생 시 화면을 다시 렌더링 해주면 된다. 아래 코드를 script
태그 부분에 추가해 보자.
<script>
...
let idx = 0; // 화면 전환을 위한 표시하기 위한 인덱스
let state = ""; // 페이지의 state를 저장하기 위한 변수
...
$goForwardBtn.addEventListener("click", (e) => {
idx += 1;
history.pushState({ page: idx }, null, `?page=${idx}`);
state = history.state.page; // 페이지 변할 때마다 state에 새로운 값 저장
$text.innerText = template({ idx });
});
$goBackwardBtn.addEventListener("click", (e) => {
idx -= 1;
history.pushState({ page: idx }, null, `?page=${idx}`);
state = history.state.page; // 페이지 변할 때마다 state에 새로운 값 저장
$text.innerText = template({ idx });
});
// popstate 바뀔 때마다 state 값 비교해서 앞으로 간건지 뒤로 간건지 파악하고 페이지 변경
window.addEventListener("popstate", (e) => {
idx -= state - history.state.page;
$text.innerText = template({ idx });
state = history.state.page;
});
</script>
이렇게 하면 브라우저의 뒤로 가기, 앞으로 가기 동작에 전부 대응이 가능하다. 조금 더 그럴듯한 라우팅이 된 셈이다.
하지만, 이 또한 완벽하진 않다. 해당 코드는 화면이 새로 고침 돼서 정적 파일을 다시 받아와 재랜더 되는 상황이나(세션 기록은 남아있기 때문에 URL 뒤의 패스는 그대로지만 화면은 초기 상태로 되돌아간다), 특정 경로로 직접 접근하는 경우(e.g. /page=3
으로 바로 접근하면 원하는 데이터가 렌더되지 않는다).
이는, 브라우저의 특성상 새로고침을 할 때 서버로 정적 파일(HTML 파일)을 새롭게 요청해서 받아온다는 특징, 그리고 URL 뒤 패스가 /page=3
으로 있더라도 사실은 index.html
이 화면을 그리고 있고, 그 안에 있는 자바스크립트로 페이지 변환을 구현하고 있는데, 페이지 변환 과정을 건너뛰었다는 상황 때문에 100% 완벽한 라우팅 구현은 한계가 있다.
이를 완벽하게 처리하기 위해선 어떠한 패스로 들어오더라도 반드시 index.html
파일이 반환될 수 있도록 서버에서 별도 처리를 해주는 게 필요할 수 있다. 아니면 SPA를 지원하는 별도 라이브러리-프레임워크의 도구를 사용해 라우팅을 구현하는 게 필요할 수 있다. 그것도 아니고, 바닐라 자바스크립트로 SPA 페이지 라우팅을 조금 더 다양한 상황을 처리해서 구현하고 싶다면 window.location.hash
값을 사용하는 방법이 있다(이후에 좀 더 공부해서 다시 포스팅해보겠다).
이외에도 History API는 go
, forward
, back
등 뒤로 가기와 앞으로 가기를 위한 다양한 기능을 추가적으로 제공한다. 관련된 좀 더 자세한 내용은 MDN 문서에서 확인 가능하다.
결론
실제 프로젝트에서 써보니 생 자바스크립트에서의 라우팅이 생각보다 쉽지 않은 것 같다(생각처럼 쉽지 않은 것일 수도 있다). 그래도, 라우팅이 왜 어려운지의 상황을 겪어보니 이후에 관련된 도구를 쓸 때 분명 도움이 될 것 같다. 너무 쉽게 도구를 사용하는 것에 길들여지기 전에 이렇게 다양한 어려움을 사서 겪어보면서 도구에 대한 이해(왜 이걸 써야 하는지)를 좀 더 탄탄하게 늘려가야겠다.
'Web' 카테고리의 다른 글
axios 인스턴스와 인터셉터 사용하기 (1) | 2024.12.08 |
---|