MV3의 백그라운드 스크립트를 persistant하게 유지하고자 찾아보았던 내용들을 정리합니다.
Manifest V3
Manifest V3은 크롬 브라우저 확장 개발을 위해 만들어진 규격입니다.
MV2에서 발전해오면서
- background-script 기능의 대폭 축소
- Event-handler 기반의 service-worker 도입
- 외부에서 원격으로 inject되는 소스코드의 금지.
등등이 박혀있는 가시처럼 사람을 귀찮게 합니다.
Plasmo

브라우저 확장 개발 스택을 조금 더 모던하게 가져가고자 react HMR과 typescript 등을 지원하는 Plasmo를 사용해 확장을 개발하고 있습니다.
vite기반으로 직접 말아올려도 되긴 한데, 아무래도 귀찮아서 사용하고 있습니다.
앞으로 MV3에서 다루어지는 각 개념들을 혼용할것인데, plasmo 기준으로 통일하겠습니다. 좌측이 MV3에서 부여받은 공식 명칭, 우측이 plasmo에서 편의상 사용하는 분류입니다.
- service worker -> background
- 백그라운드에서 실행되는 스크립트입니다.
- 상시 실행되지 않습니다. (inactive state의 존재)
- content script -> content
- 실제 웹페이지로 inject되는 스크립트입니다
- document 객체를 변경하는데 주로 사용합니다
- popup -> popup
- 확장 버튼 누르면 튀어나오는 그것입니다.
NOT Persistant SW
MV3의 background는 event기반으로 동작합니다.
event기반으로 동작하게 됨에 따라 MV2에서 있던 "persistant": true
를 사용할 수 없게 되었고, 장기간 background에서 처리가 진행되지 않을시 자동으로 작동을 멈추고 세션을 닫습니다.
종료 조건은 아래와 같습니다
- 30초간 extension API나 event를 받지 않은 경우
- event나 extension API의 처리에 5분 이상 걸리는 경우
fetch()
호출에 대한 응답이 30초 이상 지체되는 경우
아래 두가지는 일반적이지 않은 경우의 fallback으로, 일반적으로는 30초간 아무 동작이 없으면 background가 종료됩니다.
websocket
문제는 background가 종료되는 경우 웹소켓 연결이 끊기게 되며 정상적인 작동을 기대할 수 없게 된다는 데에 있습니다. 웹소켓의 존재 이유가 request없이 필요할때 정보를 건네받기 위함인데, 머리가 아파져 옵니다.
background가 종료되는 경우 setInterval
이나 setTimeout
도 중지되기 때문에 뽀모도로 등의 타이머 확장을 만드는 경우에도 신경쓰게 됩니다.
Solution
일반적으로 쉽게 떠오르는 해결법은 백그라운드에 30초마다 event를 발생시키면 됩니다. 이제 그 event를 발생시킬 요청을 어디에 맡기느냐가 문제입니다.
popup 역시 해당 팝업이 떠있는 동안에만 상태가 유지되고 스크립트가 동작하므로, 팝업을 닫은 상황에서는 이벤트를 보낼 수 없습니다.
Offscreen Document
MV3에서 사지를 찢어놓은 background의 기능을 보충하고자, chrome 109부터 offscreen document를 도입해서 눈에 보이지 않는 가상의 페이지를 확장마다 하나씩 할당해줍니다. 해당 페이지에도 여러 제약이 있지만 중요한 점은 lifecycle이 background와 다르다는 점이며, 사실상 무제한으로 떠있게 됩니다.
개발자가 쓰고싶으면 쓰는거지 왜 이유까지 밝혀야하는지는 잘 모르겠습니다.
해당 페이지에서 25초마다 background에 메시지를 전달하는 방식으로 이벤트를 트리거시키면, background가 무한정 유지되게 만들 수 있습니다.
Chrome 116 + Socket.IO
상식적으로 웹소켓을 쓰지 말라는 양 설계된 background 때문에, chrome 116부터는 웹소켓으로 들어오는 입력 역시 event로 간주하여 background가 종료되지 않도록 돕고 있습니다.
일반적인 ws()
객체를 사용하는 경우 25초 정도에 한번씩 heartbeat 통신을 만들어서 서버로 전송해주면 됩니다.
Socket.IO를 사용하는 경우 더더욱 쉬워집니다. Socketio 객체가 long-polling방식이 아닌 websocket 방식으로 정상적으로 연결된 경우, 연결이 정상적인지 확인하기 위해 내부적으로 ping
pong
이벤트를 송신합니다.
이 ping 사이의 간격이 pingInterval로 존재하며, 기본값인 25초를 유지하고 있기만 해도 background가 꺼지지 않게 됩니다.

service worker에서 socketio 연결이 수립되었기 때문에 2와 3을 차례대로 주고받고 있는 모습입니다.
Thoughts
당연한 사실이지만 persistant해짐으로 인해서 memory leak같은것들을 조금더 신경써서 개발할 수 있어야 합니다.
크롬 확장 자체가 그렇게 활발하게 개발되는 분야가 아니어서 그런지 정보가 퍼져있어서 개발하며 많은 시행착오를 겪고 있습니다. SocketIO를 사용해 백엔드 서버와 통신을 관리하던 제 입장에서는 그냥 가만히 냅둬도 persistant하다니 꽤나 김빠지는 이야기입니다만,
단적으로 간단히 구글링해봐서는 persistant하지 못하므로 content script로 아무 페이지에나 스크립트를 넣어서 25초마다 통신을 하라느니 하는 말들이 버젓이 있으므로 (구버전 크롬 기준의 ref) 괜히 고생하지 마시라고 정리해봅니다.
실제로 POC하고자 간단한 소스코드를 작성했을 적에는 그러한 방법으로 구현되어 있습니다.