라우터를 직접 구현해 보고자 라우터를 스터디하다가 BFCache를 알게 되었습니다.
그래서 관련 내용들을 이번에 한번 정리하고 넘어가고자 하여 작성하게 되었습니다.
라우터(router)는 컴퓨터 네트워크 간에 데이터 패킷을 전송하는 네트워크 장치이다 - 위키백과
웹 애플리케이션에서 라우터란, URL 주소에 따라 사용자의 요청에 맞게 서버 리소스에 연결하는 역할을 말합니다.
즉, 웹 애플리케이션에서 사용자의 인터페이스와 상호 작용에 따라 다른 뷰나 컴포넌트를 보여주는 역할을 말합니다.
이전에는 URL의 해시를 사용해 라우팅을 구현했습니다.
예를 들어 http://example.com/#/user
와 같이 URL의 해시 부분을 변경하여 페이지 콘텐츠를 변경하는 방식으로 해시가 변경될 때 페이지는 새로고침되지 않고 자바스크립트를 사용하여 해시 변경을 감지하고 해당하는 동적 변경을 적용할 수 있었습니다.
해시 기반 라우팅은 모든 브라우저에서 호환되며 구현이 간단하다는 장점이 있지만 URL에 해시(#)가 붙어야 했기 때문에 URL이 클린하지 않고 SEO에 좋지 않다는 문제점이 있었습니다.
자세한 내용은 MDN의 History API 문서를 확인해주세요
대부분의 현대적인 SPA 프레임워크와 라이브러리는 HTML5 History API를 사용하여 브라우저의 세션 히스토리(session history)에 직접 접근하고 수정할 수 있는 메서드를 제공합니다.
History API는 페이지를 새로고침 하지 않고도 페이지의 내용을 동적으로 변경할 수 있고, 각 상태에 대한 적절한 URL을 제공하기 때문에 SPA 개발에서 매우 중요한 역할을 하며, 사용자가 애플리케이션 내에서 자연스럽게 내비게이션을 할 수 있어 사용자에게 일관된 경험을 제공해줍니다.
그리고 클라이언트 측에서 URL과 상태를 관리함으로써 서버에 대한 요청 수를 줄일 수 있기 때문에 대규모 트래픽이 있는 웹 애플리케이션에서 서버 리소스를 절약할 수 있는 장점이 있으며, 사용자가 브라우저의 내비게이션 버튼을 사용했을 때 애플리케이션의 이전 상태로 돌아갈 수 있게 해줌으로써 사용자 경험이 향상되며 SEO에 유리합니다.
import Home from "./Home.js";
import Posts from "./Posts.js";
import MyPage from "./MyPage.js";
import NotFound from "./NotFound.js";
import PostDetail from "./PostDetail.js";
type Router = {
path: string;
view: () => string;
result?: RegExpMatchArray | null;
};
const NOT_FOUND_ROUTE: Router = {
path: "/404",
view: NotFound,
};
const pathToRegex = (path: string) =>
new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");
// 현재 url의 주소를 히스토리 스택에 추가
const updateUrlRoute = (url: string) => {
history.pushState(null, "", url);
};
const resolveRoute = (routes: Router[]) => {
const path: string = window.location.pathname;
const matchedRoute: Router =
routes.find((route) => path.match(pathToRegex(route.path))) ??
NOT_FOUND_ROUTE;
renderView(matchedRoute.view());
};
const renderView = (view: string) => {
const appElement: HTMLElement | null = document.getElementById("app");
if (appElement) appElement.innerHTML = view;
};
const getRouter = () => {
const routes: Router[] = [
{ path: "/", view: Home },
{ path: "/posts", view: Posts },
{ path: "/posts/:id", view: PostDetail },
{ path: "/mypage", view: MyPage },
];
resolveRoute(routes);
};
const handleLinkClick = (event: MouseEvent) => {
if (
event.target instanceof HTMLAnchorElement &&
event.target.matches("[data-link]")
) {
event.preventDefault();
updateUrlRoute(event.target.href);
}
};
const setupEventListeners = () => {
const app = document.querySelector<HTMLElement>("#app");
const nav = document.querySelector<HTMLElement>(".nav");
if (app && nav) {
app.addEventListener("click", handleLinkClick);
nav.addEventListener("click", handleLinkClick);
}
window.addEventListener("popstate", getRouter);
};
// 페이지가 처음 로드되면 실행되는 함수, 초기 라우팅 설정
document.addEventListener("DOMContentLoaded", () => {
setupEventListeners();
getRouter();
});
BFCache란, 사용자가 브라우저의 뒤로가기 또는 앞으로가기 버튼을 사용할 때 이전에 방문했던 페이지를 빠르게 로드하기 위해 페이지의 전체 상태를 메모리에 저장하는 브라우저의 기능입니다.
사용자가 다른 곳으로 이동하는 순간 현재 페이지의 전체 스냅숏(자바스크립트의 힙 포함)을 저장하는 메모리 내부 캐시이기 때문에, 사용자가 다시 이전 페이지로 돌아오려고 할 때 브라우저는 메모리에 저장해둔 전체 페이지를 사용해서 페이지를 빠르고 쉽게 복원해주기 때문에 서버 요청 없이 즉시 페이지를 렌더링 할 수 있습니다.
BFCache는 메모리에 있는 전체 페이지의 스냅숏(자바스크립트 힙 포함)을 저장하며, BFCache의 목적은 사용자가 브라우저의 뒤로가기 및 앞으로가기 버튼을 사용할 때 페이지 로딩 시간을 줄이기 위해 사용합니다.
HTTP 캐시는 이전에 보냈던 요청의 응답만 저장하며, HTTP 캐시의 목적은 웹 리소스(HTML, CSS, JavaScript, 이미지 등)를 브라우저나 서버에 저장하여 동일한 리소스에 대한 재요청시 빠르게 제공함으로 네트워크 사용량을 줄이고 페이지 로딩 시간을 개선하는데 목적이 있습니다.
Cache-Control
, Expires
, Last-Modified
, ETag
등의 헤더가 사용됩니다.즉, BFCache는 뒤로/앞으로가기 에서만 작동하며, HTTP 캐시는 일반적인 페이지 방문과 리소스 요청에서 항상 작동할 수 있으며, BFCache는 페이지 전체 상태의 빠른 복원을 목표로 하고 HTTP 캐시는 개별 웹 리소스를 캐싱하여 네트워크 요청을 줄이고 일반적인 페이지 로딩을 빠르게 하는데 초점을 둔다는 차이점이 있습니다.
현재 페이지에서 setTimeout
같은 timer
함수가 아직 실행 완료되기 이전에 사용자가 뒤로/앞으로가기 버튼을 클릭해서 페이지를 벗어난다면 브라우저는 보류중인 timer
, promise
등의 실행을 일시 중지하고(기본적으로 자바스크립트 태스크 큐에 있는 모든 작업들) 페이지가 BFCache로부터 복원이 되었을 때 다시 실행하게 됩니다.
굉장히 좋아 보이지만 때로는 굉장히 복잡한 결과나 이해할 수 없는 행동을 만들 수 있는데, 만약 브라우저가 IndexedDB transaction
의 일환인 작업을 중지한다면 다른 탭에서도 접근할 수 있는 IndexedDB의 특성상 다른 탭에서 페이지를 열었을 때 영향을 미칠 수 있기 때문에, 브라우저는 IndexedDB 트랜잭션 또는 다른 페이지에 영향을 줄 수 있는 API 호출 중에는 페이지를 캐싱하려고 하지 않습니다.
BFCache는 브라우저가 자동으로 하는 최적화 방법이기 때문에 이 동작을 잘 이해하면 페이지를 최적화하거나 성능을 측정하고 조정하는데 도움을 얻을 수 있습니다.
event.persisted
속성이 true
일 경우 페이지가 BFCache에서 복원된 것이고 false일 경우 새로 로드되거나 새로고침된 상태이기 때문에 꼭 pagehide가 되었다고 볼 수 없습니다.window.addEventListener('pageshow', function(event) {
if (event.persisted) {
console.log('이 페이지는 BFCache에서 복원되었습니다.');
} else {
console.log('이 페이지는 새로 로드되거나 서버에서 새로 고침되었습니다.');
}
});
event.persisted
속성이 true
일 경우 페이지가 BFCache에 저장된 상태이지만, true라고 해서 페이지가 반드시 캐시되는 것은 아니며, 브라우저가 캐시를 시도하지만 캐시할 수 없는 다른 요인이 있을 수 있습니다.window.addEventListener('pagehide', function(event) {
if (event.persisted) {
console.log('이 페이지는 BFCache에 저장될 수 있습니다.');
} else {
console.log('이 페이지는 BFCache에 저장되지 않습니다.');
}
});
또는 freeze , resume 도 있습니다.
BFCache의 기능은 전통적인 웹 페이지 탐색에 초점을 맞추기 때문에 단일 패이지 앱(SPA)내의 “소프트 탐색”에서는 기본적으로 작동하지 않습니다.
SPA에서의 페이지 탐색은 보통 페이지를 새로 로드하지 않고, JavaScript를 사용하여 동적으로 컨텐츠를 교체하는 방식으로 이루어집니다.(React, Angular, Vue.js등)
이러한 “소프트 탐색”은 브라우저의 기본 탐색 메커니즘을 우회하기 때문에 BFCache가 작동하지 않습니다.
즉, 사용자가 SPA 내부에서 다른 섹션으로 이동할 때는 페이지 전체를 새로 로드하지 않기 때문에 BFCache가 필요하지 않기 때문에 사용자가 다른 페이지로 완전히 이동하거나 브라우저를 닫았다 돌아오는 경우가 아니면, 페이지 상태가 자동으로 저장되거나 복원될 일이 없습니다.
SPA의 경우 사용자가 브라우저를 통해 SPA를 떠났다가 뒤로가기나 앞으로가기 버튼으로 돌아올 때 BFCache의 이점을 활용할 수 있습니다. 예를들어 사용자가 SPA를 사용 중에 다른 웹사이트로 이동했다가 뒤로가기로 SPA로 돌아오면 브라우저는 BFCache를 사용하여 이전에 보았던 페이지 상태를 빠르게 복원할 수 있으며 이때, SPA는 처음부터 완전히 다시 로드되지 않고, 이전 세션의 상태(스크롤 위치, 사용자 입력, 페이지 상태 등)를 빠르게 복원할 수 있습니다.
pageshow
이벤트를 활용하여 페이지가 BFCache에서 로드되었는지 확인하고 필요한 콘텐츠를 다시 로드할 수 있게 합니다.일반적으로 SPA로 만든 웹 사이트는 BFCache가 활성화 되지 않는게 맞지만, Next.js를 사용하는 일부 웹 사이트에서 BFCache가 활성화되는 현상이 있는데 벨로그의 경우가 이런 경우입니다.
(동영상이 업로드 되지 않아 스크린샷을 첨부합니다. 개발자 툴에서 확인할 수 있습니다)
이 부분이 궁금해서 GPT로 확인했을 경우 Next.js는 서버사이드 렌더링(SSR)과 정적 사이트 생성(SSG)을 지원하기 때문에 라우팅 시에도 페이지가 빠르게 로드될 수 있도록 도와 준다고 합니다. 브라우저는 SSR과 SSG로 생성된 페이지를 더 쉽게 BFCache에 저장할 수 있다고 했고, wappalyzer 를 사용하여 Next.js를 사용하는 다른 웹사이트들의 경우 BFCache가 일반적으로 활성화되지 않는 경우가 많았던 것을 보면 CSR일 수 있겠다는 생각을 했습니다.
BFCache의 더 자세한 내용은 여기서 확인 가능합니다.
처음에는 간단히 라우터를 직접 구현하는 과정을 정리하고자 시작했었으나 라우터에 관해 학습하면서 BFCache를 공부하게 되면서 관련된 다양한 지식을 학습할 수 있었습니다.
앞으로도 단순히 어떤 것에 대해 학습을 할 때 좀 더 다양한 관점으로 바라보고 스터디할 수 있도록 노력해야겠습니다.
잘 보고 갑니다