
사용자가 서비스를 사용할 때에는 기기 변수와 네트워크 변수가 존재합니다.
기기 변수는 사용자의 기기 자체를 바꿔줄 수는 없겠지만 저사양 기기를 대응하여 기기 성능에 대한 격차를 줄일 수 있고, 기기는 한번 바꾸면 자주 바꾸지 않는 특성 때문에 네트워크 변수에 비해 동일한 경험을 할 가능성이 높습니다.
하지만 네트워크 변수는 사용자가 접속하는 위치(예: 집, 직장, 지하철 등)와 네트워크 유형(와이파이, 4G/5G, 유선 등), 순간적인 네트워크 품질(지연, 속도, 끊김 등), 서비스 지역의 네트워크 인프라 상태 등 외부 요인에 따라 웹이나 앱의 로딩 시간, 인터랙션 반응성, 미디어 스트리밍 품질 등이 달라질 수 있기 때문에 매번 같은 경험을 하기 어려운 환경입니다.
엘리베이터, 건물 지하, 고속 이동 중 신호 약화 및 끊김
대중교통, 해외 등 다양한 지역/ISP별 네트워크 딜레이 및 속도 차이
서버 트래픽 급증으로 인한 네트워크 부하
와이파이 ↔ LTE 전환 시 발생하는 일시적 중단
사용자는 빠르고 편한 경험은 쉽게 잊어 버리지만, 느리거나 불편한 경험은 오래 기억에 남기 때문에, 한 사용자에게 계속 동일한 경험을 제공하기 위해서는 네트워크 변수를 최소화하는 것이 중요합니다.
[캐시 정책]
Cache-Control : no-cache 적용(매번 변경이 발생했는지 검증)Cache-Control : max-age:31536000 적용(1년 캐시)[배포 파이프라인]
[유저 흐름]
첫 요청 시 CDN 캐시가 없으므로, S3에서 자원들을 가져와 CDN에 캐싱을 한 후 응답 반환
응답 받은 리소스를 통해 화면을 그리고, JS 자원은 브라우저에 1년 캐싱 적용
재요청시 아직 변동사항이 없기 때문에 304 응답과 함께 기존 index.html 을 사용
index.html가 참조하는 JS 자원도 변동사항이 없기 때문에 브라우저 캐시 사용
(최신 배포 발생 ➔ 사용하는 자원 변경, CDN 캐시 무효화)
최신 배포 이후 재요청시 index.html이 변경되었기 때문에 다시 S3에서 새로 자원을 가져와 Body에 본문을 넣고 200 응답 코드와 함께 반환
index.html가 참조하는 JS 자원에 변경사항이 있고, CDN 캐시 무효화를 통해 CDN 캐시가 없어졌기 때문에 S3에서 새로 자원을 가져옴
가져온 자원들로 최신 배포 화면을 그림
이러한 과정에서 유저는 첫 요청, 최신 배포 후 첫 요청에서는 S3에서 자원을 가져오고, 그 이후에는 CDN/브라우저 캐싱을 사용하는 모습을 볼 수 있습니다.
이는 즉 첫 요청, 최신 배포후 첫 요청에서 크리티컬 리소스를 가져올 때 네트워크 변수가 작용한다는 것을 의미합니다.
만약 이 시점에서 네트워크가 느려진다면 유저는 아래 영상과 같이 약 7초가량 빈 화면을 보게 되는 안좋은 경험을 하게 됩니다.
📈 테스트 환경: 네트워크 3G

이러한 현상은 불러오는 자원의 개수가 많거나, 크기가 크거나 테스트 환경(3G)보다 네트워크가 더 좋지 않은 환경에서는 더욱 더 크게 작용될 것입니다.
이 문제를 개선하는 방법 중 하나인 SWR(Stale While Revalidate) 방식을 아래에서 소개하도록 하겠습니다.
요청이 발생했을 때 일단 이전에 사용하던 캐시 데이터를 사용자에게 보여주고, 동시에 백그라운드에서 최신 데이터를 가져와 사용자에게 빈 화면을 노출하는 경우를 최소화하는 전략입니다.
이러한 방식은 웹의 생명주기 단계에 벗어나 동작해야 하므로 서비스 워커를 활용해 구현하였습니다.
💡 서비스 워커(Service Worker)란?
웹 애플리케이션과 브라우저, 네트워크 사이에 위치하는 프록시 역할의 스크립트(JavaScript)입니다.
주로 오프라인 지원, 캐시 제어, 백그라운드 동기화, 푸시 알림 등의 기능을 제공합니다.
서비스 워커는 오프라인, 백그라운드 동기화(SWR)를 지원해 네트워크 변수를 최소화하기 적합하였습니다.
아래 영상에서는 요청이 발생했을 때 일단 서비스 워커에 있는 캐시를 꺼내서 사용하고, 백그라운드에서 새로운 자원들을 요청해 캐싱해둡니다.
이후 재요청이 발생하거나 배너의 업데이트 버튼을 클릭했을 때 최신 캐싱 데이터를 바꿔 끼우는 방식입니다.

