배경
⚓️
개발 •  • 읽는데 16분 소요

History API는 가라! 이제는 Navigation API의 시대가 온다

클라이언트 사이드 라우팅의 표준화를 위해 새롭게 제안된 Navigation API에 대해 알아봅니다.

#JavaScript
#Translation


⚠️ 현재 Navigation API는 실험적(Experimental) 기능 입니다. 실제 프로덕션 환경에서 사용하는 것을 권장하지 않습니다.


이 글은 Chrome Developers의 아티클 『Modern client-side routing: the Navigation API』를 번역 및 의역한 것임을 밝힙니다.

SPA(Single Page Application)를 지탱하는 핵심 요소 중 하나는 바로 클라이언트 사이드 라우팅(Client Side Routing) 입니다.

SPA는 클라이언트 사이드 라우팅을 통해, 사용자가 웹사이트 내에서 다른 주소로 방문하려 할 때 서버에게 새로운 HTML 파일을 달라고 요청하는 것을 막고 클라이언트 측에서 JavaScript를 이용해 다른 주소를 방문한 것처럼 만들게 할 수 있습니다. 이 때 상호작용에 필요한 컨텐츠만을 동적으로 불러오기 때문에 사용자는 부드러운 화면 전환을 경험할 수 있습니다.

현재까지 SPA의 라우팅 기능은 History API를 이용해 구현되었습니다. 물론 일부 사이트의 경우에는 아직까지 주소 끝에 해시(#, 또는 앵커)를 붙이는 방식으로 클라이언트 사이드 라우팅을 구현하기도 하지만요.

하지만 History API는 SPA라는 개념이 탄생하기도 전에 나온 오래된 API고, 현재의 웹은 라우팅에 대한 완전히 새로운 접근 방식을 필요로 하고 있습니다. 이러한 흐름에 맞추어 Navigation API가 등장했습니다.

사실 History API의 부족한 부분을 보완하기 위한 시도가 없었던 것은 아닙니다. 페이지 전환 시 스크롤 상태를 저장하고 복구(Scroll Restoration)하는 기능이 History API에 추가된 적이 있죠. 하지만 Navigation API는 History API의 부족한 부분을 보완하는 것이 아니라, 완전히 새롭게 제안된 API입니다. History API에 어떤 단점이 있었길래 Navigation API가 새롭게 제안됐는지 궁금하시지 않은가요?

그래서 이번 포스트에서는 Navigation API를 대략적으로 한 번 훑어보는 시간을 가져보고자 합니다. 보다 기술적인 명세가 궁금하신 분들은 WICG의 Navigation API 명세 초안을 살펴보세요.

Navigation API는 크롬 102 버전부터 공식으로 릴리스 됐습니다. 데모를 확인해보세요. 다만 현재 Navigation API의 일부 사양 변경이 예정되어 있기 때문에, 문서에 나온 내용과 실제 동작하는 코드가 상이할 수 있습니다.

History API의 문제점

Navigation API를 소개하기 전, History API에 대해 간단히 알아보도록 합시다. History API는 HTML5부터 도입된 API로, JavaScript에서 웹 브라우저의 주소 탐색 내역을 조회 및 수정할 수 있는 기능을 제공합니다.

history.back(); // 이전 페이지로 돌아가기
history.forward(); // 다음 페이지로 넘어가기
history.go(-2); // 2번째 전 페이지로 돌아가기
history.go(0); // 새로고침

history.pushState({ foo: 'bar' }, '', '/url/to/page'); // 새 세션 추가
history.replaceState({ foo: 'bar' }, '', '/url/to/page'); // 가장 최근의 세션을 덮어쓰기

클라이언트 측의 주소 탐색 내역을 조작할 수 있었기에 SPA가 등장할 수 있었지만… 이 기능을 DOM과 연결하는 것은 꽤나 번거로운 일입니다. 만약 여러분이 History API를 사용하여 SPA에 대한 라우팅 코드를 완전히 바닥부터 작성해본 적이 있다면, 아마 다음과 같이 코드를 작성했을 겁니다.

// 현재 보고 있는 문서 내 모든 링크를 찾아서...
const links = [...document.querySelectorAll('a[href]')];

// 링크 클릭 시 새로운 페이지를 서버에서 받아오게 하지 않도록 이벤트 리스너를 추가하자.
links.forEach((link) => link.addEventListener('click', updatePage));

function updatePage(event) {
  // 브라우저의 기본 동작 멈춰! ✋
  event.preventDefault();

  // 주소 탐색 히스토리에 새 세션을 추가하자.
  window.history.pushState(null, '', event.target.href);

  // 대충 새 페이지에 필요한 정보를 불러오고 렌더하는 로직...
}

이것은 잘 동작하지만 완벽한 방법은 아닙니다. 링크는 페이지 내에서 어떤 방식과 형태로 등장할지 모르며, 링크 클릭 외에도 폼 제출, 이미지 맵(Image map)과 같은 방법으로도 주소 탐색을 할 수도 있기 때문입니다. 이러한 예외 상황들을 일일이 처리할 수도 있지만, 이것들을 보다 간단하게 해결하기 위해 Navigation API가 제안되었습니다.

History API만으로는 이러한 문제를 해결하기 어렵습니다. 왜냐하면 History API가 제공하는 기능은 브라우저의 뒤로 가기와 앞으로 가기, 새 주소 탐색 내역을 추가하거나 대체하는 것 뿐이기 때문입니다. 위에서 설명한 것처럼 여러분이 어떤 페이지 내에서 발생하는 모든 주소 탐색을 처리하기 위한 목적으로는 적합한 용도가 아니죠. 흠, 뭐 페이지 내에서 발생하는 모든 클릭 이벤트에 대해 이벤트 리스너를 걸어두면 모르겠지만요. 😅

원문에는 소개되지 않았지만 사실 History API에는 이 외에도 몇 가지 문제가 더 있는데, 그 중에서도 최상위 프레임과 iframe이 히스토리를 공유하는 문제에 대해 소개해드리고자 합니다.

iframe iframe 내 히스토리 변경 내역이 최상위 프레임의 히스토리를 오염시킨다

위 사진을 살펴보면, 두 개의 iframe을 포함하고 있는 SPA가 보입니다. 여기에서 iframe 내부에 포함된 링크를 한 번씩 클릭한 후, 브라우저의 뒤로 가기 버튼을 누르면 어떤 동작이 일어날까요?

우리는 최상위 프레임의 뒤로 가기를 기대하지만, History API는 모든 변경 내역을 최상위 프레임에서 선형적(linear)으로 관리하기 때문에 실제로는 iframe의 주소 탐색이 이루어진 역순으로 뒤로 가기가 실행됩니다.

여기서 문제가 생기죠. iframe을 포함하고 있는 어떤 SPA가 있는데, iframe 내에서는 몇 번의 링크 클릭이 일어날 지는 예측할 수 없는 상황을 가정해봅시다. 만약 최상위 프레임, 즉 SPA에서 뒤로 가기를 실행하고 싶다면 history.go()의 인자로 어떤 숫자 값을 넣어야 할까요? 전혀 알 수 없습니다.

그렇다면 iframe 내의 주소가 바뀔 때마다 이벤트를 발생시킨 후 최상위 프레임에서 그 횟수를 세면 될까요? 그러다가 사용자가 뒤로 가기를 누르는 경우에는 또 어떡하죠?

이와 같이, History API를 쓰는 페이지에서 iframe은 부비 트랩처럼 동작합니다. 최상위 프레임에서의 주소 탐색 내역을 오염시키고 API가 올바르게 동작한다는 신뢰성을 잃게 만드니까요. 이는 개발자로 하여금 혼란을 불러일으킵니다. Navigation API는 이러한 History API의 복잡하고 혼란스러운 설계를 개선하고자 하는 목적도 포함되어 있습니다.

좋습니다, History API의 한계에 대한 설명은 이 정도면 충분한 것 같네요. 이제 본격적인 Navigation API에 대한 설명으로 넘어가봅시다.

우선 Navigation API의 기본 예제부터 차근차근 살펴봅시다.

Navigation API를 사용하기 위해서는 "navigate" 이벤트를 전역 navigation 객체에서 수신하게 만들면 됩니다. 이 이벤트 리스너는 기본적으로 주소 탐색이 발생하는 모든 경우의 이벤트중앙 집중식(Centralized) 으로 수신합니다. 즉 링크 클릭, 폼 제출, 뒤로 가기, 앞으로 가기 등 사용자의 행동으로 주소 탐색이 발생하는 경우뿐만 아니라 코드를 이용한 프로그래밍 방식의 주소 탐색 이벤트까지 수신할 수 있습니다.

"navigate" 이벤트 리스너는 NavigationEvent 타입의 이벤트를 수신하는데, 여기에는 이동하고자 하는 목적지 URL 등의 정보가 담겨 있습니다. 그리고 이 이벤트 리스너는 전역적이기 때문에 한 곳에서 코드 관리가 가능하죠.

// 1. navigation은 전역 객체이므로 하나의 콜백 함수에서 주소 탐색 이벤트를 전역적으로 관리합니다.
navigation.addEventListener('navigate', (navigateEvent) => {
  // 2. navigationEvent에는 다양한 속성이 있습니다.
  console.log(navigateEvent.navigationType); // "reload", "push", "replace", "traverse"

  // 3. navigationEvent.destination 객체를 조회해 도착 주소에 대한 정보를 확인할 수 있습니다.
  switch (navigateEvent.destination.url) {
    case 'https://example.com/':
      navigateEvent.transitionWhile(loadIndexPage());
      break;
    case 'https://example.com/cats':
      navigateEvent.transitionWhile(loadCatsPage());
      break;
  }
});

Navigation API는 브라우저의 기본 주소 탐색 동작을 오버라이드 할 수 있는 기능을 제공합니다. SPA의 개념으로 접근해보자면, 사용자를 동일한 페이지에 위치시키면서 사이트 콘텐츠를 새로 불러오거나 변경할 수 있는 기능을 API로 제공한다는 것을 의미하죠.

크게 두 가지 방법을 통해 기본적인 주소 탐색 동작을 가로챌 수 있습니다.

  • transitionWhile(): 주소 탐색 과정을 별도 코드로 처리
  • preventDefault(): 주소 탐색을 완전히 취소

예제 코드에서는 이벤트가 발생할 때 transitionWhile()을 호출하는데, 인자로는 프로미스를 반환하는 비동기 함수를 받습니다. 이 메서드를 호출함으로서, 브라우저는 해당 주소로 새 페이지를 불러오는 요청을 보내지 않고, 여러분이 직접 작성한 코드를 바탕으로 페이지가 구성된다는 것을 알게 됩니다. 이 행동은 navigation.transition 이라는 객체를 만드는데, 다른 코드에서 이를 참조하여 현재 페이지 전환 진행 과정에 대한 정보를 판단할 수 있습니다.

일반적으로는 transitionWhile() 이나 preventDefault() 를 호출해 브라우저의 주소 탐색을 막을 수 있지만, 그렇지 않은 경우도 있습니다. 교차 출처(Cross Origin)의 주소 탐색, 즉 현재 도메인을 떠나려는 경우에는 transitionWhile() 을 호출할 수 없습니다.

또한 사용자가 브라우저에서 뒤로 가기 또는 앞으로 가기 버튼을 누른 경우 preventDefault() 를 통해 탐색을 취소할 수 없습니다. 이는 사용자가 특정 웹사이트를 떠나지 못하게 만드는 용도로 악용될 수 있기 때문입니다(여기에 대한 토론이 현재 진행 중입니다).

하지만 탐색 자체를 중지하거나 차단할 수 없더라도 "navigation" 이벤트는 계속 발생합니다. 해당 이벤트에는 유용한 정보가 담겨 있기 때문에, 사용자가 사이트를 떠나는 경우의 로그를 수집하는 용도 등으로 활용할 수도 있죠.

새 주소 탐색

"navigate" 이벤트 리스너 내에서 transitionWhile() 을 호출하면, 우리는 브라우저에게 새 페이지를 개발자가 직접 제공할 것이라고 알릴 수 있습니다. 이것은 시간이 걸릴 수 있는 작업이기 때문에, transitionWhile() 의 인자로 넘긴 프로미스를 통해 이 작업을 비동기로 수행하라고 브라우저에게 알려줄 수 있죠.

따라서 Navigation API는 SPA의 라우팅 개념을 브라우저가 이해할 수 있게 해 준다는 점에서 의의가 있습니다. 또한 접근성을 비롯해서 여러 가지 잠재적인 이점이 있는데, 바로 브라우저 단에서 주소 탐색의 시작과 종료, 그리고 잠재적인 실패를 인터페이스로 표시할 수 있기 때문입니다.

주소 탐색의 성공과 실패

"navigation" 이벤트가 완료되면 탐색하려 했던 주소의 URL이 실제로 적용됩니다. 비동기 작업을 인자로 받는 transitionWhile() 을 호출해도 즉시 주소가 변경됩니다.

이는 navigation.currentEntry, location.href 등의 속성이 즉시 업데이트됨을 의미합니다. 이는 새 데이터나 리소스를 로드할 때 URL 확인과 같은 것에 영향을 줍니다. 이것으로 인해 웹 페이지의 URL과 화면의 콘텐츠가 일치하지 않는 경우가 생길 수도 있으니 조심해야 합니다. 여기에 대한 토론이 현재 진행 중입니다.

transitionWhile() 의 인자로 프로미스를 전달하면 프로미스는 이행(resolve)되거나 거부(reject)됩니다. 프로미스가 이행됐을 때에는 "navigatesuccess" 이벤트(Event)가 발생하며, 거부되면 "navigateerror" 에러 이벤트(ErrorEvent)가 발생합니다. transitionWhile() 을 이벤트 리스너 내에서 호출하지 않은 경우에는 이행된 것으로 여깁니다.

각 이벤트를 처리하는 코드를 navigation 에 이벤트 리스너로 작성하여 성공과 실패에 대한 처리 코드를 작성할 수 있습니다. 다음 예제 코드와 같이 진행 완료 시 로딩 이펙트를 숨기거나, 실패 시 오류 메시지를 표시할 수 있습니다.

navigation.addEventListener('navigatesuccess', (event) => {
  // 성공했다면 로딩 이펙트를 숨기자
  loadingIndicator.hidden = true;
});

navigation.addEventListener('navigateerror', (event) => {
  // 실패했을 때에도 로딩 이펙트는 숨기자
  loadingIndicator.hidden = true;

  // 실패에 대한 메시지 띄우기
  showMessage(`Failed to load page: ${event.message}`);
});

에러 이벤트를 수신하는 "navigateerror" 이벤트 리스너는 새 주소 탐색이 실패하면 항상 실행된다는 점을 이용해, 네트워크를 사용할 수 없는 경우에 대한 예외 처리도 간편하게 구현 가능합니다.

중단 신호(Abort Signal)

transitionWhile() 을 이용해 새 페이지가 준비되는 동안에도 다른 비동기 작업을 실행하는 것이 가능하기 때문에, 특정 URL이나 상태를 로드하는 코드의 실행이 중단되어야 하는 경우가 생깁니다. 일반적으로 사용자가 어떤 링크를 클릭한 후 페이지가 전환되기 전에 또 다른 링크를 클릭하는 경우에 주로 발생합니다.

이러한 가능성을 처리하기 위해 NavigationEvent 에는 AbortSignal 인터페이스signal 이라는 이름으로 포함되어 있습니다. AbortSignal진행 중인 작업을 중지하기 위한 이벤트 를 발생시키는 객체를 제공하는데, 이를 이용해 중단 가능한 패치(Abortable fetch)를 한 번 구현해봅시다.

fetch() 의 두 번째 인자에 AbortSignal 을 전달함으로서 주소 탐색이 취소되는 경우 진행 중인 네트워크 요청을 취소하게 만들 수 있습니다. 이를 잘 활용한다면 사용자의 데이터(bandwidth)를 절약하게 만들 수 있죠.

또한 fetch()에 의해 반환된 프로미스는 거부(reject)되기 때문에, 현재 이미 취소된 페이지 탐색 결과를 화면에 표시하도록 DOM을 업데이트하는 코드가 실행되는 것을 방지합니다.

아래 코드를 살펴봅시다. "navigate" 이벤트 리스너에서 fetch() 를 호출해 고양이 사진을 로드하도록 코드를 작성한 상태입니다. fetch() 의 두 번째 인자 속성으로 navigateEvent.signal 을 전달함으로써, fetch()가 완료되기 전에 사용자가 다른 페이지를 로드하게 되면 기존에 실행되었던 fetch() 를 중단하게 만들 수 있습니다.

navigation.addEventListener('navigate', (navigateEvent) => {
  if (isCatsUrl(navigateEvent.destination.url)) {
    const processNavigation = async () => {
      const request = await fetch('/cat-memes.json', {
        signal: navigateEvent.signal,
      });
      const json = await request.json();
      // TODO: 고양이 사진을 화면에 표시하기
    };
    navigateEvent.transitionWhile(processNavigation());
  } else {
    // 다른 페이지에 대한 처리
  }
});

주소 탐색 내역의 각 항목(Navigation Entry)에 접근하기

엔트리 쉽게 이야기하자면 방문 기록 하나하나를 나타내는 정보가 엔트리(항목)라고 보면 된다

navigation.currentEntry 는 사용자가 현재 어떤 주소에 머무르고 있는지를 설명하는 객체로, 현재 방문한 주소 탐색 항목(entry)에 대한 접근을 제공합니다. 각 객체는 NavigationHistoryEntry 인터페이스를 가지는데, 여기에는 현재 URL, 각 주소를 방문한 기록을 식별하는 데 사용할 수 있는 메타데이터, 및 개발자를 위한 상태(state)값이 들어있습니다.

주소 탐색 항목은 브라우저의 탐색 내역을 담고 있는 값이기 때문에, Navigation API를 명시적으로 사용하지 않는다고 해도 주소 탐색 항목 자체는 항상 존재합니다. 이 값은 Navigation API로만 수정할 수 있는 것은 아니고, 호환성 유지를 위해 History API의 history.pushState()history.replaceState() 를 호출해도 값이 수정됩니다.

각 주소 탐색 항목의 메타데이터에는 키(key)가 있는데, 키는 각 항목과 방문 순서를 식별하는 고유한 문자열 속성입니다. 키는 한 번 생성된 이상 현재 항목의 URL이나 상태가 변경되더라도 동일하게 유지됩니다. 하지만 사용자가 뒤로 가기 버튼을 누른 다음 동일한 페이지 링크를 다시 클릭해 열게 된다면, 새로운 항목이 새로운 방문 순서를 생성하는 것이므로 키가 변경됩니다.

currentEntry 현재 주소 탐색 내역의 키 값이 UUID로 들어가 있는 걸 확인 가능하다

키는 꽤나 유용한 옵션인데, Navigation API를 이용하면 일치하는 키가 있는 탐색 내역 항목으로 사용자를 직접 이동시킬 수 있기 때문입니다. 특정 페이지를 쉽게 이동하기 위해 이 값을 변수로 저장해 사용할 수도 있습니다.

// JavaScript가 처음 실행 될 때, 처음으로 로드된 페이지의 키를 저장해둬서 언제든 첫 페이지로 돌아갈 수 있게 하자
const { key } = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);

