ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 뒤로 가기했더니 예전 화면이 그대로 나타나는 이유: bfcache
    Programming 2026. 9. 21. 12:27
    728x90
    반응형

    커튼 뒤에 의자와 책이 그대로 놓인 작은 방 모형
    이전 화면 상태를 복원하는 bfcache를 비유한 AI 생성 이미지입니다.

    뒤로 가기에서 이전 화면이 복원되면 bfcache 여부를 확인하고 다시 검증할 데이터만 명확히 갱신하세요.

    목록에서 상세 페이지로 들어갔다가 뒤로 가기를 눌렀더니 스크롤 위치와 펼친 항목까지 그대로 돌아옵니다. 서버 요청은 보이지 않는데 화면은 즉시 나타납니다. 이전 데이터를 잘못 캐시한 것처럼 보일 수 있지만, 브라우저가 페이지 자체를 복원한 상황일 수 있습니다.

    이 기능을 bfcache, 즉 back/forward cache라고 합니다. 사용자가 보던 자리를 보존하는 장점이 있지만 복원된 데이터가 현재 서버 상태와 같다는 뜻은 아닙니다. 이 글은 2026년 9월 13일 공식 문서를 바탕으로 복원 여부를 확인하고 갱신할 부분을 정하는 방법을 설명합니다. 브라우저 실험 결과가 아니라 설계 예시입니다.

    HTTP 응답 재사용과 페이지 복원은 다릅니다

    HTTP 캐시는 이전 요청의 응답을 저장해 다시 쓰는 기능입니다. bfcache는 페이지를 떠날 때 즉시 폐기하는 대신 JavaScript 실행을 멈추고 페이지 상태를 메모리에 보존했다가 뒤로·앞으로 탐색할 때 복원하는 기능입니다. JavaScript 메모리 상태도 복원의 일부입니다. bfcache의 기본 동작

    따라서 HTML을 다시 받고 스크립트를 처음부터 실행하는 흐름과 다릅니다. 기존 화면 상태와 변수, 등록된 이벤트 처리가 이어질 수 있습니다. 새로고침 때 실행되는 초기화 코드만 생각하면 복원 시점의 갱신이 빠질 수 있습니다.

    예를 들어 목록의 펼친 항목과 스크롤 위치는 사용자가 돌아오려던 자리이므로 보존하는 편이 좋을 수 있습니다. 반면 목록 위에 표시한 현재 접수 가능 건수는 다른 곳에서 바뀌었을 수 있습니다. 같은 화면 안에서도 유지할 상태와 다시 확인할 데이터가 다릅니다.

    SPA에서 라우터가 화면 일부만 바꾸는 이동은 브라우저가 다른 문서로 이동했다가 복원하는 bfcache와 구분해야 합니다. SPA를 완전히 떠났다가 돌아오는 경우에는 bfcache가 관여할 수 있지만 모든 라우트 전환을 bfcache라고 부르지는 않습니다. SPA 탐색과 bfcache

    pageshow의 persisted로 복원을 구분합니다

    pageshow는 최초 로드에서도 발생하므로 이벤트가 왔다는 사실만으로 bfcache 적중을 판정하지 않습니다. 복원을 구분할 때는 event.persisted를 확인합니다. 다음은 페이지 코드에 넣는 진단 형태이며 실제 수집 로그가 아닙니다.

    window.addEventListener("pageshow", (event) => {
      console.log("pageshow", { restored: event.persisted });
    });
    

    이 문서 간 탐색 진단에서 persisted가 true인 pageshow는 bfcache에서 복원됐다는 신호로 사용할 수 있습니다. 최초 로드의 pageshow는 load 이후에 발생합니다. 이미 최초 로드 시 API를 부르는 앱에 무조건 같은 요청을 추가하면 초기 요청이 중복될 수 있습니다. pageshow 이벤트

    이벤트 이름의 show도 주의해서 읽어야 합니다. pageshow는 페이지가 실제로 사용자 눈앞에 보이는 순간만 뜻하지 않습니다. 백그라운드 탭이나 사전 렌더링에서도 발생할 수 있습니다. 화면이 보이는지 자체를 감지하려는 목적이라면 visibilitychange 등과 구분해서 설계합니다. pageshow와 실제 가시성의 차이

    떠날 때의 pagehide에도 persisted가 있지만 의미는 대칭적이지 않습니다. true이면 브라우저가 보존하려는 상황을 나타내지만 나중에 반드시 저장·복원된다는 보증은 아닙니다. 실제로 돌아왔을 때의 신호를 따로 확인해야 합니다. 저장 의도와 실제 복원

    가상의 공개 목록에서 숫자만 다시 확인해 봅니다

    예시 페이지에는 공개 목록과 “현재 접수 가능 건수”가 있다고 하겠습니다. 목록의 펼침 상태와 스크롤은 유지하고, 숫자만 같은 출처의 /api/open-count에서 다시 받습니다. 민감한 로그인 정보가 없는 화면이라는 조건입니다.

    HTML에는 숫자를 담는 #open-count와 갱신 상태를 알리는 #refresh-state 요소가 있고, 최초 로드에서 값을 채우는 기존 코드가 있다고 가정합니다. 아래 코드는 해당 요소가 준비된 뒤 실행하는 모듈 스크립트의 복원 처리 부분입니다. 실제 API를 호출한 기록이 아닙니다.

    const countNode = document.querySelector("#open-count");
    const stateNode = document.querySelector("#refresh-state");
    let revision = 0;
    let activeRequest = null;
    
    if (!countNode || !stateNode) {
      throw new Error("갱신 대상 요소가 없습니다.");
    }
    
    async function refreshCount() {
      const mine = ++revision;
      activeRequest?.abort();
      const controller = new AbortController();
      activeRequest = controller;
      stateNode.textContent = "최신 건수를 확인하고 있습니다.";
    
      try {
        const response = await fetch("/api/open-count", {
          cache: "no-cache",
          signal: controller.signal,
        });
        if (!response.ok) throw new Error("HTTP " + response.status);
        const data = await response.json();
        if (!Number.isSafeInteger(data.count) || data.count < 0) {
          throw new Error("건수 형식이 올바르지 않습니다.");
        }
        if (mine !== revision) return;
        countNode.textContent = String(data.count);
        stateNode.textContent = "건수 확인을 마쳤습니다.";
      } catch (error) {
        if (mine !== revision || error.name === "AbortError") return;
        stateNode.textContent = "갱신하지 못했습니다. 이전 건수입니다.";
      } finally {
        if (mine === revision) activeRequest = null;
      }
    }
    
    window.addEventListener("pageshow", (event) => {
      if (event.persisted) void refreshCount();
    });
    
    window.addEventListener("pagehide", () => {
      revision += 1;
      activeRequest?.abort();
      activeRequest = null;
    });
    

    이 코드의 갱신 요청은 HTTP 캐시가 있으면 검증을 거쳐 사용할 수 있도록 no-cache 모드를 선택했습니다. 서버의 검증자와 캐시 설정이 올바르고, 서비스 워커가 별도 응답을 반환하지 않는다는 전제가 필요합니다. bfcache에서 복원됐다는 사실만으로 이 API까지 최신이라고 취급하지 않습니다. fetch의 캐시 모드

    응답이 성공했다고 곧바로 화면을 바꾸지 않고 건수가 음수가 아닌 안전한 정수인지 확인합니다. 값을 확인하지 못했을 때 0으로 대체하지도 않습니다. 0건과 조회 실패는 사용자에게 전혀 다른 의미이기 때문입니다.

    revision은 먼저 시작한 요청이 나중 요청의 결과를 덮어쓰는 일을 막기 위한 번호입니다. 페이지를 떠날 때도 번호를 바꾸고 요청을 중단하므로 이전 갱신의 결과가 뒤늦게 반영되는 것을 방지합니다. 이 예시는 복원 이벤트와 비동기 갱신을 연결하는 방법을 보여 주며 앱 전체의 상태 관리 구현을 대신하지는 않습니다.

    실패했을 때 이전 값이 무엇인지 알려야 합니다

    복원 직전의 건수가 12였고 서버에서 현재 9를 반환했다고 가정하면, 코드가 유효한 9를 받은 뒤 숫자만 바꿉니다. 목록 전체를 다시 만들지 않으므로 사용자가 펼쳐 둔 항목을 유지할 수 있습니다. 이 숫자는 설명을 위한 입력값이지 측정 결과가 아닙니다.

    네트워크가 끊겼다면 이전 숫자를 남기되 “갱신하지 못했습니다. 이전 건수입니다”라고 표시합니다. 갱신 중, 갱신 완료, 이전 값이라는 상태를 나눠야 사용자가 12를 지금의 확정값으로 오해하지 않습니다. 실제 앱에서는 재시도 버튼과 접근성 알림도 함께 검토합니다.

    그러나 모든 데이터에 이전 값 유지가 적절한 것은 아닙니다. 권한이나 로그인 상태, 민감한 개인정보처럼 확인 전 노출이 문제가 되는 화면은 해당 영역을 숨기거나 안전한 초기 상태로 두는 별도 설계가 필요합니다. 공개 건수 예시를 로그인 보호 코드로 그대로 사용해서는 안 됩니다.

    또한 pageshow 뒤에 검증 요청을 넣는 것만으로 이전 민감 정보가 어떤 순간에도 표시되지 않는다고 보장할 수 없습니다. 로그아웃 시 화면 상태 정리, 페이지 수명 주기, 서버의 매 요청 권한 검사와 실제 복원 화면 검증을 함께 다뤄야 합니다.

    no-store를 bfcache의 영구 차단 스위치로 쓰지 않습니다

    Cache-Control: no-store는 HTTP 캐시 저장 정책입니다. bfcache는 별도 기능이며 브라우저마다 적격성 판단이 달라질 수 있습니다. 특히 Chrome은 제한된 조건에서 no-store 페이지도 bfcache를 사용할 수 있도록 변경했다고 공식 문서에서 설명합니다. Chrome의 no-store 페이지 처리 변경

    따라서 오래된 설명만 보고 “no-store면 뒤로 가기에 절대로 복원되지 않는다”고 가정하지 않습니다. 반대로 성능을 위해 민감한 응답의 no-store를 무조건 제거하는 것도 적절하지 않습니다. 정보의 보관 요구와 해당 브라우저의 복원 조건을 따로 확인합니다.

    unload 이벤트를 억지로 붙여 복원을 막는 방식도 피합니다. unload는 신뢰할 수 없는 종료 신호이며 bfcache 사용을 방해할 수 있습니다. 떠나는 시점에 연결을 정리할 필요가 있다면 pagehide 등 수명 주기에 맞는 처리를 검토합니다. pagehide와 bfcache 호환성

    pagehide 역시 모바일에서 앱 전환 뒤 브라우저가 종료되는 상황 등에서는 오지 않을 수 있습니다. 이 이벤트 하나에 중요한 데이터 저장이나 로그아웃의 완결성을 맡기면 안 됩니다. 복원 최적화와 데이터 보존 보장은 별개입니다.

    검사 도구의 성공과 실제 사용자 흐름을 함께 봅니다

    Chrome DevTools의 Application 영역에는 Back/forward cache 검사 기능이 있습니다. 해당 페이지를 떠났다가 돌아오며 복원 가능한지 확인하고, 실패하면 원인을 보여 줍니다. 화면 구성과 명칭은 버전에 따라 다를 수 있습니다. DevTools 검사 절차

    보고된 원인이 페이지에서 수정 가능한 것인지, 브라우저 지원이나 외부 조건 때문에 생긴 것인지 나눠 봅니다. iframe이 있는 경우 어떤 프레임이 원인인지도 확인합니다. 모든 실패를 앱 코드 한 줄로 해결할 수 있는 것은 아닙니다.

    검사 한 번의 성공은 모든 방문에서 bfcache가 사용된다는 보증이 아닙니다. 페이지가 메모리에 무기한 남는 것도 아닙니다. 실제 목표 브라우저에서 목록→상세→뒤로 가기, 데이터 변경 뒤 복원, 네트워크 실패 뒤 복원을 각각 확인해야 합니다.

    마지막 판단은 화면 전체를 무조건 새로고침할지의 선택으로 끝내지 않습니다. 사용자가 돌아오려던 자리와 입력은 보존하고, 현재성이 필요한 데이터는 갱신 상태를 분명히 하며 다시 확인하는 것이 목표입니다. 복원 여부를 관찰하고 데이터별 갱신 기준을 정하면 빠른 뒤로 가기와 정확한 화면을 함께 설계할 수 있습니다.

    728x90
    반응형
Designed by Tistory.