서비스 워커
고친 사람 github-actions[bot]
서비스 워커는 웹 페이지가 보내는 네트워크 요청을 중간에서 받아 대신 답합니다. 브라우저가 페이지와 따로 돌리는 작은 스크립트라서 페이지를 닫아도 남아 있습니다. 저장해 둔 응답을 돌려주면 네트워크가 끊긴 채로도 화면이 뜹니다.
쉽고 빠른 이해
서비스 워커는 페이지와 네트워크 사이에 서서 요청을 가로채는 스크립트입니다. 지하철에서 신호가 끊겼는데도 조금 전에 보던 화면이 다시 열린다면 이 스크립트가 일한 것입니다.
이게 없으면 브라우저는 요청할 때마다 서버까지 다녀와야 합니다. 신호가 약하면 다녀올 곳이 없어 화면이 빈 채로 멈춥니다. 서버가 죽으면 아무것도 못 보여 줍니다.
어떻게 도는가:
- 페이지가 한 번 등록해 두면 브라우저가 이 스크립트를 따로 설치합니다
- 그 뒤로 같은 사이트가 보내는 요청은 전부 이 스크립트를 먼저 거칩니다
- 스크립트는 저장해 둔 응답을 돌려주거나, 없으면 네트워크로 내보냅니다
대가도 있습니다. 한 번 저장한 응답은 저절로 새것이 되지 않습니다. 언제 버리고 다시 받을지를 사람이 정해 두지 않으면 방문자가 낡은 화면을 계속 보게 됩니다.
상세
사무실 정문에 접수 담당자를 한 명 세워 둔다고 해 봅니다. 밖으로 나가는 심부름을 이 사람이 먼저 받습니다. 이미 받아 둔 서류면 바로 건네주고, 없을 때만 밖으로 나갑니다.
브라우저 안에서 그 접수 담당자 노릇을 하는 것이 서비스 워커입니다. 다만 사람과 달리 한가할 때는 브라우저가 이 스크립트를 재워 두고, 요청이 올 때만 깨웁니다.
접수 담당자가 없는 사무실이라면 심부름마다 사람이 밖으로 나가야 합니다. 브라우저도 같아서, 서비스 워커를 안 얹으면 요청 하나하나가 서버까지 갑니다. 서버가 멀거나 신호가 나쁘면 그 왕복이 곧 방문자가 기다리는 시간입니다.
페이지와 따로 도는 스크립트
웹 페이지에 실린 자바스크립트는 그 페이지가 열려 있는 동안에만 삽니다. 탭을 닫으면 같이 사라집니다. 화면을 그리는 일과 같은 줄에 서서 번갈아 돕니다.
서비스 워커는 그 줄 밖에서 돕니다. 브라우저가 실행 환경을 따로 하나 띄우고 그 안에서 이 스크립트를 굴립니다. 그래서 페이지가 바쁘거나 탭이 전부 닫혀 있어도 서비스 워커는 살아 있을 수 있습니다.
대신 화면을 직접 건드리지 못합니다. 페이지의 요소를 객체로 다루는 DOM(Document Object Model, 문서 객체 모델)은 페이지가 열려 있는 쪽에만 있어서, 서비스 워커는 버튼 하나 바꾸지 못합니다. 화면을 고쳐야 하면 페이지에 메시지를 보내 대신 시킵니다.
둘이 각각 어느 칸에 앉아 있는지를 그림으로 보면 이렇습니다. 화면으로 가는 화살표가 페이지 칸 안에서만 나오는 것이 이 소절의 요지입니다.
flowchart TD
subgraph PAGE["페이지(탭)"]
JS["페이지의 자바스크립트"] -->|화면을 고친다| DOM["DOM"]
end
subgraph WORKER["서비스 워커"]
S["서비스 워커 스크립트"]
end
S -->|메시지| JS
S -->|요청| NET["네트워크"]
요청을 가로채는 방법
서비스 워커가 하는 일의 본체는 fetch 이벤트를 받는 것입니다. 페이지가 이미지 하나를
불러오려 하면 브라우저는 곧장 서버로 가지 않고, 먼저 이 이벤트를 서비스 워커에게 던집니다.
서비스 워커는 그 요청에 응답 하나를 돌려주면 됩니다. 응답이 어디서 났는지는 브라우저가 묻지 않습니다. 저장해 둔 것이든, 방금 네트워크에서 받은 것이든, 코드로 지어낸 것이든 페이지는 그것을 서버의 답으로 받습니다.
저장해 둔 응답이 있으면 그걸 주고 없으면 네트워크로 나가는 코드는 이렇게 생겼습니다.
self.addEventListener('fetch', (event) => {
event.respondWith(answer(event.request));
});
async function answer(request) {
const hit = await caches.match(request);
if (hit) return hit; // 저장된 응답
return fetch(request); // 없으면 네트워크
}
fetch 라는 이름이 이 코드에 두 번 다른 뜻으로 나옵니다. 위쪽의 'fetch' 는 브라우저가
서비스 워커에게 던지는 이벤트의 이름입니다. 아래쪽의 fetch(request) 는 요청을 네트워크로
내보내는 함수입니다. 받는 쪽과 내보내는 쪽이 이름만 같습니다.
self 는 이 스크립트 자신을 가리킵니다. event.respondWith 는 브라우저에게 「이 응답으로
답하라」고 건네는 함수입니다. 거기에 넘긴 것이 페이지가 받는 답이 됩니다.
caches 는 브라우저가 서비스 워커에게 내주는 저장 공간입니다. 이 공간의 이름이
캐시 스토리지입니다. 요청을 열쇠로 삼고 응답을 값으로 삼아 한 벌씩 담아 둡니다.
브라우저가 원래 갖고 있는 브라우저 캐시와는 다른 공간입니다. 브라우저 캐시는 응답에 붙은 유효 기간을 보고 브라우저가 알아서 넣고 뺍니다. 캐시 스토리지는 무엇을 넣고 언제 뺄지를 코드가 정합니다.
어디까지 가로채는지는 범위가 정합니다. 스크립트 파일이 놓인 경로 아래의 요청만 서비스 워커를 거칩니다. 사이트 최상단에 두면 사이트 전체가 범위입니다. 하위 폴더에 두면 그 폴더 아래만 걸립니다.
저장해 둔 응답에는 유효 기간이 없습니다. 언제 버리고 다시 받을지는 코드가 정합니다. 그 판단을 빠뜨리면 서버의 파일을 고쳐도 방문자 화면은 옛것에 머뭅니다.
설치부터 활성까지
서비스 워커는 등록한 순간부터 요청을 가로채지 않습니다. 브라우저가 몇 단계를 거쳐 이 스크립트를 앞에 세웁니다.
페이지가 등록을 요청하면 브라우저는 스크립트 파일을 받아 설치를 시작합니다. 설치 단계는 미리 저장해 둘 파일을 긁어모으는 때입니다. 이 단계를 마치면 스크립트는 활성으로 넘어가고, 그때부터 요청을 받습니다.
이미 일하고 있는 서비스 워커가 있으면 새 스크립트는 곧바로 활성이 되지 못합니다. 새 스크립트는 대기 상태가 되어, 옛 스크립트를 쓰는 페이지가 전부 닫힐 때까지 기다립니다. 한 사이트의 두 탭이 서로 다른 스크립트를 쓰는 일을 막으려는 규칙입니다.
stateDiagram-v2
[*] --> 설치: 등록
설치 --> 활성: 처음 설치다
설치 --> 대기: 옛 스크립트가 남아 있다
대기 --> 활성: 옛 스크립트를 쓰는 탭이 다 닫혔다
이 대기 때문에 새로 올린 스크립트가 바로 안 도는 일이 흔합니다. 새로고침을 해도 옛 스크립트가 계속 답합니다. 탭을 모두 닫았다 다시 열거나, 코드에서 대기를 건너뛰라고 알려야 넘어갑니다.
보안 연결 요구
서비스 워커는 보안 연결에서만 돕니다. HTTPS(HyperText Transfer Protocol Secure)로 받은 사이트가 아니면 브라우저가 등록을 거절합니다. 요청을 가로채는 힘이 세서, 중간에서 페이지를 바꿔치기한 공격자에게 그 힘을 넘겨줄 수 없기 때문입니다.
자기 컴퓨터를 가리키는 localhost 는 개발용 예외로 보안 연결처럼 칩니다. 개발자가 인증서를
갖추지 않고도 손에서 돌려볼 수 있게 터 준 구멍입니다.
브라우저가 쥔 실행 시간
실행 시간은 브라우저가 쥐고 있습니다. 할 일이 없으면 브라우저가 스크립트를 끝냅니다. 다음 요청이 오면 다시 띄웁니다.
활성이 된 뒤로는 이 껐다 켜기가 계속 되풀이됩니다. 앞 소절의 설치와 대기는 한 번 지나가면 끝이지만, 이 순환은 활성 하나 안에서 요청이 올 때마다 돕니다.
stateDiagram-v2
멈춤 --> 깨어남: 요청이 온다
깨어남 --> 멈춤: 처리를 마치고 전역 변수도 같이 사라진다
그래서 전역 변수에 값을 담아 두고 다음 요청에서 꺼내 쓸 수 없습니다. 껐다 켜는 사이에 그 변수가 통째로 사라지기 때문입니다. 남겨야 할 값은 IndexedDB 같은 브라우저 저장소에 적어 둡니다.
오래 걸리는 일도 마음대로 못 합니다. 브라우저는 이벤트를 처리하는 동안에만 스크립트를 살려 둡니다. 답을 이미 돌려줬는데 뒤에서 파일을 더 저장하고 있으면, 그 일이 끝나기 전에 스크립트가 꺼질 수 있습니다.
그래서 이벤트마다 기다려 달라고 신청하는 함수가 하나 딸려 있습니다. 아직 안 끝난 일을
event.waitUntil() 에 넘기면 브라우저는 그 일이 끝날 때까지 스크립트를 살려 둡니다.
얹을 사이트와 얹지 않을 사이트
한 번 받아 두면 잘 안 바뀌는 파일이 많은 사이트에 잘 맞습니다. 화면 뼈대와 글꼴, 아이콘 같은 것들입니다. 이런 파일을 미리 저장해 두면 두 번째 방문부터는 네트워크를 거의 안 씁니다.
신호가 자주 끊기는 환경에서 쓰는 사이트에도 값이 큽니다. 지하철이나 엘리베이터에서 보는 화면, 창고나 매장에서 쓰는 현장 도구가 그렇습니다.
반대로 매번 최신 값이어야 하는 화면에는 얻을 것이 적습니다. 잔고나 재고처럼 한 발 늦으면 틀린 값이 되는 데이터는 저장해 둔 응답으로 대신할 수 없습니다. 요청을 가로채도 결국 네트워크로 내보내게 되어 관리할 스크립트만 하나 늘어납니다.
서버 앞에서 응답을 저장하는 다른 장치와도 가릅니다. CDN(Content Delivery Network)이나 리버스 프록시는 여러 사람의 요청을 한군데서 받습니다. 서비스 워커는 브라우저 안에 있어서 그 사람 한 명의 요청만 받습니다. 네트워크가 아예 끊긴 때를 이 스크립트만 다룰 수 있는 까닭입니다.
같은 응답을 저장하더라도 경로 위 어디에 앉느냐가 다릅니다. 브라우저 칸을 벗어나는 첫 화살표가 끊기는 곳이고, 그 앞까지는 서비스 워커만 서 있습니다.
flowchart TD
subgraph A["사용자 A 의 브라우저"]
SWA["서비스 워커"]
end
subgraph B["사용자 B 의 브라우저"]
SWB["서비스 워커"]
end
SWA -->|여기가 끊긴다| MID["CDN · 리버스 프록시"]
SWB -->|여기가 끊긴다| MID
MID --> SRV["서버"]
관련 항목
서비스 워커가 값을 담아 두는 브라우저 저장소
캐시 스토리지 · IndexedDB · localStorage · 세션 스토리지 · 쿠키 · 브라우저 캐시
서비스 워커가 다루는 요청과 응답의 규약
HTTP · Fetch · Cache-Control · 오리진 · CORS · TLS
서비스 워커를 얹어 만드는 웹 애플리케이션
프로그레시브 웹 앱 · 웹 푸시 · 백그라운드 동기화 · 앱 셸 · 오프라인 우선
서비스 워커와 같은 갈래인 브라우저 실행 환경
웹 워커 · 셰어드 워커 · 워커 스레드 · 메인 스레드 · 이벤트 루프
서비스 워커 대신 요청을 받아 주는 중간 장치
CDN · 리버스 프록시 · 관리형 캐시 · 포워드 프록시 · 오리진 서버
서비스 워커가 올라타는 브라우저 구성 요소
브라우저 · DOM · 자바스크립트 엔진 · 렌더링 · 사용자 에이전트 · HTML
서비스 워커를 갱신할 때 겪는 문제
다른 이름: service worker · Service Worker · 서비스워커