네트워크 요청으로 인해 빈 화면이 노출되는 상황이 발생하지 않고, 백그라운드에서 요청이 이루어지기 때문에 네트워크 환경이 좋지 않더라도 계속 서비스를 사용할 수 있는 평등한 경험을 제공할 수 있습니다.
하지만 이 방식은 첫 요청 이후에 새로고침(페이지 이동)이나 사용자가 업데이트 배너의 버튼을 누르는 등의 행동이 없다면 최신 배포 버전을 보지 못하는 문제가 있습니다.
SPA + CSR 환경에서의 페이지 이동(React-Router 등의 사용)은 페이지 요청이 발생하지 않고 스크립트를 통해 화면을 바꾸기 때문에 페이지 요청이 발생하는 경우가 많지 않아 최신 배포 화면을 보지 못하는 문제가 더욱 더 크게 와닿을 수 있습니다.
즉, 사용자가 새로고침을 2번 눌러야 최신 배포 화면을 볼 수 있는 상황입니다.
백그라운드에서 최신 데이터를 가져오는 방식은 서비스 워커가 업데이트 되었는지 체크를 한 후, 업데이트 되었다면 최신 데이터를 요청하는 방식으로 동작하고 있습니다.
기본적으로 워커 업데이트 체크는 두가지 상황에서 발생합니다.
페이지 요청(탐색)
sw.js)가 기존과 바이트 단위로 다르면 새로운 버전이 있다고 판단하여 설치 프로세스로 진입합니다.24시간마다 자동 검사
➔ SWR 방식의 문제를 다시 정의하자면, 페이지 요청을 통해 워커 업데이트를 체크하고 있기 때문에 사용자가 새로고침을 2번 눌러야 최신 배포 화면을 볼 수 있었습니다.
하지만 워커 업데이트는 위 두 가지 방식 이외에도 registration.update() 와 같은 메서드를 명시적으로 호출하여 수동으로 업데이트를 체크하는 방식이 있습니다.
아래에서는 수동 업데이트 체크 방식을 활용하여 사용자에게 보다 더 빠른 최신 배포 화면을 제공할 수 있는 방법을 소개합니다.
위 방식과 가장 큰 차이점은 페이지 새로고침 등의 페이지 요청으로 워커 업데이트를 트리거할지, 사용자가 워커 업데이트를 트리거하지 않고 백그라운드로 주기적으로 체크하는지의 차이점이 있습니다.
기존 방식: 서비스 사용 중 새로고침 ➔ 캐싱 데이터 반환 및 백그라운드 데이터 요청 ➔ 업데이트 배너 클릭 ➔ 최신 배포 화면
개선 방식: 서비스 사용 중 업데이트가 있다면 배너 띄우기 ➔ 업데이트 배너 클릭 ➔ 최신 배포 화면
// 10분마다 워커 업데이트 체크
setInterval(() => {
registration.update();
}, 10 * 60 * 1000);
아래 영상에서는 이전과 다르게 새로고침 버튼을 클릭하지 않아도 백그라운드에서 워커 업데이트를 체크하고, 최신 데이터를 가져와 업데이트 배너를 제공하는 방식을 적용한 영상입니다.