// 다른 페이지로 이동해도 위 버튼은 계속 동작한다
await navigation.navigate('/another_url').finished;

상태(State)

Navigation API에는 상태(State)라는 개념이 있는데, 각 주소 탐색 항목에 영구적으로 저장되지만 사용자에게 직접적으로 표시되지는 않는 정보입니다. 개발자가 주소 탐색을 위한 정보를 추가로 저장할 수 있는 공간이죠. 사실 History API의 history.state 와 거의 동일한 작업을 하지만 좀 더 개선된 버전입니다.

Navigation API에서 현재 항목(또는 모든 항목)의 .getState() 메서드를 호출하여 상태의 복사본을 조회할 수 있습니다.

console.log(navigation.currentEntry.getState());
// undefined

기본적으로 이것은 undefined 값을 가지는데요, 다음 코드를 호출하여 현재 NavigationHistoryEntry 의 상태를 동기적으로 설정할 수 있습니다.

navigation.updateCurrentEntry({ state: something });

console.log(navigation.currentEntry.getState());
// something

Navigation API에서 .getState() 에서 반환된 상태는 이전에 설정된 상태의 복사본이기 때문에, 값을 수정한다고 해도 저장된 상태가 변경되지는 않습니다. 다음의 코드를 보면 아마 이해되실 거에요.

