최신 클라이언트 측 라우팅: Navigation API

단일 페이지 애플리케이션 빌드를 완전히 정비하는 새로운 API를 통해 클라이언트 측 라우팅을 표준화합니다.

브라우저 지원

  • Chrome: 102 <ph type="x-smartling-placeholder">
  • Edge: 102. <ph type="x-smartling-placeholder">
  • Firefox: 지원되지 않음 <ph type="x-smartling-placeholder">
  • Safari: 지원되지 않음 <ph type="x-smartling-placeholder">

소스

단일 페이지 애플리케이션(SPA)은 서버에서 완전히 새로운 페이지를 로드하는 기본 방법이 아닌, 사용자가 사이트와 상호작용할 때 콘텐츠를 동적으로 재작성하는 핵심 기능으로 정의됩니다.

SPA는 History API를 통해 (또는 제한된 경우에 사이트의 #hash 부분을 조정하여) 이 기능을 제공할 수 있었지만, SPA가 표준이 되기 오래 전에 개발된 투박한 API는 완전히 새로운 접근방법을 요구하고 있습니다. Navigation API는 단순히 History API의 부족한 부분을 패치하는 것이 아니라 이 영역을 완전히 정비하는 제안된 API입니다. 예를 들어 스크롤 복원에서 History API를 재구성하려고 하는 대신 패치를 적용했습니다.

이 게시물에서는 Navigation API를 개략적으로 설명합니다. 기술 제안서를 읽으려면 WICG 저장소에서 보고서 초안을 확인하세요.

사용 예

Navigation API를 사용하려면 먼저 전역 navigation 객체에 "navigate" 리스너를 추가합니다. 이 이벤트는 기본적으로 중앙 집중화됩니다. 이 이벤트는 사용자가 작업을 실행 (예: 링크 클릭, 양식 제출, 뒤로 이동)하거나 탐색이 프로그래매틱 방식으로 (예: 사이트 코드를 통해) 트리거될 때 모든 유형의 탐색에서 실행됩니다. 대부분의 경우 코드가 해당 작업에 대한 브라우저의 기본 동작을 재정의할 수 있습니다. SPA의 경우 이는 사용자가 동일한 페이지에 머무르게 하면서 사이트의 콘텐츠를 로드하거나 변경한다는 것을 의미합니다.

NavigateEvent는 도착 URL과 같은 탐색 관련 정보가 포함된 "navigate" 리스너에 전달되며 이를 통해 한곳에서 탐색에 응답할 수 있습니다. 기본 "navigate" 리스너는 다음과 같습니다.

navigation.addEventListener('navigate', navigateEvent => {
  // Exit early if this navigation shouldn't be intercepted.
  // The properties to look at are discussed later in the article.
  if (shouldNotIntercept(navigateEvent)) return;

  const url = new URL(navigateEvent.destination.url);

  if (url.pathname === '/') {
    navigateEvent.intercept({handler: loadIndexPage});
  } else if (url.pathname === '/cats/') {
    navigateEvent.intercept({handler: loadCatsPage});
  }
});

탐색은 다음 두 가지 방법 중 하나로 처리할 수 있습니다.

  • 위에 설명된 대로 intercept({ handler })를 호출하여 탐색을 처리합니다.
  • preventDefault()를 호출하여 내비게이션을 완전히 취소할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

이 예에서는 이벤트에서 intercept()를 호출합니다. 브라우저가 handler 콜백을 호출하여 사이트의 다음 상태를 구성합니다. 이렇게 하면 다른 코드에서 탐색 진행 상황을 추적하는 데 사용할 수 있는 전환 객체 navigation.transition가 생성됩니다.

일반적으로 intercept()preventDefault()가 모두 허용되지만 호출할 수 없는 경우도 있습니다. 탐색이 교차 출처 탐색인 경우 intercept()를 통해 탐색을 처리할 수 없습니다. 또한 사용자가 브라우저에서 뒤로 또는 앞으로 버튼을 누르고 있으면 preventDefault()를 통해 탐색을 취소할 수 없습니다. 사이트 사용자를 함정에 빠뜨려서는 안 됩니다. (GitHub에서 논의 중)

내비게이션 자체를 중지하거나 가로챌 수 없더라도 "navigate" 이벤트는 계속 실행됩니다. 이렇게 하면 유용한 정보이므로 예를 들어 사용자가 사이트를 떠나고 있음을 나타내는 애널리틱스 이벤트를 기록할 수 있습니다.

플랫폼에 다른 이벤트를 추가해야 하는 이유는 무엇인가요?

"navigate" 이벤트 리스너는 SPA 내에서 URL 변경사항을 중앙 집중식으로 처리합니다. 이전 API를 사용하는 것은 어려운 제안입니다. History API를 사용하여 자체 SPA의 라우팅을 작성한 적이 있다면 다음과 같은 코드를 추가했을 수도 있습니다.

function updatePage(event) {
  event.preventDefault(); // we're handling this link
  window.history.pushState(null, '', event.target.href);
  // TODO: set up page based on new URL
}
const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', updatePage));

이는 괜찮지만 모든 사례를 포함하고 있지는 않습니다. 링크는 페이지에 오고 갈 수 있으며 사용자가 페이지를 탐색할 수 있는 유일한 방법은 아닙니다. 예를 들어 양식을 제출하거나 이미지 지도를 사용할 수도 있습니다. 페이지에서 이러한 문제를 다뤄도 되지만, 새로운 Navigation API로 달성할 수 있는 수많은 가능성을 단순화할 수 있습니다.

또한 위의 함수는 뒤로-앞으로 탐색을 처리하지 않습니다. 다른 일정이 있습니다. "popstate".

개인적으로 History API는 이러한 가능성에 도움이 되는 방법을 사용할 수 있을 때가 많습니다. 하지만 실제로는 사용자가 브라우저에서 뒤로 또는 앞으로를 누를 때 응답하는 것과 URL을 푸시하고 바꾸는 두 가지 영역만 있습니다. "navigate"와 비유되지 않습니다. 단, 위에 설명된 것처럼 클릭 이벤트 리스너를 수동으로 설정하는 경우는 예외입니다.

탐색 처리 방법 결정

navigateEvent에는 특정 탐색을 처리하는 방법을 결정하는 데 사용할 수 있는 탐색에 관한 많은 정보가 포함되어 있습니다.

주요 속성은 다음과 같습니다.

canIntercept
false인 경우 탐색을 가로챌 수 없습니다. 교차 출처 탐색 및 문서 간 순회는 가로챌 수 없습니다.
destination.url
아마도 탐색을 처리할 때 고려해야 할 가장 중요한 정보일 것입니다.
hashChange
탐색이 동일한 문서이고 해시가 URL에서 현재 URL과 다른 유일한 부분인 경우 참입니다. 최신 SPA에서 해시는 현재 문서의 다른 부분에 연결하기 위한 것이어야 합니다. 따라서 hashChange이 true이면 이 탐색을 가로채지 않아도 됩니다.
downloadRequest
이 값이 true이면 download 속성이 있는 링크로 탐색이 시작된 것입니다. 대부분의 경우 이를 가로챌 필요가 없습니다.
formData
null이 아니면 이 탐색은 POST 양식 제출의 일부입니다. 탐색을 처리할 때 이를 고려해야 합니다. GET 탐색만 처리하려면 formData가 null이 아닌 경우 탐색을 가로채지 마세요. 이 도움말 뒷부분의 양식 제출 처리에 관한 예시를 참고하세요.
navigationType
"reload", "push", "replace" 또는 "traverse" 중 하나입니다. "traverse"인 경우 preventDefault()를 통해 이 탐색을 취소할 수 없습니다.

예를 들어 첫 번째 예에서 사용된 shouldNotIntercept 함수는 다음과 같을 수 있습니다.

function shouldNotIntercept(navigationEvent) {
  return (
    !navigationEvent.canIntercept ||
    // If this is just a hashChange,
    // just let the browser handle scrolling to the content.
    navigationEvent.hashChange ||
    // If this is a download,
    // let the browser perform the download.
    navigationEvent.downloadRequest ||
    // If this is a form submission,
    // let that go to the server.
    navigationEvent.formData
  );
}

가로채기

코드가 "navigate" 리스너 내에서 intercept({ handler })를 호출하면 이제 업데이트된 새로운 상태에 맞게 페이지를 준비하는 중이며 탐색에 다소 시간이 걸릴 수 있음을 브라우저에 알립니다.

브라우저는 먼저 현재 상태의 스크롤 위치를 캡처하므로 나중에 선택적으로 복원할 수 있도록 복원한 다음 handler 콜백을 호출합니다. handler가 프로미스를 반환하면 (비동기 함수에서 자동으로 발생) 이 프로미스는 탐색에 걸리는 시간과 성공 여부를 브라우저에 알려줍니다.

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

따라서 이 API는 브라우저가 이해할 수 있는 의미론적 개념을 도입합니다. 즉, 시간이 지남에 따라 문서를 이전 URL과 상태에서 새 URL로 변경하는 SPA 탐색이 현재 발생하고 있습니다. 이렇게 하면 접근성을 비롯한 여러 가지 잠재적인 이점이 있습니다. 브라우저에서 탐색의 시작, 끝 또는 잠재적인 실패를 표시할 수 있습니다. 예를 들어 Chrome은 기본 로드 표시기를 활성화하고 사용자가 중지 버튼과 상호작용할 수 있도록 합니다. (이는 현재 사용자가 뒤로/앞으로 버튼을 통해 이동할 때 발생하지 않지만 곧 수정될 예정입니다.)

탐색을 가로채면 handler 콜백이 호출되기 직전에 새 URL이 적용됩니다. DOM을 즉시 업데이트하지 않으면 이전 콘텐츠가 새 URL과 함께 표시되는 마침표가 생성됩니다. 이는 데이터를 가져오거나 새 하위 리소스를 로드할 때 상대 URL 확인 등에 영향을 미칩니다.

URL 변경을 지연하는 방법은 GitHub에서 논의되고 있지만 일반적으로 수신 콘텐츠에 대한 일종의 자리표시자를 사용하여 페이지를 즉시 업데이트하는 것이 좋습니다.

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

이렇게 하면 URL 확인 문제를 피할 수 있을 뿐만 아니라 사용자에게 즉시 응답하기 때문에 빠르게 느껴집니다.

신호 취소

intercept() 핸들러에서 비동기 작업을 할 수 있으므로 탐색이 중복될 수 있습니다. 이는 다음과 같은 경우에 발생합니다.

  • 사용자가 다른 링크를 클릭하거나 일부 코드가 다른 탐색을 실행합니다. 이 경우 이전 탐색은 폐기되고 새 탐색으로 대체됩니다.
  • 사용자가 버튼을 클릭합니다.

이러한 가능성을 처리하기 위해 "navigate" 리스너에 전달되는 이벤트에는 AbortSignalsignal 속성이 포함됩니다. 자세한 내용은 취소 가능한 가져오기를 참고하세요.

축약된 버전은 기본적으로 작업을 중지해야 할 때 이벤트를 실행하는 객체를 제공합니다. 특히 fetch()를 호출하는 모든 호출에 AbortSignal를 전달할 수 있습니다. 그러면 탐색이 선점되면 진행 중인 네트워크 요청이 취소됩니다. 이렇게 하면 사용자의 대역폭이 절약되고 fetch()에서 반환된 Promise가 거부되므로 다음 코드가 현재 유효하지 않은 페이지 탐색을 표시하도록 DOM을 업데이트하는 등의 작업을 할 수 없습니다.

다음은 이전 예이지만 getArticleContent를 인라인하여 AbortSignalfetch()와 함께 사용하는 방법을 보여줍니다.

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContentURL = new URL(
          '/get-article-content',
          location.href
        );
        articleContentURL.searchParams.set('path', url.pathname);
        const response = await fetch(articleContentURL, {
          signal: navigateEvent.signal,
        });
        const articleContent = await response.json();
        renderArticlePage(articleContent);
      },
    });
  }
});

