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

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

제이크 아치볼드
제이크 아치볼드

브라우저 지원

  • 102
  • 102
  • x
  • x

소스

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

SPA는 History API를 통해 (또는 사이트의 #hash 부분을 조정하는 제한적인 경우에) 이 기능을 제공할 수 있었지만, SPA가 표준이 되기 훨씬 전에 개발된 투박한 API이며 웹은 완전히 새로운 접근 방식을 요구하고 있습니다. Navigation API는 History API의 다듬어진 부분을 단순히 패치하는 것이 아니라 이 공간을 완전히 정비하는 제안된 API입니다. 예를 들어 스크롤 복원은 History API를 재구상하려고 하기보다는 이 API에 패치를 적용했습니다.

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

사용 예

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

도착 URL과 같은 탐색에 관한 정보가 포함된 "navigate" 리스너에 NavigateEvent가 전달되며 개발자가 한곳에서 탐색에 응답할 수 있습니다. 기본 "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과 다른 유일한 부분인 경우 true입니다. 최신 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가 프로미스 (async functions와 함께 자동으로 발생)를 반환하는 경우 해당 프로미스는 탐색에 걸리는 시간과 탐색 성공 여부를 브라우저에 알려줍니다.

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은 기본 로드 표시기를 활성화하고 사용자가 중지 버튼과 상호작용할 수 있도록 합니다. 현재는 사용자가 뒤로-앞으로 버튼을 탐색할 때는 이 문제가 발생하지 않지만 곧 해결될 예정입니다.

탐색을 가로채면 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" 리스너에 전달되는 이벤트에는 signal 속성(AbortSignal)이 포함됩니다. 자세한 내용은 중단 가능한 가져오기를 참조하세요.

짧은 버전은 기본적으로 작업을 중지해야 할 때 이벤트를 실행하는 객체를 제공한다는 것입니다. 특히 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와 매우 유사하지만 이 API와 비슷합니다.

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에 대한 의견을 제공해 주시기 바랍니다.

참조

감사의 말씀

게시물을 검토해 주신 토마스 슈타이너, 도메닉 데니콜라, 네이트 차핀에게 감사드립니다. Unsplash의 히어로 이미지, Jeremy Zero