크롬 bfcache 제외 기준과 디버깅

share · 2026-4-10

← 리스트로

정말 공감 가는 부분입니다. Safari는 예전부터 "어지간하면 다 메모리에 때려 박고 본다"는 느낌으로 매우 공격적인(Aggressive) bfcache 정책을 써왔기 때문에 뒤로가기를 눌렀을 때 마법처럼 빠릿빠릿하게 작동하죠.

반면 Chrome(Blink 엔진)은 비교적 늦게 bfcache를 본격적으로 도입하면서 **“조금이라도 페이지 상태가 꼬이거나 보안 문제가 생길 여지가 있다면 절대 캐싱하지 않는다”**는 매우 보수적인(Conservative) 철학을 채택했습니다.

크롬이 공식적으로 삼고 있는 기준과, 그럼에도 불구하고 왜 진우님께 그 기준이 오락가락하고 불명확하게 느껴지는지 정리해 드릴게요.


1. Chrome의 명시적인 bfcache 제외 기준

개발자가 작성한 코드 중에서 다음 중 하나라도 해당하면 크롬은 해당 페이지를 bfcache에 넣지 않습니다.

  • unload 이벤트 리스너의 존재: 가장 흔한 원인입니다. 크롬은 unload 이벤트가 등록되어 있으면 해당 페이지가 파기될 때 무언가 중요한 작업을 한다고 간주하고 캐싱하지 않습니다. (대신 pagehidevisibilitychange를 사용해야 합니다.)
  • 완료되지 않은 네트워크 요청: fetchXMLHttpRequest 요청이 아직 진행 중(Pending)인 상태에서 페이지를 떠나면 캐싱되지 않습니다.
  • 활성화된 실시간 연결: WebSocket, WebRTC, Server-Sent Events(SSE)가 열려있는 상태로 유지되고 있으면 제외됩니다.
  • 열려있는 IndexedDB 트랜잭션: 트랜잭션이 완전히 닫히지 않은 상태면 데이터 무결성을 위해 캐싱을 포기합니다.
  • window.opener 참조: 다른 창에서 window.open()으로 열린 창이 원본 창의 참조를 가지고 있는 경우 보안 및 컨텍스트 충돌 방지를 위해 제외됩니다.

2. 기준이 '불명확하고 맘대로’라고 느껴지는 진짜 이유

코드를 완벽하게 최적화하고 위의 기준을 다 맞췄는데도 어떨 때는 되고 어떨 때는 안 되는 것처럼 보이는 이유는, 프론트엔드 코드 외적인 변수들이 너무 많기 때문입니다.

  • 시스템 메모리 압박 (Memory Pressure): 크롬은 사용자의 디바이스(특히 모바일이나 램이 적은 PC) 메모리 상태를 실시간으로 모니터링합니다. 가용 메모리가 부족해지면 가장 먼저 조용히 날려버리는 것이 bfcache입니다. 즉, 어제는 캐싱이 잘 됐는데 오늘은 사용자가 크롬 탭을 30개 열어놨다는 이유만으로 캐싱이 안 될 수 있습니다.
  • 크롬 익스텐션 (Extensions)의 방해: 이 부분이 가장 골치 아픕니다. 내 코드는 깨끗해도, 사용자가 설치한 특정 크롬 익스텐션이 페이지에 강제로 unload 이벤트를 주입하거나 백그라운드와 통신하는 연결을 쥐고 있으면 bfcache가 깨집니다. (시크릿 모드에서는 잘 되는데 일반 모드에서는 안 된다면 100% 이 경우입니다.)
  • 서드파티 스크립트의 통제 불가능성: 특히 뉴스나 미디어 사이트처럼 광고 모듈, 애널리틱스, 각종 트래커가 무겁게 덕지덕지 붙는 환경에서는 이 문제가 극대화됩니다. 내가 짠 앱 코드에서는 pagehide를 잘 썼더라도, 외부 광고 스크립트가 구식 unload를 쓰거나 비동기 요청을 물고 늘어지면 페이지 전체의 캐싱이 무효화됩니다.
  • Cache-Control: no-store의 정책 변화: 과거 크롬은 HTTP 헤더에 no-store가 있으면 bfcache를 비활성화했습니다. 하지만 최근(Chrome 86+)에는 "HTTP 캐시와 메모리 스냅샷(bfcache)은 다르다"며 이를 캐싱하는 쪽으로 스펙을 변경했는데, 브라우저 버전이나 세부 상황에 따라 동작이 달라지기도 합니다.

💡 확실한 디버깅 방법

이 불명확함을 그나마 확실하게 눈으로 확인하는 방법이 있습니다. 크롬 개발자 도구를 활용하는 것입니다.

  1. 크롬 개발자 도구(F12)를 엽니다.
  2. Application 탭으로 이동합니다.
  3. 좌측 메뉴의 Background services 아래에 있는 Back/forward cache를 클릭합니다.
  4. 하단의 [Test bfcache] 버튼을 누릅니다.