스크롤 처리

탐색을 intercept()할 때 브라우저에서 자동으로 스크롤을 처리하려고 시도합니다.

새 기록 항목으로 이동하는 경우 (navigationEvent.navigationType"push" 또는 "replace"인 경우) URL 프래그먼트에 표시된 부분 (# 뒤의 비트)으로 스크롤하거나 페이지 상단으로 스크롤을 재설정하는 것을 의미합니다.

새로고침 및 순회에서는 스크롤 위치를 이 기록 항목이 마지막으로 표시되었던 위치로 복원합니다.

기본적으로 이 작업은 handler에서 반환된 프로미스가 확인되면 발생하지만 더 일찍 스크롤하는 것이 타당하다면 navigateEvent.scroll()를 호출할 수 있습니다.

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
        navigateEvent.scroll();

        const secondaryContent = await getSecondaryContent(url.pathname);
        addSecondaryContent(secondaryContent);
      },
    });
  }
});
드림

또는 intercept()scroll 옵션을 "manual"로 설정하여 자동 스크롤 처리를 완전히 선택 해제할 수도 있습니다.

navigateEvent.intercept({
  scroll: 'manual',
  async handler() {
    // …
  },
});

포커스 처리

handler에서 반환한 프로미스가 확인되면 브라우저에서 autofocus 속성이 설정된 첫 번째 요소에 포커스를 두고 해당 속성이 있는 요소가 없는 경우 <body> 요소에 포커스를 맞춥니다.

