34.1 오프라인에서도 앱이 떠야 할까요?

지하철 터널, 엘리베이터, 지하 주차장, 비행기. 인터넷이 뚝 끊기는 순간은 생각보다 자주 와요. 일반 웹사이트는 이때 공룡 게임이나 "인터넷 없음" 오류 화면을 띄우죠. PWA(Progressive Web App, 앱처럼 동작하는 웹)가 특별한 건 바로 이 순간에도 내 화면을 보여줄 수 있다는 거예요.


앞에서 서비스워커(service worker, 브라우저 뒤에서 도는 작은 스크립트)가 요청을 가로채고 캐시에서 응답을 줄 수 있다고 했죠. 오프라인 지원은 이 능력을 "네트워크가 죽었을 때"에 집중해서 쓰는 거예요. 핵심 질문은 딱 하나예요. "요청이 실패하면, 대신 뭘 보여줄까?"

34.2 인터넷이 끊기면 fetch는 어떻게 되나요?

서비스워커 안에서 fetch(네트워크 요청)를 호출했는데 인터넷이 없으면, 이 요청은 성공하지 못하고 거부(reject, 실패로 끝남)돼요. 자바스크립트에선 이게 catch로 잡히죠. 서버가 404(찾을 수 없음)나 500(서버 오류) 같은 코드를 준 게 아니라, 요청이 네트워크에 닿지도 못한 상황이에요.


fetch(e.request)
.then((res) => res)
.catch(() => {
// 여기가 오프라인일 때 들어오는 자리예요
});


바로 이 catch 자리가 오프라인 지원의 무대예요. 여기서 미리 저장해둔 대체 화면을 꺼내 주면, 사용자는 브라우저 기본 오류 대신 우리가 준비한 페이지를 보게 돼요. 반대로 catch를 비워두면 예전과 똑같은 오류 화면이 뜨고요. 그러니 이 자리를 어떻게 채우느냐가 관건이에요.

34.3 오프라인 대비 페이지를 어떻게 챙기죠?

catch가 발동한 그 순간엔 이미 네트워크가 없어요. 그러니 대체 페이지는 미리 저장돼 있어야 해요. 인터넷이 살아 있을 때, 즉 서비스워커 install(설치) 시점에 챙겨두는 거죠.


const OFFLINE = '/offline.html';
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open('v1').then((c) => c.addAll([
'/', '/style.css', '/app.js', OFFLINE
]))
);
});


여기서 offline.html은 "지금 오프라인이에요, 연결되면 다시 보여드릴게요" 같은 안내를 담은 단순한 페이지예요. waitUntil은 "이 저장이 끝날 때까지 설치를 완료로 치지 마"라고 브라우저에 부탁하는 거고요. addAll은 목록의 파일을 전부 받아서 한꺼번에 캐시에 넣어줘요. 이 파일들만큼은 무슨 일이 있어도 창고에 들어가 있어야 하니, 설치 목록에 빠짐없이 적어둬요.

34.4 요청이 실패하면 뭘 대신 보여줄까요?

이제 fetch 이벤트에서 상황을 나눠요. 특히 페이지 이동(navigation, 주소창으로 새 페이지를 여는 요청)이 실패했을 때 offline.html을 돌려주는 게 핵심이에요.


self.addEventListener('fetch', (e) => {
if (e.request.mode === 'navigate') {
e.respondWith(
fetch(e.request).catch(() => caches.match(OFFLINE))
);
return;
}
e.respondWith(
caches.match(e.request).then((hit) => hit || fetch(e.request))
);
});


request.mode === 'navigate'는 "사용자가 새 페이지로 이동하려는 요청"이라는 표시예요. 이게 네트워크에서 실패하면 catch가 offline.html을 꺼내 주죠. 그 아래 respondWith는 페이지가 아닌 나머지 파일들을 캐시 우선으로 처리해요. 이미 저장된 CSS나 스크립트는 오프라인에서도 캐시에서 바로 나오고요.