크롬이 페이지를 닫았다가 다시 열어보는 시뮬레이션을 돌린 후, 정확히 어떤 스크립트의 몇 번째 라인 때문에(혹은 어떤 익스텐션 때문에) bfcache가 실패했는지 빨간 글씨로 족집게처럼 짚어줍니다. 최적화를 진행하실 때 이 탭을 켜두고 원인을 하나씩 소거해 나가는 방식을 추천합니다.

정말 예리하게 보셨습니다. 말씀하신 것처럼 굵직한 네트워크나 이벤트 리스너 문제 외에도, DOM에 존재하는 특정 엘리먼트나 브라우저 API가 **“조용한 암살자”**처럼 bfcache를 무력화하는 경우가 수두룩합니다.

특히 단순한 텍스트 위주의 페이지가 아니라, 미디어 요소가 많고 다양한 서드파티 모듈이 붙는 복잡한 페이지(예를 들어, 뉴스 기사 본문이나 콘텐츠 포털 같은 환경)에서는 이런 자잘한 요소들이 가장 큰 골칫거리가 됩니다. 프론트엔드 레벨에서 상태 관리 구조나 렌더링을 아무리 가볍고 정교하게 최적화해 두어도, 브라우저 스펙 레벨에서 막혀버리니까요.

대표적으로 언급하신 두 가지와, 흔히 놓치는 자잘한 함정들을 짚어보겠습니다.

1. <iframe> : 연대 책임 (Guilt by Association)

iframe은 페이지 안에 또 다른 페이지를 품고 있는 구조이기 때문에, 크롬은 **“iframe 내부의 문서가 bfcache 조건을 만족하지 못하면, 부모 페이지도 캐싱하지 않는다”**는 연대 책임 룰을 적용합니다.

  • 크로스 오리진(Cross-Origin) iframe: 광고 네트워크 태그, 유튜브 임베드, 소셜 미디어 위젯 등이 여기에 해당합니다. 내가 짠 부모 페이지의 코드는 완벽해도, iframe으로 불러온 외부 광고 스크립트가 구식 unload 이벤트를 쓰고 있거나 닫히지 않은 커넥션을 쥐고 있으면 부모 페이지 전체의 bfcache가 날아갑니다. 프론트엔드 개발자 입장에서 외부 스크립트에 대한 제어권이 없기 때문에 가장 억울하고 답답한 케이스입니다.

2. <video><audio> : 리소스 점유 문제

단순히 태그가 존재하는 것만으로는 괜찮을 수 있지만, 미디어의 상태에 따라 크롬이 매우 예민하게 반응합니다.

  • 재생 중이거나 버퍼링 중인 경우: 미디어가 재생 중이거나 네트워크를 통해 청크를 다운로드(버퍼링)하고 있는 상태에서 페이지를 떠나면 캐싱되지 않습니다. 크롬은 보이지 않는 백그라운드 페이지가 하드웨어 디코더나 네트워크 리소스를 계속 점유하는 것을 막으려 합니다.
  • 카메라/마이크 접근 (getUserMedia): 미디어 스트림이 활성화되어 하드웨어를 점유하고 있으면 보안 및 개인정보 보호를 위해 절대 캐싱하지 않습니다.
  • DRM (Encrypted Media Extensions): 저작권 보호가 걸려있는 미디어를 처리 중인 경우에도 보안 컨텍스트 문제로 캐싱에서 제외될 확률이 높습니다.

3. 그 외 프론트엔드를 괴롭히는 자잘한 API들

최신 웹 API들을 적극적으로 사용할수록 bfcache의 지뢰밭을 걷게 될 확률이 높습니다.

  • BroadcastChannel 통신: 탭 간 상태 동기화 등을 위해 BroadcastChannel을 열어두고 닫지 않은 상태로 떠나면 캐시되지 않습니다. (복잡한 상태 관리 로직에서 흔히 놓치는 부분입니다.)
  • Web Locks API (navigator.locks): 자원 충돌을 막기 위해 락을 걸어둔 상태로 페이지를 벗어나면 데드락 방지를 위해 페이지를 파기합니다.
  • 진행 중인 다운로드: a 태그의 download 속성 등으로 파일 다운로드가 백그라운드에서 진행 중인 상태로 페이지를 벗어나면 캐싱이 취소됩니다.
  • SpeechSynthesis (TTS) API: 브라우저가 음성을 읽어주고 있는 도중에 뒤로가기를 누르면 동작이 꼬이는 것을 막기 위해 캐싱이 깨집니다.

💡 현실적인 대응 방안

