
크롬 삼권 분리
쓰고있던 크롬 확장프로그램에 기능이 하나 있었다면 좋겠다고 생각이 들어서 직접 만들어보기로 했다. 가볍게 생각하고 개발에 들어갔는데 고려해야 할 부분이 예상보다 많았고, 간단히 정리해보려 한다.
크롬 확장프로그램은 사용할 때 작은 화면만 뜨다보니 간단한 구조를 가지고 있을 거라는 선입견을 가지기 쉽다.
내가 그랬다.
실제로는 일반적인 웹 프로그램들보다 복잡한 구조로 동작한다. 또 이를 제대로 이해하지 않으면 개발에 난항을 겪는다.
크롬 확장프로그램은 크게 세 가지 요소로 이루어진다. Popup, Background, Content 스크립트인데 요약하자면 분리된 환경에서 실행되는 자바스크립트 파일들이라고 할 수 있다. 직관적인 이름을 통해 어떤 역할일지 유추해볼 수 있다.
위에서 언급한, 확장프로그램을 눌렀을 때 뜨는 작은 화면이 바로 Popup 스크립트이다. 페이지 렌더링을 수행해야 하므로 세 스크립트 중에 유일하게 HTML, CSS, JS를 모두 필요로 한다. 주로 확장프로그램 동작에 필요한 설정 화면을 표시하는데, 상태관리를 위해 React처럼 화면 렌더링용 라이브러리를 사용하는게 일반적이다.
기본적으로 확장프로그램 Popup이 표시되는 화면은 http가 아닌 chrome-extension 스킴으로 실행된다. 때문에 URL을 변경해 외부 페이지를 로딩할 순 없고 확장프로그램 내부 파일을 요청하거나 CSR의 routing 용으로 사용된다.
페이지 리소스들을 로딩하는 index.html도 내부 파일에서 요청하므로 SSR과는 상충되는 면이 있다. 만약 꼭 SSR을 사용해야 한다면 권한 설정을 더 상세하게 추가하고 iframe같은 별도 우회법이 필요하다.
Popup 스크립트는 확장프로그램 아이콘을 눌러서 팝업 화면이 표시되는 동안에만 활성화된다. 화면이 보이지 않는 평소에도 확장프로그램의 기능을 작동시키기 위해선 상시 동작할 수 있는 능력을 가진 스크립트가 필요하다. 이 역할로 Background 스크립트가 존재한다.
상시로 동작할 수 있는 능력이 효율적으로 동작하기 위해선 두 가지 요구 조건이 충족되어야 한다.
첫 번째로 메인쓰레드를 사용하지 않아야 한다. 상시로 동작할 수 있는 코드가 메인쓰레드에서 돌아간다면 페이지 렌더링 코드와 서로 영향을 미칠 것이다.
예시로 확장프로그램이 동작하는 동안 페이지 인터렉션을 막을 수도 있고, 페이지 이동이 완료된 후에야 확장프로그램 동작할 수도 있다.
따라서 Background 스크립트는 이름대로 백그라운드에서 독립적인 이벤트루프로 동작해야 한다.
두 번째로 상시 동작하면 안된다. 이상하게 들리지만 처음부터 끝까지 꺼지지 않고 유지된다면 디바이스의 자원을 너무 많이 사용할 것이다. 필요한 순간에만 활성화되어야 한다.
위 두 가지 요구 조건에 부합하는 게 바로 Extension Service Worker이다. Service Worker는 백그라운드에서 별도의 이벤트루프를 가지고 서브쓰레드에서 실행되는 js 파일이다. 앞에 Extension이 붙은 이유는 크롬 확장프로그램 전용으로 추가된 인터페이스가 존재해 세부 동작에서 차이가 존재하기 때문이다.
일반 Service Worker와의 차이점은 다음과 같다.
Chrome API에서 제공하는 이벤트 기반으로 잠깐 백그라운드에서 활성화되었다가 사라지므로 브라우징을 하면서 부가적인 기능을 하는, 말 그대로 확장프로그램으로써의 역할에 충실할 수 있다.
경험 하나
직접 만든 확장프로그램에서 일정 시간 간격으로 헤더 값을 갱신하는 기능을 구현할 때 처음 시도했던 방법은
setInterval이었다.기대했던 건
setInterval이 일단 등록되면 Service Worker가 종료되더라도 시간이 되면 콜스택으로 들어와 함수가 실행되는 것이었다. 하지만setInterval의 콜백이 등록되는 곳은 Service Worker의 이벤트루프이고, 이는 워커가 종료될 때 같이 사라지므로 콜백은 실행되지 못한다.최종적으로 사용한 방식은 Chrome API가 제공하는 브라우저 이벤트를 트리거로 시간을 체크하는 것이다.
chrome.tabs.onActivated는 탭이 활성화될 때 발생하는 이벤트로, 일상적으로 발생하면서 너무 자주는 아니기 때문에 적당했다.
결국 차이를 만드는 핵심은 Chrome API이다.
Chrome API는 브라우저의 기능을 관찰하거나 제어할 수 있는 통로를 제공한다. 예시로 알아보자.
경험 둘
네트워크 요청에 헤더를 추가하는데 사용한 API는
declarativeNetRequest이다. 제어 대상과 방법, 조건에 대해 다양한 옵션을 제공한다.런타임에 동적으로 룰을 추가할 뿐 아니라, manifest라는 설정 파일을 통해 정적인 룰도 추가할 수 있다. 광고 차단 프로그램에서 사용하는 방식이 바로 이것이다.
경험 셋
무한루프로 인한 과도한 네트워크 요청을 막는 기능에는 어떤 API를 썼을까?
막는 것 자체는
declarativeNetRequest를 썼다. block도 크롬이 제공하는 옵션 중 하나다.네트워크 수를 세는 것은
chrome.webRequest.onBeforeRequest를 사용했다. 이 API는 브라우저에서 발생하는 모든 요청 발생 시점에 요청에 대한 정보를 전달한다.
Background 스크립트는 거의 모든 Chrome API를 사용할 수 있다. 반면 Popup 스크립트는 매우 제한적인 종류만 가능한데 이유는 몇 가지가 있다. 가장 큰 이유는 보안으로, 세 번째 사례에서 언급했듯 Chrome API가 제공하는 정보에는 민감한 정보도 포함할 수 있다. 기본적으로 격리되어 외부 코드가 유입될 가능성이 적은 Background 스크립트와 다르게 Popup은 외부와의 연결이 막혀있지는 않다.
그럼 Popup은 Chrome API에 아예 관여하지 못할까? 아니다. API 중에는 스크립트 간에 정보 전달을 위한 chrome.runtime.sendMessage같은 것도 존재하므로 Background에게 역할을 넘길 수 있다.
경험 둘+
네트워크 요청에 헤더를 추가하는데 사용한 API는
declarativeNetRequest라고 했는데, 이 API도 당연히 Background 스크립트에서만 사용이 가능하다. 하지만 헤더에 대한 정보를 입력하는 곳은 Popup 스크립트이다. 따라서 Popup에서는 입력받은 정보를 담아 Background에 message를 보내고, Background에선 message 이벤트를 구독하다가 정보를 받아 실제 API를 호출한다.
지금까지 소개한 두 가지 스크립트로도 확장프로그램을 만드는데 큰 문제 없을 것 같다.
실제로 내가 만든 확장프로그램은 Popup과 Background만 사용했다.
하지만 브라우저 화면에 나타나는 정보에 접근하고 제어하는 기능은 두 가지 스크립트로는 할 수 없다. 이 역할로 Content Script가 존재한다.
Content 스크립트는 페이지 DOM에 접근하는 게 최우선 목표이다. 때문에 Service Worker도 쓰지않고 메인쓰레드에서 돌아가고 일반적인 자바스크립트와 같은 실행 컨텍스트를 공유한다.
다만 실행 컨텍스트를 공유한다는 것이지 아예 일체화되어서 실행되는 것은 아니므로 자바스크립트 사이에서 변수 같은 건 공유하지 않는다. DOM과 window 객체 등을 공유할 뿐이다.
Content 스크립트는 DOM의 직접적인 변경도 가능하므로 실제 사용자가 보는 페이지 레이아웃에 변화를 줄 수도 있다.
자주 쓰는 확장프로그램 중에 페이지의 텍스트를 드래그하면 자동 번역을 보여주는 프로그램이 있다. 이 기능도 드래그한 텍스트, 마우스 위치 등의 정보를 얻고 툴팁으로 나타내기 위해선 Content 스크립트를 써야할 것이다.
페이지에 다크모드를 강제로 만들어주는 확장프로그램도 있다. 사이트의 CSS를 가져와 색상을 대비시켜서 주입시키려면 이 또한 Content 스크립트가 필요할 것이다.
배포는 크롬 웹스토어를 통해서 이루어진다. 범용적 용도가 아니라면 빌드 파일을 직접 브라우저에 넣어서 실행할 수도 있다.
일반적인 앱 스토어와 비슷하다. 일부 Chrome API는 특정 권한을 요구하는데 권한의 범위가 넓을수록 심사가 오래 걸린다. 따라서 권한의 범위를 최소한으로 맞추거나 선택적으로 요구하는 걸 권장하기도 한다.
다행히도 앱 스토어와 다르게 확장프로그램은 한 번 설치하면 업데이트는 자동으로 이루어진다. 업데이트 사항이 심사를 통과한 뒤 대략 3~6시간 이후 유저에게도 반영된다.