unload 비동기 API 요청이 동작하지 않을 때 sendBeacon을 사용하자

dobby·2025년 11월 11일
0
post-thumbnail

새로고침시 브라우저 이벤트가 발생하게 된다.
새로고침에 의한 이벤트는 beforeunloadunload 가 있는데, beforeunloadunload 하기 전, 사용자에게 새로고침을 할 것인지 경고 혹은 알리는데 주로 사용한다.

unload 는 새로고침 되기 직전에 발생하는 이벤트이다.

사용자가 직접 새로고침한 것이기에 의도된 것이라고 판단하고 beforeunload 는 고려하지 않기로 했다.
그래서 unload 이벤트에 서버로 shutdown API를 요청해 클라이언트와 서버의 시스템 상태를 동기화시키도록 로직을 작성해줬다.

새로고침은 모든 Page 컴포넌트에서 발생할 수 있는거기에 App.tsx 파일에 이벤트를 등록해주었다.

  const { updateSystemState } = useSystemStateStore();
  const { mutate: shutdownMutate } = useApi<ShutdownResponse>(
    systemAPI.shutdown,
    {
      onSuccess: () => {
        updateSystemState(SYSTEM_MODE.SHUTDOWN);
      },
      onError: (res) => {
        updateSystemState(SYSTEM_MODE.ERROR);
      },
    },
  );

  useEffect(() => {
    const unloadFunc = async () => {
      await shutdownMutate()
    };
    
    window.addEventListener('unload', unloadFunc);
    return () => {
      window.removeEventListener('unload', unloadFunc);
    };
  }, []);

하지만 unloadFunc 함수는 실행이 되지만 shutdownMutate 함수가 정상적으로 실행되지 않았고, console로 API 요청 후 출력까지 해봤지만 console도 실행되지 않았다.


문제 발생 이유

브라우저 환경에서 unload 이벤트가 발생하면 브라우저는 현재 페이지를 즉시 unload 하고 새 페이지 로드를 시작한다.

여기서 unload 이벤트는 동기적이며, unload 핸들러가 실행되더라도 브라우저가 이 핸들러가 완전히 종료될때까지 기다려주지 않는다.

즉, 페이지 unload 작업이 매우 빠르게 진행된다.

그런데 핸들러 안에서 비동기적으로 시간이 소요되는 로직을 호출하고, 작업이 종료되기 전에 페이지가 unload 되면서 이후의 console이 출력되지도, 정상적으로 요청이 처리되지도 않은 것이다.


문제 해결

하지만 나는 브라우저가 페이지를 닫기 전에 서버에 데이터를 보내야 하고, 비동기 로직을 await 하는걸 기다려주지 않는다.

그렇기에 unload 이벤트에 핸들러를 등록하는 것 대신, 브라우저가 직접 요청을 처리할 수 있는 방법을 찾아야 한다.

이를 알아보다 navigator 에서 제공해주는 sendBeacon() 이라는 메소드를 알아냈다.

MDN

navigator.sendBeacon() 메서드는 적은 양의 데이터를 포함하는 HTTP POST 요청을 비동기적으로 웹 서버에 보냅니다.

딱 나한테 필요한 기능!

서버로부터 응답을 받아야 하면 fetchkeepalivetrue 로 설정한 걸 사용하라고 한다.
나한텐 필요없다.

sendBeacon(url)
sendBeacon(url, data)
  • url : data를 받을 서버의 URL, 상대 주소와 절대 주소 모두 가능하다
  • data : ArrayBuffer , TypedArray, DataView, Blob , 문자열 또는 객체 리터럴, FormData, URLSearchParams 등 전송할 데이터를 담은 객체

성공적으로 사용자 에이전트가 전송할 data를 대기열에 추가하면 true 를 반환하고, 아니라면 false 를 반환한다.

사용자 에이전트?
사용자를 대표하는 컴퓨터 프로그램으로, 웹 맥락에선 브라우저를 의미한다.

설명을 보면 분석 정보나 진단 데이터를 서버에 보내기 위한 목적으로 만들었다고 한다.
하지만? 사용하기 편하고 적절한 대안이니 상황에 따라 사용 가능

원래는 unload 이벤트 발생 시 브라우저가 정상적으로 비동기 요청을 전송할 수 있도록 지원했다고 한다.

하지만 이는 다음 페이지로의 탐색 속도가 저하되기 때문에, 사용자는 새로운 페이지가 느리다고 느끼게 되는 것이다. 이 UX를 개선하기 위해 고안된 메소드라고 한다.

(사실 unload 이벤트에 비동기 로직을 작성하는건 굉장히 좋지 않다고 적혀있다.)

장점은 다음과 같다고 설명한다.

  • 데이터가 안정적으로 전송됨
  • 비동기적임
  • 다음 페이지에 영향을 끼치지 않음

이제 어떤 목적으로 이 메소드가 만들어졌고, 어떤 이점이 있는지에 대해 알았으니 적용해보자.

  const unloadFunc = async () => {
    const shutdownPayload = JSON.stringify({ reason: 'window_unload' });
    const blob = new Blob([shutdownPayload], { type: 'application/json' });

    // sendBeacon을 사용하여 API 요청을 브라우저에 위임
    const success = navigator.sendBeacon('/api/system/shutdown', blob);

    if (success) {
      console.log('Shutdown request successfully initiated via sendBeacon.');
    } else {
      console.log('Failed to initiate sendBeacon request.');
    }

    // sendBeacon은 비차단이므로 다음 코드는 즉시 실행되지만,
    // API 응답을 기다리지 않으므로 로그아웃 성공/실패 여부를 알 수 없다.
    console.log('dhfh (sendBeacon started)');
  };

  useEffect(() => {
    window.addEventListener('unload', unloadFunc);
    return () => {
      window.removeEventListener('unload', unloadFunc);
    };
  }, []);

전달할 수 있는 데이터 중 알고 있는 타입이 Blob 였기에 이를 사용했다.

이렇게 작성하고 실행해봤더니, 정상적으로 처리된걸 확인했다.

네트워크 탭으로 확인할 땐 Type ping으로 cancled 상태의 요청만 보였는데, 이건 실패했다는건 아니고 sendBeacon 은 성능에 영향을 주지 않도록 백그라운드에서 실행되기 때문에 ping 이나 other 등을 분류되는게 일반적이라고 한다.

그리고 ping 은 매우 낮은 우선순위로 처리되기 때문에 브라우저 개발자 도구가 이를 완전히 추적하지 못했다는 것으로 해석할 수 있다.

확실하게 처리됐는지 확인하고자 한다면, 서버 로그를 확인하는걸 추천한다!

profile
성장통을 겪고 있습니다.

0개의 댓글