아래 글은 Chrome Extension(확장앱)에서 WebSocket 요청에 사용자 정의 헤더를 지정하기 위한 고군분투 과정을 정리한 글입니다.
프로젝트 진행 중, Client(확장앱)에서 WebSocket Connection 요청을 보낼 때 USER_ID: 1 같은 사용자 정의 헤더를 지정해야 했습니다.
브라우저에서 기본으로 지원하는 WebSocket (new WebSocket(url)) 으로는 사용자 정의 헤더 지정이 불가능하기 때문에, 나름 다양한 방법을 찾아보았지만 방법이 없어 꽤나 막막했습니다.
소켓 통신에서 커스텀 헤더를 넣으려면 SockJS 등의 라이브러리를 Client와 Server 양측에서 모두 사용해야 하는데, 이미 서버는 구현이 완료되어 실서버에서 돌아가는 상황이었기에 새로운 라이브러리를 적용하기가 어려웠습니다.
때문에 “안되는 건가...🥹” 하고 포기하려 하다, 문득 아이디어가 떠올랐습니다.
“Chrome declarativeNetRequest API로 WS 요청을 감지해 헤더를 수정할 수 있지 않을까?”

일반적으로 자바스크립트에서는 WebSocket을 다음과 같이 생성합니다.
const ws = new WebSocket('ws://test.com');
이렇게 생성한 WebSocket 객체에는 직접 헤더를 설정할 인터페이스가 없습니다.
SockJS 라이브러리를 쓰면 해결할 수 있지만, 서버도 동일 라이브러리를 적용해야 하므로 일방적으로 클라이언트에서만 사용할 수 없었습니다.
declarativeNetRequest를 통해 특정 리소스 유형(웹소켓, XHR 등)에 대한 요청을 감지하고, 그 요청의 헤더를 수정할 수 있습니다.
아래와 같이 manifest.json 권한에 declarativeNetRequest를 추가해줍니다.
{
"permissions": [
"declarativeNetRequest"
],
// ...
}
이후, 확장앱 페이지(예: Page.js)에서 다음과 같이 코드를 작성합니다.
const ruleId = 1; // 고유한 규칙 ID
const studentId = 1; // 예시로 USER_ID에 들어갈 값
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [ruleId],
addRules: [
{
id: ruleId,
priority: 1,
action: {
type: "modifyHeaders",
requestHeaders: [
{
header: "USER_ID",
operation: "set",
value: String(studentId),
},
],
},
condition: {
urlFilter: "ws://*", // WebSocket 요청만 감지
resourceTypes: ["websocket"],
},
},
],
}, () => {
if (chrome.runtime.lastError) {
console.error("Error updating rules:", chrome.runtime.lastError);
} else {
console.log(`Rule updated with USER_ID: ${studentId}`);
}
});
이렇게 설정하고 ws 연결 시 호출하면, 해당 확장앱 페이지가 동작 중인 탭에서 발생하는 ws:// 요청을 가로채 USER_ID 헤더를 강제로 설정해줍니다.
이 방식을 택한 이유는 StudentId를 동적으로 설정하기 위함입니다. DynamicRules를 미리 지정해둘 수도 있지만, 그 경우 실행 시점에 생성되는 StudentId를 반영할 수 없습니다. 따라서 규칙을 매번 업데이트하는 방식을 통해, 실제 요청 시점에 동적 StudentId를 헤더에 포함시키도록 한 것입니다.
동작 확인
웹소켓 요청을 발생시키면, 개발자도구 Network 탭 또는 서버 로그를 통해 USER_ID 헤더가 정상적으로 포함되어 전송되는 걸 확인할 수 있습니다.

Foreground에서는 성공했지만, Background(Service Worker) 환경에서 declarativeNetRequest로 ws:// 요청을 잡아 헤더를 추가하려고 하자, 의도대로 동작하지 않았습니다.
Foreground(확장앱 페이지)는 사용자가 페이지를 닫는 순간 웹소켓 연결이 종료됩니다.
반면, Background(Service Worker)는 브라우저가 완전히 종료되지 않는 한 계속 유지될 수 있기에, 연결을 장시간 유지하기 적합합니다.
이번 프로젝트에서는 “확장앱 페이지가 닫힌 뒤에도 서버와의 WebSocket 연결이 계속 이어져야 한다”는 요구사항이 있었기에, Background(Service Worker) 환경에서 웹소켓을 구동하고자 했습니다.
처음에는 크롬 확장앱 자체의 구조적 제한 때문이라고 의심했으나, 찾아보니 Service-Worker에서 declarativeNetRequest 사용 관련 버그가 보고된 상황이었습니다.
관련 버그:
https://issues.chromium.org/issues/40815149?utm_source=chatgpt.com
해당 문제를 어떻게든 우회해보고자, content-scripts를 사용하는 방법을 시도했습니다.
Content Script는 chrome.scripting.executeScript 를 통해 일반 웹사이트에 코드를 주입하여 동작시킬 수 있습니다.
탭 내부, 즉 Foreground에서 실행되므로 declarativeNetRequest가 정상적으로 동작하여 웹소켓 요청에 헤더가 수정될 것으로 기대했습니다.
직접 테스트해본 결과, Content Script를 통한 해결은 기대에 미치지 못했습니다.
Foreground(확장앱 페이지)에서처럼 명확하게 웹소켓 요청이 수정되는 것을 확인하기 어려웠으며, 결국 Service Worker에서 WS 요청 시 declarativeNetRequest를 사용해 Header를 추가하는 작업은 실패했습니다. 🥹
정리하자면,
“버그 때문에 실패라니 ㅡㅡ💢”
아쉬운 결과이지만, 언젠가 관련 버그가 해결될지도 모르니 계속 모니터링할 예정입니다.
비슷한 문제로 고민 중이신 분들께 이번 글이 조금이나마 도움이 되길 바랍니다.