// count를 1로 설정
navigation.updateCurrentEntry({ state: { count: 1 } });

// state 객체를 변수로 저장해 그 값을 2로 수정
const state = navigation.currentEntry.getState();
state.count = 2;

// 위 코드는 복사본을 수정한 것이기 때문에 원본 상태값은 여전히 1
console.info(navigation.currentEntry.getState().count);

모든 주소 탐색 내역에 접근하기(Access all entries)

key 주소 탐색 내역을 코드로 확인 가능

Navigation API는 현재 항목에 대한 접근 뿐만 아니라, 전체 주소 탐색 내역에 대한 접근도 제공합니다. 모든 항목의 스냅샷 배열을 반환하는 navigation.entries() 호출을 통해, 사이트를 사용하는 동안 사용자가 탐색한 전체 항목 목록에 접근할 수 있습니다.

이것은 사용자가 특정 페이지에 접근한 방법에 따라 다른 UI를 표시하거나, 이전 URL 또는 이전 상태값을 다시 확인하는 과정이 필요할 때 사용할 수 있습니다. 기존의 History API만으로는 불가능한 방법이었죠.

한편으로는 각 항목이 브라우저의 탐색 내역에서 삭제될 때 발생하는 폐기(dispose) 이벤트를 수신할 수도 있습니다. 가령 뒤로 가기 버튼을 10번 누른 다음, 새 페이지로 접근하면 기존 10개의 방문 기록 항목이 삭제되는데 이러한 경우에 대해 별도 로직을 구현 가능하죠.