intercept()focusReset 옵션을 "manual"로 설정하여 이 동작을 선택 해제할 수 있습니다.

navigateEvent.intercept({
  focusReset: 'manual',
  async handler() {
    // …
  },
});

성공 및 실패 이벤트

intercept() 핸들러가 호출되면 다음 두 가지 중 하나가 발생합니다.

  • 반환된 Promise가 처리되거나 intercept()를 호출하지 않은 경우 Navigation API는 Event와 함께 "navigatesuccess"를 실행합니다.
  • 반환된 Promise가 거부되면 API는 ErrorEvent와 함께 "navigateerror"를 실행합니다.

이러한 이벤트를 통해 코드가 중앙에서 성공 또는 실패를 처리할 수 있습니다. 예를 들어, 다음과 같이 이전에 표시된 진행률 표시기를 숨기면 성공할 수 있습니다.

navigation.addEventListener('navigatesuccess', event => {
  loadingIndicator.hidden = true;
});

또는 실패 시 오류 메시지가 표시될 수 있습니다.

navigation.addEventListener('navigateerror', event => {
  loadingIndicator.hidden = true; // also hide indicator
  showMessage(`Failed to load page: ${event.message}`);
});

ErrorEvent를 수신하는 "navigateerror" 이벤트 리스너는 새 페이지를 설정하는 코드에서 오류를 수신할 수 있으므로 특히 편리합니다. 네트워크를 사용할 수 없는 경우 오류가 결국 "navigateerror"로 라우팅된다는 것을 알고 await fetch()만 하면 됩니다.