SPA + CSR 환경에서는 페이지 요청이 자주 발생하지 않는 특징을 고려하여, 사용자에게 서비스를 사용 중에 최신 배포 업데이트를 확인할 수 있게 워커 업데이트를 주기적으로 체크하여 사용자 경험을 올릴 수 있었습니다.
이로 인해 사용자는 보다 더 빠르게 최신 배포 화면을 볼 수 있었지만, 이 방식에는 장점만 있는 것은 아니였습니다.
백그라운드에서 워커 업데이트를 주기적으로 체크하기 때문에 그만큼 CDN 요청 비용이 증가하게 됩니다.
만약 10분마다 워커 업데이트를 체크하면 1시간에 6번 요청을 보내게 되고, 서비스 사용자가 10,000명이라고 가정한다면 1시간에 워커 업데이트 요청만 60,000번 발생하게 됩니다.
➔ 자주 업데이트를 체크할수록 최신 배포 화면 노출을 빠르게 제공할 수 있지만, 과도한 비용과 서버 부하 발생합니다.
그렇다고 워커 업데이트 주기를 막 3시간으로 올리게 되면 사용자가 최신 배포 화면을 볼 수 있는 조건이 새로고침, 3시간 워커 업데이트 체크 으로 최신 배포 화면을 빠르게 볼 수 없는 환경이 되는 트레이드오프가 있습니다.
그렇기 때문에 저희는 워커 업데이트 주기를 무분별하게 올리는 것이 아니라 전략적으로 올리는 의사결정이 중요합니다.
워커 업데이트를 체크한다는 것은 즉, 최신 배포가 발생했는지 체크하는 것과 동일합니다.
서비스의 배포 주기가 빠르다면 그에 맞게 워커 업데이트 주기도 빠르게 가져가고, 배포 주기가 느린 경우에는 워커 업데이트 주기도 느리게 가져가 효율적으로 워커 업데이트 주기를 정할 수 있습니다.
또한 최신 배포를 비교적 느리게 보여줘도 된다는 비지니스적인 맥락이 있다면 워커 업데이트 주기를 느리게 가져가는 판단을 할 수도 있습니다.
이를 표로 정리하면 다음과 같습니다.
| 배포 주기 | 최신성 요구 | 워커 업데이트 주기 | 최신 배포 반영 속도 | CDN 비용 영향 | 비즈니스/운영 전략 |
|---|---|---|---|---|---|
| 빠름 | O | 빠름 (수~수십분) | 빠름 | 증가 | 실시간/최신 화면 즉시 노출 서비스 |
| 빠름 | X | 중간 (수시간) | 중간 | 적정 | 신선도 덜 중요, 비용 균형 서비스 |
| 느림 | O | 중간 (수시간) | 중간 | 적정 | 배포간 이슈 방지, 최신 보장 |
| 느림 | X | 느림 (수시간~하루) | 느림 | 감소 | 최신성 크게 불요, 비용 우선 서비스 |
이러한 기준들로 본인의 상황에 맞는 업데이트 주기 전략을 선택할 수 있습니다.
서비스 워커가 백그라운드에서 안정적으로 동작하며, 네트워크 요청 가로채기, 캐시 관리, skipWaiting, clients.claim 등 핵심 API들이 모두 정상 작동합니다.
사파리는 기술적으로 WebKit 엔진을 사용하며, 이 엔진에는 여러 서비스 워커 관련 제한점이 존재해 SWR 방식이나 주기적 워커 업데이트를 구현할 때 여러 제약을 받습니다. 주요 제한점은 아래와 같습니다.
WKWebView 지원 제한: iOS 14.5부터 일부 지원되지만, iOS 17 이전까지는 안정성과 기능 제한이 많아 앱 내장 웹뷰 환경에서 서비스 워커 활용이 어렵습니다.(최신 버전에서도 일부 제약이 남아 있습니다)
백그라운드 동작 제한: 사파리는 탭이 백그라운드로 전환되면 서비스 워커가 곧 종료되거나 네트워크 작업이 중단되어 비교적 불안정합니다.
clients.claim() 및 페이지 제어: clients.claim() 호출 후 즉시 제어권이 넘어가지 않고, 페이지 재로드가 필요할 수 있습니다.
⛔️ 서비스 워커를 사용해서 SWR 방식을 구현한다면 특히 사파리, WebKit 환경의 호환성을 확인하시는 것을 권장드립니다.
SWR(Stale-While-Revalidate) 방식은 본질적으로 클라이언트 사이드 렌더링(CSR)에 최적화된 전략으로, 사용자가 이전에 받은 캐시된 데이터를 우선 보여주는 동시에 백그라운드에서 최신 데이터를 가져와 갱신하는 방식입니다.
반면, 서버 사이드 렌더링(SSR) 환경은 서버가 클라이언트 요청 시점에 API를 호출해 최신 데이터를 반영한 HTML을 생성해 전송합니다. 즉, 서버가 렌더링 타임에 최신 데이터 상태를 반영하는 것이 핵심입니다.
➔ 이러한 차이 때문에 SSR 환경에서 SWR 방식을 HTML 문서 자체에 적용하는 것은 적합하지 않습니다.
예를 들어, 만약 30분 전에 생성된 index.html을 SWR 방식으로 백그라운드에서 캐싱해 재사용한다면, 그 HTML은 30분 전 API 데이터 상태를 반영한 화면이 됩니다. 만약 사용자가 카드 해지와 같은 상태 변경을 했음에도, 캐시되어 오래된 HTML을 받으면 실제 상태와 다른 잘못된 화면을 보게 될 수 있습니다.
따라서, SWR처럼 HTML 문서 단위에서 이전 캐시를 우선 보여주고 나중에 갱신하는 방식은 SSR의 시점 일관성과 맞지 않아 잘못된 화면 제공 위험이 큽니다.
SWR(Stale-While-Revalidate) 방식은 네트워크 요청으로 인한 빈 화면 노출을 줄이고, 연결 상태가 불안정한 환경에서도 일관된 사용자 경험을 보장하기 위한 전략입니다. 캐시된 데이터를 우선 제공하고 백그라운드에서 최신 데이터를 갱신함으로써 사용자에게는 빠른 반응성과 안정적인 초기 화면 표시 경험을 제공합니다.
그러나 CSR 환경에서는 클라이언트 단에서 API 호출로 화면이 갱신되기 때문에 SWR의 효과가 제한적입니다. 또한 서비스 워커의 업데이트 주기를 짧게 설정하면 최신 배포를 빠르게 반영할 수 있지만, 그만큼 CDN 요청 비용과 서버 부하가 증가합니다.
결국 이 방식의 핵심은 사용자 경험의 품질(신속한 업데이트, 안정적인 화면 표시)과 운영 비용(SWR 방식 적용, CDN 요청, 서버 트래픽)의 트레이드 오프 사이의 균형을 어디에 둘지 결정하는 데 있습니다.
배포 주기가 잦거나 최신성이 중요한 서비스라면 SWR 기반 배포 전략이 사용자에게 평등한 경험 제공하여 서비스 품질을 높이는 데 좋은 선택이지만, 배포 빈도가 낮거나 최신성이 덜 중요한 서비스라면 CDN·브라우저 캐싱 전략이 더 효율적일 수 있습니다.
즉, SWR(Stale-While-Revalidate) 배포 방식은 모든 서비스에 일괄적으로 적용할 전략이 아니라, 서비스의 배포 패턴과 비지니스 맥락에 맞춰 전략적으로 선택해야 하는 도구로 보는 것이 적절합니다.
기존에는 네트워크 요청이 발생하는 것을 당연하게 생각했었는데 Toss Makers Conference 25에서 토스뱅크 강현구님이 발표해주신 『언제나, 누구에게나, 평등하게 빠른 웹』 내용을 듣고 네트워크 변수를 0으로 만들려는 시도들에 대해 알게 되었고, 궁금한 부분들을 여쭤보았는데 커피챗을 통해 친절히 답변해주셔서 많은 깨달음을 얻을 수 있었습니다.
이후 저도 네트워크 변수를 최소화하여 사용자에게 평등한 경험을 제공하는 부분에 대해 공감을 하여 도전을 하게 되었고, 도전을 하는 과정에서 SPA + CSR 환경에서 SWR 방식의 문제점을 보완하며 주기적인 워커 업데이트 체크로 인한 CDN 요청 비용 문제, 서버 트래픽 부하 등의 트레이트 오프를 비지니스의 맥락에 맞는 전략적인 의사결정을 하여 사용자 경험을 개선하는 유의미한 경험들을 할 수 있었던 것 같습니다.
제 글에서는 주로 CSR 환경에서 네트워크 변수를 최소화하는 방법들에 대해 작성되어 있는데, 『언제나, 누구에게나, 평등하게 빠른 웹』 영상에서는 SSR 환경에서의 네트워크 변수 최소화하는 방법에 대해 소개하고 있어서 관심이 있으신 분들은 해당 영상을 보시는 것을 추천드립니다🙃
TMC25 | Engineering - 언제나, 누구에게나, 평등하게 빠른 웹
웹 서비스 캐시 똑똑하게 다루기
🌐 웹 브라우저의 Cache 전략 & 헤더 다루기
TTL none? CloudFront에서는 HIT인데 왜 캐시가 안 될까?
프론트엔드 서비스 최적화? 토스에서는 '이렇게' 합니다! | EP.9 모닥불
서비스 워커 캐싱 전략
서비스 워커의 수명주기
작업 상자 사전 캐싱
Workbox란 무엇인가요?
Workers at Your Service
This is a fantastic breakdown of how we can improve the user experience by minimizing network variables in a CSR environment! I especially appreciate how you highlighted the trade offs between SWR and CDN/browser caching, and the challenges of balancing user experience with operational costs. I’ve also worked with service workers before and noticed the limitations in environments like Safari it’s always a challenge to deal with those inconsistencies across different browsers.
I think a key point you made is how crucial it is to tailor these approaches depending on the service’s deployment cycle. The idea of adjusting the worker update cycle based on how quickly the service is updated is a smart strategy, and I agree that this approach should be implemented strategically rather than universally.
For businesses with less frequent deployments, CDN caching definitely makes more sense, but for services with real time updates, SWR can be a game changer. It would be interesting to hear more about any specific challenges you faced during implementation or any further optimizations that were made. Thanks for sharing your insights!