이런 세세하고 까다로운 기준들 때문에 복잡한 프로덕트 환경에서는 "어차피 크롬에선 bfcache를 완벽하게 제어하기 힘들다"고 포기하기 쉬운데요. 이런 “자잘한” 요소들로 인한 캐시 누수를 막기 위해, 페이지를 떠나는 순간(pagehide 이벤트 발생 시)에 적극적인 클린업(Cleanup) 을 해주는 것이 좋습니다.

  • pagehide가 발생할 때 재생 중인 모든 <video>, <audio> 엘리먼트를 찾아서 강제로 pause() 시키기.
  • BroadcastChannel 이나 열려있는 연결 인스턴스에 명시적으로 close() 호출하기.
  • 필요하다면 문제의 원인이 되는 통제 불가능한 서드파티 iframe을 DOM에서 통째로 제거해 버리기. (이후 사용자가 뒤로가기로 돌아와 pageshow 이벤트가 발생하면 다시 렌더링)

결국 크롬의 bfcache는 "우리가 알아서 마법처럼 캐싱해 줄게"가 아니라, **“캐싱을 방해할 만한 요소들을 네가 다 깔끔하게 정리하고 나가면 캐싱해 줄게”**에 가깝습니다.

네, 정확하게 짚으셨습니다. 사실상 **“조금이라도 브라우저가 복구하기에 리스크가 있거나 귀찮은 상황”**이면 크롬은 가차 없이 bfcache를 포기해 버립니다.

진우님 말씀대로 페이지가 무겁거나 복잡할 때 '삐그덕’하는 현상은 크게 세 가지 관점에서 이해할 수 있습니다.


1. “메모리 점유율이 깡패” (Resource Pressure)

브라우저 입장에서 bfcache는 일종의 **‘도박’**입니다. 사용자가 뒤로 올지 안 올지 모르는 상황에서 현재 페이지의 모든 메모리 스냅샷을 들고 있는 거니까요.

  • Heap Size의 압박: 페이지에 올라온 JS 객체가 너무 많거나, 메모리 누수(Memory Leak) 기미가 보여서 힙 메모리 사용량이 일정 수준을 넘어가면 크롬은 해당 페이지를 bfcache 대상에서 우선순위로 밀어냅니다.
  • 시스템 전체 상황: 이건 내 페이지 잘못이 아닐 때가 많습니다. 사용자가 다른 탭을 너무 많이 띄워놔서 브라우저 전체 가용 메모리가 부족해지면, 크롬은 "미래의 뒤로가기 속도"보다는 "현재 탭의 안정성"을 위해 캐싱된 메모리를 가장 먼저 해제(Eviction)해 버립니다.

2. “너무 복잡한 스크립트 상태” (State Complexity)

스크립트가 복잡하다는 건 브라우저가 ‘얼려놓기(Freezing)’ 힘든 상태라는 뜻이기도 합니다.

  • 무한 루프성 setInterval: 멈추지 않고 계속 돌아가는 타이머가 너무 많으면, 브라우저는 이 실행 맥락을 일시정지했다가 나중에 완벽하게 복구하는 것에 부담을 느낍니다.
  • 비동기 체인의 늪: 수많은 Promise가 얽혀 있고, 백그라운드에서 계속 무언가를 계산하거나 상태를 업데이트하려는 시도가 잦은 무거운 라이브러리들이 돌아가고 있으면 캐싱 확률이 뚝 떨어집니다.

3. “브라우저 API의 지뢰밭” (Fragile APIs)

말씀하신 것처럼 특정 API들은 그 자체로 ‘bfcache 금지어’ 수준입니다. 브라우저 엔진이 "이건 내가 나중에 다시 돌아왔을 때 똑같은 상태로 되돌려줄 자신이 없어!"라고 선언하는 부분들이죠.

카테고리 대표적인 ‘삐그덕’ 요소 (Blocking factors)
통신 WebSocket, WebRTC, SSE, 아직 응답 안 온 fetch
저장소 IndexedDB 트랜잭션 중, FileSystem API 사용 중
디바이스 Web Bluetooth, Web Serial, Web USB (하드웨어 연결)
기타 SharedWorker, ServiceWorker와의 복잡한 메시징

🧐 왜 유독 크롬만 이럴까? (vs Safari)

Safari는 시스템 리소스를 굉장히 공격적으로 활용해서라도 사용자 경험(빠른 전환)을 챙기는 편이지만, 크롬은 **“멀티 프로세스 아키텍처”**와 **“보안 격리(Site Isolation)”**를 엄청나게 중요하게 여깁니다.

조금이라도 프로세스 간 상태가 꼬일 것 같거나(예: iframe과 부모 창의 상태 불일치), 메모리 스냅샷을 보관했다가 다시 꺼낼 때 보안 취약점이 생길 것 같으면 그냥 **“새로 고침이 낫다”**고 판단해 버리는 거죠.