navigation.currentEntry는 현재 항목에 대한 액세스를 제공합니다. 사용자의 현재 위치를 설명하는 객체입니다. 이 항목에는 현재 URL, 시간 경과에 따라 이 항목을 식별하는 데 사용할 수 있는 메타데이터, 개발자가 제공한 상태가 포함됩니다.

메타데이터에는 현재 항목과 항목의 슬롯을 나타내는 각 항목의 고유한 문자열 속성인 key가 포함됩니다. 이 키는 현재 항목의 URL 또는 상태가 변경되더라도 동일하게 유지됩니다. 여전히 같은 슬롯에 있습니다. 반대로 사용자가 뒤로를 누른 다음 동일한 페이지를 다시 열면 이 새로운 항목이 새 슬롯을 만들기 때문에 key가 변경됩니다.

개발자는 Navigation API를 통해 일치하는 키가 있는 항목으로 사용자를 직접 이동할 수 있으므로 key가 유용합니다. 다른 항목이 있는 상태에서도 계속 유지하여 페이지 사이를 쉽게 이동할 수 있습니다.

// On JS startup, get the key of the first loaded page
// so the user can always go back there.
const {key} = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);

// Navigate away, but the button will always work.
await navigation.navigate('/another_url').finished;

Navigation API는 '상태' 개념을 표시합니다. 이는 개발자가 제공하는 정보로, 현재 기록 항목에 영구적으로 저장되지만 사용자에게는 직접 표시되지 않습니다. 이는 History API의 history.state와 매우 유사하지만 이를 개선했습니다.

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

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

