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

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

브라우저 지원

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 지원되지 않음
  • Safari: 지원되지 않음

소스

단일 페이지 애플리케이션(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는 브라우저가 이해하는 시맨틱 개념을 도입합니다. 현재 SPA 탐색이 진행 중이며 시간이 지남에 따라 문서가 이전 URL 및 상태에서 새 URL 및 상태로 변경됩니다. 이렇게 하면 접근성을 비롯한 여러 이점이 있습니다. 브라우저에서 탐색의 시작, 끝 또는 잠재적 실패를 표시할 수 있습니다. 예를 들어 Chrome은 기본 로드 표시기를 활성화하고 사용자가 중지 버튼과 상호작용할 수 있도록 허용합니다. 현재 사용자가 뒤로/앞으로 버튼을 통해 탐색할 때는 이 문제가 발생하지 않지만 곧 수정될 예정입니다.

탐색을 가로채면 새 URL이 handler 콜백이 호출되기 직전에 적용됩니다. 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에서 반환된 Promise가 해결되면 브라우저는 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가 변경됩니다.

개발자에게 key는 유용합니다. Navigation API를 사용하면 키가 일치하는 항목으로 사용자를 직접 이동할 수 있기 때문입니다. 다른 항목의 상태에서도 페이지 간에 쉽게 이동할 수 있도록 이 탭을 계속 누르고 있을 수 있습니다.

// 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입니다.

왼쪽 또는 오른쪽에서 여는 데모

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에서 플래그 없이 사용할 수 있습니다. 도메닉 데니콜라데모를 사용해 보세요.

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

참조

감사의 말씀

이 게시물을 검토해 주신 토마스 슈타이너, 도메닉 데니콜라, 네이트 채핀님께 감사드립니다.