ECMAScript | TypeScript

하이브리드 웹과 BF캐시

partner_jun 2022. 9. 9. 17:37

BF캐시와 pageshow

Back-Forward Cache. BF캐시는 SPA나 다이나믹한 웹 사이트 등 JS의 영향이 커지고 각 페이지의 '무게감'이 커짐에 따라 중요해졌다. 브라우저가 노출하던 페이지를 메모리에 그대로 유지해두고 다른 페이지로 이동함으로써, 사용자의 뒤로가기/앞으로가기 동작시 유지해둔 페이지와 그 상태를 그대로 노출하는 것이다. 간단하게 각 페이지를 스택 구조로 쌓는다고 보면 될 것이다.

크롬 개발자 도구에서 테스트도 가능하다

사용자 입장에서는 보던 페이지를 빠르게 볼 수 있고 하던 작업을 이어서 할 수 있으니 아주 좋은 경험을 할 수 있다. 특히 초기 동작이 느린 SPA기반 사이트에서는 더 큰 도움이 된다. 하지만 FE개발자의 입장에서는 이 BF캐시가 양날의 검이다. '느리다'는 말 대신 화면이 업데이트 되지 않거나(메모리에 올라가 있는 것을 사용하므로) react의 hook이나 vue의 mounted와 같은 라이프사이클에서 실행되어야 할 로직이 무시되는 경우가 발생한다. 그래서 우리 개발자들은 잘 알려진대로 window 객체의 pageshow 이벤트를 핸들링한다.

useEffect(() => {
  const onPageShow = (e: any) => {
    // e.persisted <- BF 캐시 작동 여부(boolean)
    // 로직 실행
  };

  window.addEventListener('pageshow', onPageShow);
  return () => {
    window.removeEventListener('pageshow', onPageShow);
  }
}, []);

pageshow 이벤트를 핸들링하는 예시

 
하지만, 앱의 웹뷰에서 노출되는 하이브리드 웹에서 추가로 고려해야 할 문제가 있다. 바로 앱 비활성화/활성화 상태에 따른 핸들링이다.
 
 

하이브리드 웹 페이지와 visibilitychange

안드로이드와 IOS의 앱 라이프 사이클. 두 운영체제 모두 background-&amp;gt;active에 해당하는 'resume' 동작이 있다.

하이브리드 웹에서는 비활성화된 앱(정확히는 웹뷰)이 활성화될 때를 확인할 필요가 있다. 백그라운드로 넘어간 앱(웹뷰)가 다시 활성화 될 때마다 웹뷰를 새로고침 하게 하여 해결할 수도 있겠지만, 위에서 설명한 BF캐시와 같이 최근의 웹 환경상(하이브리드 웹으로 사용되는 페이지는 더더욱) 페이지를 새로고침 하는 것이 꽤나 무겁고 어려운 일이다. 페이지에 따라서는 끔찍한 사용자 경험을 선사할 것이다. 앱 개발자 역시 이를 알고 최대한 웹뷰를 유지하고자 할 것이며, 서비스에 따라 웹뷰 위에 웹뷰가 쌓이는 스택 형태로 구현되기도 할 것이다.
따라서 웹 페이지에서 활성화 상태를 체크하여 로직을 수행하는 것이 더 낫다는 것은 명확하다. 웹 페이지에서 웹뷰의 활성화, 혹은 비활성화 상태를 알 수 있는 방법은 documentvisibilitychange 이벤트다.

useEffect(() => {
  const onVisibilityChanged = () => {
    if (document.hidden) return; // document가 보여지고 있는 여부 체크
    // 로직 수행
  };

  // visibilitychange는 document 이벤트임에 주의
  document.addEventListener('visibilitychange', onVisibilityChanged);
  return () => {
    document.removeEventListener('visibilitychange', onVisibilityChanged);
  }
}, []);

visibilitychange 이벤트를 핸들링한 코드

 
이 코드에서 주의할만한 것이 두가지 있다. 첫번째는 document.hidden 값이다. 이 값은 현재 웹 페이지-document가 활성화 되어 있는지 여부이다. 따라서 이 값이 true라면 화면에 보이지 않고 있는 상태이기 때문에 로직을 분기할 수 있다. 불필요한 API 요청을 막기 위해서는 반드시 체크해야 할 것이다. 두번째는 document에 이벤트 핸들러를 등록했다는 것이다. 특이하게도 이 visibilitychange 이벤트는 document에서 트리거된다. window에 핸들링하면 이벤트 핸들러가 작동되지 않는다.
 
 

PageShow vs VisibilityChange