"navigate" 이벤트가 발생하는 경우를 API 사양에서 직접 확인할 수 있습니다. 가장 일반적인 탐색의 경우는 사용자가 하이퍼링크를 클릭하는 것이겠죠. 하지만 눈여겨봐야 하는 좀 더 복잡한 탐색 유형 두 가지가 있습니다.

프로그래밍 방식의 탐색

첫 번째는 프로그래밍 방식 탐색으로, 주소 탐색이 클라이언트 측 코드 내부의 메서드 호출에 의해 발생하는 경우입니다. 일반적으로 navigation.navigate()를 호출하여 탐색할 수 있습니다.

const { committed, finished } = navigation.navigate('/another_page');

.navigate() 메소드를 호출하면 "navigate" 리스너에 등록된 중앙 집중식 이벤트 리스너에 의해 동기적으로 처리됩니다. 이 메서드를 이용하면 location.assign() 와 같은 구식 메서드와 History API의 pushState()replaceState() 메서드를 모두 통합할 수 있습니다.

URL을 변경하기 위한 기존 API들을 호출해도 "navigate" 이벤트가 발생합니다. 즉 기존 API는 그대로 유지되며, 시간이 지남에 따라 .navigate() 로 자연스럽게 대체될 것이라고 믿습니다.

.navigate() 메서드는 두 개의 프로미스 인스턴스를 포함하는 객체를 반환합니다. 여기서 committed은 표시된 URL이 변경되고 새 NavigationHistoryEntry 이 내역에 추가되어 사용 가능함을 나타내는 프로미스이며, finishedtransitionWhile() 에 전달된 모든 프로미스가 이행 또는 거부었는지를 나타내는 프로미스입니다. 이렇게 하면 .navigate() 를 호출한 코드에서 주소 탐색이 성공 또는 완료, 실패 되었는지를 확인할 수 있습니다.