💡 결론적으로

진우님이 느끼신 그 '명확하지 않음’은 사실 브라우저의 실시간 컨디션(메모리) + 내가 통제 못 하는 외부 스크립트 + 브라우저의 보수적인 정책이 섞인 결과물입니다.

그래서 복잡한 라이브러리를 직접 만드실 때나 프로젝트를 하실 때는, 크롬의 bfcache를 100% 믿기보다는 pageshow 이벤트의 persisted 속성을 체크해서 "캐시로 복구됐을 때 다시 살려야 할 로직"을 수동으로 관리해 주는 게 가장 확실한 방법이 되곤 합니다.

맞아요, 그게 실무에서 기획자나 PM분들과 소통할 때 가장 골치 아픈 지점이죠. 개발자 입장에서는 "코드를 잘 짰으니 무조건 빨라야 한다"는 확답을 주고 싶지만, 브라우저의 **bfcache는 '약속’이 아니라 ‘제안’**에 가깝거든요.

이걸 설득하실 때는 "우리가 통제할 수 있는 영역"과 "브라우저가 통제하는 영역"을 나누어 **‘비결정론적 특성’**을 강조하시는 게 좋습니다. 기획팀에 설명하실 때 참고할 만한 논리를 정리해 드릴게요.


1. “사용자마다, 기기마다 결과가 다를 수밖에 없습니다”

가장 명확한 근거는 디바이스의 자원 상태입니다.

  • 메모리 가변성: “최신 아이폰이나 고사양 PC를 쓰는 사용자는 90% 확률로 작동하겠지만, 보급형 기기를 쓰거나 탭을 수십 개 띄워놓은 사용자는 브라우저가 메모리 확보를 위해 캐시를 스스로 파기합니다. 즉, 우리 서비스가 아니라 사용자 환경이 결정권을 쥐고 있습니다.”
  • 브라우저의 변덕: “Chrome은 구글의 업데이트 주기에 따라 캐싱 기준(예: 특정 API 허용 여부)을 수시로 변경합니다. 오늘 되는 게 내일 크롬 업데이트 후에 안 될 수도 있는 구조입니다.”

2. “서드파티(외부 스크립트)라는 통제 불능 변수”

프로젝트 규모가 클수록 광고, 분석 도구(GA 등), 고객 상담 챗봇 등 외부 스크립트가 많이 붙는데요.

  • 연대 책임 룰: “우리 코드는 bfcache를 지원하도록 완벽하게 짰더라도, 서비스에 붙어있는 광고 모듈이나 외부 라이브러리 중 단 하나라도 '구식 방식’을 쓰고 있으면 브라우저는 페이지 전체를 캐싱 대상에서 제외합니다. 이 외부 스크립트들을 우리가 일일이 수정할 수 없기 때문에 100% 보장은 불가능합니다.”

3. 기획/PM을 위한 “현실적인 기대치” 가이드

대화의 결론을 **'작동 여부’가 아니라 ‘최적화 확률’**로 유도해 보세요.

“bfcache는 보너스 같은 기능입니다.” “우리는 이 기능이 작동할 '확률’을 높이기 위해 기술적인 조치(unload 제거, pagehide 활용 등)를 다 할 것입니다. 하지만 브라우저 정책상 100% 보장되는 기능이 아니므로, 기획 단계에서는 **‘운 좋으면 즉시 뜨고, 아니면 일반 로딩’**이 일어난다고 가정하는 것이 가장 안전합니다.”


💡 기술적 ‘팩트’ 체크 (확신을 주기 위한 도구)

만약 "진짜 안 되는 게 맞냐"라고 날카롭게 물어본다면, 구글 크롬 개발자 문서의 철학을 인용해 보세요.

  • Eviction(파기)은 브라우저 마음: 크롬 공식 문서에서도 **“Browsers may decide not to cache a page for various reasons… such as memory pressure”**라고 명시하고 있습니다.
  • Safari vs Chrome: Safari는 사용자 경험을 위해 메모리를 과하게 써서라도 캐싱을 밀어붙이는 편이고, Chrome은 안정성과 보안을 위해 조금만 의심쩍어도 캐시를 버리는 정책을 취한다는 점을 강조하시면 됩니다.

결론적으로 진우님이 느끼시는 **“자기 마음대로고 명확하지 않다”**는 판단이 기술적으로 정확한 팩트입니다. 안 되는 걸 된다고 했다가 나중에 "왜 여기선 안 돼요?"라는 질문을 받으면 더 곤란해지니, 지금처럼 "브라우저가 판단하는 조건부 기능"임을 명확히 선을 그어두시는 게 훨씬 전략적인 커뮤니케이션입니다.