기본값은 undefined입니다.

설정 상태

상태 객체는 변경될 수 있지만 이러한 변경사항은 기록 항목과 함께 다시 저장되지 않으므로 다음과 같이 됩니다.

const state = navigation.currentEntry.getState();
console.log(state.count); // 1
state.count++;
console.log(state.count); // 2
// But:
console.info(navigation.currentEntry.getState().count); // will still be 1

상태를 설정하는 올바른 방법은 스크립트 탐색 중에 설정하는 것입니다.

navigation.navigate(url, {state: newState});
// Or:
navigation.reload({state: newState});

여기서 newState는 모든 클론 가능한 객체일 수 있습니다.

현재 항목의 상태를 업데이트하려면 현재 항목을 대체하는 탐색을 실행하는 것이 가장 좋습니다.

navigation.navigate(location.href, {state: newState, history: 'replace'});

그러면 "navigate" 이벤트 리스너가 navigateEvent.destination를 통해 이 변경사항을 선택할 수 있습니다.

navigation.addEventListener('navigate', navigateEvent => {
  console.log(navigateEvent.destination.getState());
});

동기식으로 상태 업데이트

일반적으로 navigation.reload({state: newState})를 통해 상태를 비동기식으로 업데이트하는 것이 더 좋으며 그러면 "navigate" 리스너가 이 상태를 적용할 수 있습니다. 그러나 사용자가 <details> 요소를 전환하거나 사용자가 양식 입력 상태를 변경할 때와 같이 코드에서 변경사항에 관해 듣을 때쯤에는 상태 변경이 이미 완전히 적용되는 경우도 있습니다. 이러한 경우 새로고침 및 순회를 통해 변경사항이 보존되도록 상태를 업데이트할 수 있습니다. updateCurrentEntry()를 사용하면 가능합니다.

navigation.updateCurrentEntry({state: newState});

이번 변경사항에 관해 알아보는 이벤트도 있습니다.

navigation.addEventListener('currententrychange', () => {
  console.log(navigation.currentEntry.getState());
});

그러나 "currententrychange"의 상태 변경에 반응하는 경우 "navigate" 이벤트와 "currententrychange" 이벤트 간에 상태 처리 코드를 분할하거나 복제할 수 있지만 navigation.reload({state: newState})를 사용하면 한곳에서 처리할 수 있습니다.

상태 매개변수와 URL 매개변수 비교

상태는 구조화된 객체일 수 있으므로 모든 애플리케이션 상태에 사용하고 싶을 수 있습니다. 그러나 대부분의 경우 URL에 이 상태를 저장하는 것이 더 좋습니다.

사용자가 URL을 다른 사용자와 공유해도 상태가 유지될 것으로 예상하는 경우 URL에 상태를 저장합니다. 그 외의 경우에는 상태 객체를 사용하는 것이 더 좋습니다.

모든 항목 액세스