또한 .navigate() 메서드의 두 번째 인자로 추가 설정이 가능한 옵션 객체를 전달할 수 있습니다.

const { committed, finished } = navigation.navigate('/another_page', {
  state: { ... }, // `NavigationHistoryEntry`의 `.getState()` 메서드를 통해 사용 가능한 상태값
  history: "auto", // 현재 항목을 내역에 어떻게 저장할 것인지, "push" 또는 "replace"
  info: { ... } // `NavigationEvent.info`를 통해 조회 가능한 추가 정보 전달
})

Navigation API에는 navigate() 외에도 traverseTo(), back(), forward(), reload() 등의 추가 메소드가 존재하며, 이들 역시 "navigate" 이벤트를 발생시키죠.

폼 제출

두 번째는 HTML <form> 태그를 이용한 폼 제출입니다. 일반적으로 폼 제출은 HTTP의 POST를 이용하는데요, Navigation API는 이를 똑똑하게 가로챌 수 있습니다. 폼에는 추가 페이로드가 포함되어 있지만, 주소 탐색은 여전히 ​​"navigate" 이벤트 리스너에 의해 중앙에서 처리됩니다.

폼 제출은 NavigateEvent에서 formData 속성이 있는지를 찾아 구별할 수 있습니다. 다음은 fetch() 를 통해 현재 페이지에 남아 있는 모든 양식 제출을 간단히 바꾸는 예입니다.