이 두개 이벤트를 조금 더 비교해보자면 아래와 같다.

  •  pageshow
    • 페이지 진입/이탈시 트리거됨
    • 페이지 첫 진입시 실행됨 (event.persisted 값으로 BF캐시 작동여부 체크)
    • Next.js 개발환경 등 특정 상황에서 실행되지 않을 수 있음(캐시 연관)
  • visibilitychange
    • 페이지(웹뷰 혹은 브라우저 탭) 활성화/비활성화시 트리거됨
    • 페이지 첫 진입시 미실행 (document.hidden 값으로 현재 상태 체크)
    • 캐시와 무관하게 항상 실행됨

이쯤 오면 pageshow와 visibilitychange를 묶을만한 이유가 보인다. 대부분의 하이브리드 웹 페이지에서 이 두개 이벤트는 같은 일을 해야 하기 때문이다. 이번에 진행했던 프로젝트의 여정을 예로 들어본다.

BF 캐시로 인한 불일치

  1. 숙소 상세 페이지를 보던 중 객실 페이지로 이동
  2. 객실 페이지에서 이 숙소 찜하기 클릭
  3. 뒤로 가기를 이용하여 숙소의 상세 페이지로 이동
    • 숙소 상세 페이지의 '찜하기' 상태가 불일치

웹뷰 비활성화로 인한 불일치

  1. 숙소 상세 페이지를 보던 중 앱 비활성화
  2. 다른 기기, 혹은 웹 페이지에서 이 숙소 찜하기 클릭
  3. 앱 다시 활성화
    • 숙소 상세 페이지의 '찜하기' 상태가 불일치

이 두 상황 모두 숙소 상세 페이지의 '찜하기' 상태의 불일치가 일어났다. 이미 보여지고 있던 페이지이기 때문인데, 앱이 한번에 여러 화면을 띄우지는 않기 때문에 pageshow, visibilitychange 이벤트 핸들러를 통해 '찜하기' 상태를 다시 가져와 화면을 다시 그려내 아주 간단하게 해결할 수 있는 문제다. 이와 같이 하이브리드 웹 페이지에서는 노출되는 특정 값이 다이나믹하게 변하는 경우가 많고, 두개 이벤트가 함께 처리될 수 있는 경우가 많다. 

/**
 * BFCache 및 최소화-활성화에 대한 대응 코드
 *
 * visibilitychange와 pageshow 두개 이벤트 모두를 핸들링하므로
 * debounce 처리된 함수를 핸들러로 등록한다.
 */
useEffect(() => {
  const onVisibilityChanged = debounce((e: any) => {
    if (!e.persisted || document.hidden) return; // bf캐시 미작동(첫진입)이거나 화면이 보이지 않는 경우 무시
    // 페이지에서 다시 처리할 로직 실행 (ex. refetch)
  }, 300);

  // visibilitychange는 document 이벤트임에 주의
  document.addEventListener('visibilitychange', onVisibilityChanged);
  window.addEventListener('pageshow', onVisibilityChanged);
  return () => {
    document.removeEventListener('visibilitychange', onVisibilityChanged);
    window.removeEventListener('pageshow', onVisibilityChanged);
  }
}, []);

debounce 함수를 이용하여 두개 이벤트를 한번에 핸들링

 
위 코드와 같이 앞서 설명한 두개 이벤트에 debounce 처리된 이벤트 핸들러를 추가하여 한번에 처리할 수 있다. 여기서 주의해야 할 것은 pageshow 이벤트는 visibilitychange 이벤트와 달리 페이지에 첫 진입시에도 트리거된다는 것이다. 따라서 명확한 조건을 설정하지 않는다면 페이지 진입시 불필요하게 API가 호출될 수 있다.
 
 
 

마지막으로

결과적으로 위와 같은 코드를 작성해 웹뷰에 노출되는 웹 페이지에서 앱의 상태를 확인할 수 있고, 그에 따라 로직을 수행할 수 있다. 개인적으로 이 문제는 여러 상황을 이해해야 하는 골치아픈 문제라고 생각한다. 기획자도 이 문제에 대해 이해해야 더 명확한 기획안을 제시할 수 있기 때문이다. 하이브리드 웹의 숙명이라고도 볼 수 있을테니 개발자가 잘 설명해주는 수밖에 없을 것 같다.
크롬에는 Page Lifecycle API로 앱과 유사하게 freeze, resume 이벤트가 추가되었다. 이전처럼 단순한 문서 형식의 웹 사이트는 사라져가고 하이브리드 웹이나 사용자의 동작에 따라 다이나믹하게 변경되는 사이트가 주류로 자리잡고 있기 때문에 금새 브라우저 표준으로 등록될 것이라고 생각한다. 특히나 IE가 박멸되어가고 있으니...