'현재 항목' 전부는 아닙니다. 또한 API는 사용자가 사이트를 사용하는 동안 navigation.entries() 호출을 통해 탐색한 전체 항목 목록에 액세스할 수 있는 방법을 제공합니다. 이 호출은 항목의 스냅샷 배열을 반환합니다. 예를 들어 사용자가 특정 페이지로 이동한 방법에 따라 다른 UI를 표시하거나 이전 URL 또는 상태를 다시 살펴볼 때 사용할 수 있습니다. 현재 History API로는 불가능합니다.

개별 NavigationHistoryEntry에서 "dispose" 이벤트를 수신 대기할 수도 있습니다. 이 이벤트는 항목이 더 이상 브라우저 기록의 일부가 아니면 실행됩니다. 이는 일반 정리의 일부로 발생할 수 있지만 탐색 중에도 발생할 수 있습니다. 예를 들어 10개 장소를 뒤로 이동했다가 앞으로 이동하면 기록 항목 10개가 삭제됩니다.

"navigate" 이벤트는 위에 설명된 대로 모든 유형의 탐색에 대해 발생합니다. (실제로 가능한 모든 유형의 사양에 긴 부록이 있습니다.)

많은 사이트에서 사용자가 <a href="...">를 클릭하는 경우가 가장 흔하지만, 두 가지의 눈에 띄고 복잡한 탐색 유형이 두 가지 있습니다.

프로그래매틱 탐색

첫 번째는 프로그래매틱 탐색으로, 탐색은 클라이언트 측 코드 내의 메서드 호출로 인해 발생합니다.

코드 어디에서든 navigation.navigate('/another_page')를 호출하여 탐색을 실행할 수 있습니다. 이 작업은 "navigate" 리스너에 등록된 중앙 집중식 이벤트 리스너에서 처리하며 중앙 집중식 리스너는 동기식으로 호출됩니다.

이는 location.assign() 및 친구 같은 이전 메서드와 History API의 메서드 pushState(), replaceState()의 개선된 집계를 위한 것입니다.

navigation.navigate() 메서드는 { committed, finished }에 두 개의 Promise 인스턴스가 포함된 객체를 반환합니다. 이렇게 하면 호출자가 전환이 '커밋'될 때까지 기다릴 수 있습니다. (표시 URL이 변경되고 새 NavigationHistoryEntry를 사용할 수 있음) 또는 '완료' (intercept({ handler })에서 반환된 모든 프라미스는 실패하거나 다른 탐색에 의해 선점되어 완료되거나 거부됩니다.)

navigate 메서드에는 옵션 객체도 있으며, 이 객체에서 다음을 설정할 수 있습니다.

  • state: 새 기록 항목의 상태로, NavigationHistoryEntry.getState() 메서드를 통해 사용할 수 있습니다.
  • history: "replace"로 설정하여 현재 기록 항목을 대체할 수 있습니다.
  • info: navigateEvent.info를 통해 탐색 이벤트에 전달할 객체입니다.

특히 info는 예를 들어 다음 페이지를 표시하는 특정 애니메이션을 나타내는 데 유용할 수 있습니다. 대안은 전역 변수를 설정하거나 #hash의 일부로 포함하는 것입니다. 두 옵션 모두 약간 어색합니다.) 특히 사용자가 나중에 뒤로 버튼과 앞으로 버튼을 통해 탐색을 하면 이 info가 재생되지 않습니다. 실제로 이러한 경우에는 항상 undefined입니다.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> 왼쪽 또는 오른쪽에서 여는 모습 데모

navigation에는 { committed, finished }가 포함된 객체를 반환하는 다른 여러 탐색 메서드도 있습니다. traverseTo() (사용자 기록에서 특정 항목을 나타내는 key을 허용함) 및 navigate()는 이미 언급했습니다. back(), forward(), reload()도 포함됩니다. 이러한 메서드는 모두 navigate()와 마찬가지로 중앙 집중식 "navigate" 이벤트 리스너에 의해 처리됩니다.

양식 제출

둘째, POST를 통한 HTML <form> 제출은 특수한 탐색 유형이며 Navigation API가 이를 가로챌 수 있습니다. 추가 페이로드가 포함되어 있지만 탐색은 여전히 "navigate" 리스너에 의해 중앙에서 처리됩니다.