navigation.addEventListener('navigate', (navigateEvent) => {
  // formData 속성이 있으면 폼 제출임을 확인할 수 있음
  // canTransition은 동일 출처(Origin) 확인 및 가로채기가 가능함을 나타내는 플래그
  if (navigateEvent.formData && navigateEvent.canTransition) {
    const submitToServer = async () => {
      await fetch(navigateEvent.destination.url, {
        method: 'POST',
        body: navigateEvent.formData,
      });

      // 폼 제출 완료를 나타내기 위해 `{ history: 'replace' }` 를 설정할 수 있겠네요
      navigation.navigate('https://example.com/submit-success', {
        history: 'replace',
      });
    };
    navigateEvent.transitionWhile(submitToServer());
  }
});

다른 특징

이렇게 해서 간단히 Navigation API의 사용법에 대해 알아봤는데요, 본문에서 언급하지는 않았지만 추가적으로 이야기해볼만한 내용 세 가지에 대해 소개해보고자 합니다.

우선은 Navigation API는 최초 페이지 로드 시에는 동작하지 않는다 는 것입니다. "navigate" 이벤트 리스너의 중앙 집중식 특성에도 불구하고, 현재 Navigation API 사양은 페이지의 첫 번째 로드에서 "navigate" 이벤트를 발생시키지 않는 것으로 되어 있습니다. 최초 상태를 서버에서 초기화하는 서버 사이드 렌더링(SSR)의 경우에는 문제가 없지만, 온전히 클라이언트 측 코드만을 활용해 페이지를 생성하는 사이트는 최초 상태 초기화를 위해 추가 기능을 구현해야 할 수도 있습니다.