결과는 이래요. 터널에 들어가 새 글 링크를 눌러도, 브라우저 기본 오류 화면 대신 우리가 만든 안내 페이지가 떠요. 사용자는 "아, 지금 인터넷이 없구나"를 깔끔한 화면으로 알게 되죠. 앱이 완전히 죽지 않았다는 인상만 줘도 신뢰가 올라가요.

34.5 이미지가 없을 땐 어떻게 하죠?

페이지 말고 이미지 하나가 오프라인이라 안 뜨는 경우도 있어요. 이럴 땐 깨진 이미지 아이콘 대신 대체 이미지를 준비해두면 화면이 덜 흉해요.


if (e.request.destination === 'image') {
e.respondWith(
caches.match(e.request).then((hit) =>
hit || fetch(e.request).catch(() => caches.match('/fallback.png'))
)
);
return;
}


request.destination은 이 요청이 무엇을 위한 것인지 알려줘요. 'image'면 이미지, 'style'이면 CSS, 'script'면 자바스크립트, 'font'면 글꼴이죠. 이걸 보고 종류별로 다른 대체품을 줄 수 있어요. 물론 fallback.png도 install 때 미리 캐시에 넣어둬야 해요. 결과적으로 사진이 안 떠도 회색 자리표시 이미지가 대신 채워져서 레이아웃이 무너지지 않아요.

34.6 지금 온라인인지 오프라인인지 알 수 있나요?

서비스워커 말고 일반 페이지 쪽에서도 연결 상태를 챙기면 좋아요. 브라우저는 navigator.onLine이라는 값을 주는데, 온라인이면 true, 오프라인이면 false예요. 그리고 상태가 바뀌는 순간을 알려주는 online, offline 이벤트도 있어요.


window.addEventListener('offline', () => {
document.body.classList.add('is-offline');
});
window.addEventListener('online', () => {
document.body.classList.remove('is-offline');
});


결과는 이래요. 인터넷이 끊기면 화면 위쪽에 "오프라인 상태예요" 같은 띠 배너를 슬쩍 띄웠다가, 다시 연결되면 감출 수 있어요. 다만 navigator.onLine은 와이파이는 잡혔는데 정작 인터넷은 안 되는 경우까지 잡아내진 못해요. 정확한 판단은 결국 fetch의 성공, 실패로 확인하는 게 맞아요.

34.7 오래된 캐시는 언제 치우나요?

오프라인 지원엔 그림자가 하나 있어요. 파일을 고쳐서 새로 배포해도 옛 캐시가 남아 있으면 사용자는 계속 옛 화면을 봐요. 그래서 캐시 이름에 버전(v1, v2)을 붙이고, 새 버전이 켜질 때 옛 버전을 지워요. 이건 서비스워커의 activate(활성화) 이벤트에서 해요.


self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((names) =>
Promise.all(
names.filter((n) => n !== 'v1')
.map((n) => caches.delete(n))
)
)
);
});


caches.keys()는 저장된 캐시 이름을 전부 가져와요. 그중 지금 버전(v1)이 아닌 것만 골라 caches.delete로 지우는 거죠. 덕분에 낡은 파일이 저장소에 무한정 쌓이지 않아요. 다음 배포 땐 캐시 이름을 v2로 바꾸기만 하면 되고요.

34.8 그래서 오프라인 지원, 어떻게 정리할까요?

큰 그림은 세 박자예요. install에서 대체 페이지와 이미지를 미리 저장하고, fetch의 catch에서 실패한 요청에 대체품을 응답하고, activate에서 낡은 캐시를 청소해요. 여기에 online, offline 이벤트로 상태 안내까지 얹으면 경험이 한결 부드러워지죠.


머리로만 읽으면 금방 헷갈려요. 개발자 도구의 Network 탭에는 Offline 체크박스가 있어요. 그걸 켜고 새로고침해서 내 offline.html이 뜨는지, 대체 이미지가 자리를 채우는지 직접 확인해보세요. Application 탭의 Cache Storage에서 어떤 파일이 저장됐는지도 눈으로 볼 수 있고요. 이 맛을 한 번 보면 PWA가 왜 매력적인지 알게 될 거예요.