NavigateEvent에서 formData 속성을 찾아 양식 제출을 감지할 수 있습니다. 다음은 제출된 양식을 fetch()를 통해 현재 페이지에 유지되는 형식으로 변환하는 예입니다.

navigation.addEventListener('navigate', navigateEvent => {
  if (navigateEvent.formData && navigateEvent.canIntercept) {
    // User submitted a POST form to a same-domain URL
    // (If canIntercept is false, the event is just informative:
    // you can't intercept this request, although you could
    // likely still call .preventDefault() to stop it completely).

    navigateEvent.intercept({
      // Since we don't update the DOM in this navigation,
      // don't allow focus or scrolling to reset:
      focusReset: 'manual',
      scroll: 'manual',
      handler() {
        await fetch(navigateEvent.destination.url, {
          method: 'POST',
          body: navigateEvent.formData,
        });
        // You could navigate again with {history: 'replace'} to change the URL here,
        // which might indicate "done"
      },
    });
  }
});

누락된 정보

"navigate" 이벤트 리스너의 중앙 집중식 특성에도 불구하고 현재 Navigation API 사양은 페이지를 처음 로드할 때 "navigate"를 트리거하지 않습니다. 모든 상태에 서버 측 렌더링 (SSR)을 사용하는 사이트의 경우 이 방법이 좋을 수 있습니다. 서버가 올바른 초기 상태를 반환할 수 있어 사용자에게 콘텐츠를 제공하는 가장 빠른 방법입니다. 하지만 클라이언트 측 코드를 활용하여 페이지를 만드는 사이트에서는 페이지를 초기화하는 추가 함수를 만들어야 할 수도 있습니다.

Navigation API의 또 다른 디자인 선택은 단일 프레임(즉, 최상위 페이지 또는 단일 특정 <iframe>) 내에서만 작동하는 것입니다. 이는 사양에 자세히 문서화되어 있는 여러 흥미로운 함의를 포함하고 있지만, 실제로는 개발자의 혼동을 줄일 수 있습니다. 이전 History API에는 프레임 지원과 같이 혼란스러운 극단적인 사례가 여러 개 있었으며, 새롭게 디자인된 Navigation API는 이러한 특이 사례를 처음부터 처리합니다.

마지막으로, 사용자가 탐색한 항목 목록을 프로그래매틱 방식으로 수정하거나 재정렬하는 것에 관해 아직 합의되지 않았습니다. 현재 논의 중이지만 이전 항목 또는 '향후 모든 항목' 중에서 삭제만 허용하는 옵션이 있을 수 있습니다. 후자는 임시 상태를 허용합니다. 예를 들어 개발자는 다음과 같은 작업을 할 수 있습니다.

  • 새 URL 또는 상태로 이동하여 사용자에게 질문할 수 있습니다.
  • 사용자가 작업을 완료하도록 허용 (또는 뒤로 이동)
  • 작업 완료 시 기록 항목 삭제

이는 임시 모달 또는 전면 광고에 적합할 수 있습니다. 새 URL은 사용자가 뒤로 동작을 사용하여 페이지를 나갈 수 있지만 실수로 앞으로 이동하여 다시 열 수는 없습니다 (항목이 삭제되었기 때문에). 현재의 History API로는 불가능합니다.

Navigation API 사용해 보기

Navigation API는 Chrome 102에서 플래그 없이 사용할 수 있습니다. Domenic Denicola데모를 사용해 볼 수도 있습니다.

기존 History API는 간단해 보이지만 잘 정의되어 있지 않으며 특정 사례와 브라우저마다 다르게 구현되는 방식과 관련하여 많은 문제가 있습니다. 새로운 Navigation API에 대한 의견을 제공해 주시기 바랍니다.

참조

감사의 말씀

이 게시물을 리뷰해 주신 토마스 슈타이너, 도메닉 데니콜라, 네이트 채팽님께 감사드립니다. Unsplash의 히어로 이미지(제레미 제로 제작)