두 번째는 Navigation API는 단일 프레임 내에서만 작동한다는 것입니다. 즉, 최상위 프레임 또는 단일 특정 iframe이 각각의 탐색 내역을 가집니다. 글 초반부에서 History API의 한계점을 지적했던 것, 기억나시죠? API 사양 문서를 읽어보면 여기에 대한 보충 설명이 추가되어 있으니 한 번 읽어보시는 것을 추천합니다.

마지막으로, 사용자의 주소 탐색 내역을 프로그래밍 방식으로 수정하거나 재배열하는 것에 대한 합의현재 진행 중입니다. 만약 이 기능이 가능해진다면, 이전 또는 이후의 탐색 내역 항목을 삭제하거나 대체할 수 있고, 또는 상태를 변경할 수도 있습니다. 꽤 유용한 기능이지만 이것은 현재의 History API로는 불가능합니다.

History API는 간단하게 사용 가능하지만 오래 전에 나온 만큼 여태까지 많은 문제가 있었습니다. Navigation API는 History API의 한계를 극복하고, 현대 웹 애플리케이션의 라우팅에 필요한 접근 방식을 충족하기 위해 제안되었습니다.

caniuse 아직 호환성이 좋은 편은 아니지만 최신 버전 크롬에서 바로 확인 가능하다는 점이 인상 깊었다

Navigation API는 별도의 플래그 설정 없이 Chrome 102 버전부터 사용할 수 있습니다. 최신 버전의 크롬으로 업데이트하여 데모를 직접 사용해보세요.

참고 자료

이 포스트가 유익하셨다면?




프로필 사진

👨‍💻 정종윤

글 쓰는 것을 좋아하는 프론트엔드 개발자입니다. 온라인에서는 재그지그라는 닉네임으로 활동하고 있습니다.


Copyright © 2024, All right reserved.

Built with Gatsby