메뉴
한국어
AdGuard VPN 블로그 Manifest V3로 이전된 AdGuard VPN 확장 프로그램

Manifest V3로 이전된 AdGuard VPN 확장 프로그램

새로운 Chrome API 버전인 Manifest V3는 많은 개발자에게 걸림돌이 되고 있습니다. 이것은 브라우저 확장 프로그램이 따라야하는 규칙에도 큰 변화이며, 이는 불편을 초래할 뿐만 아니라 기능의 일부가 작동하지 않게 합니다.

2022년 8월, AdGuard는 세계 최초로 매니페스트 V3를 기반으로 하는 광고 차단 브라우저 확장 프로그램을 출시했습니다. 개발은 결코 쉽지 않았으며 그 과정에서 많은 문제에 직면했습니다. 하지만 당시 매니페스트 V3의 등장으로 광고 차단기의 전망이 있다는 것을 증명했습니다.

구글이 매니페스트 V2 확장 프로그램을 최소 2024년 1월까지 연장했지만, 저희는 매니페스트 V3 규칙에 따라 작동하는 AdGuard VPN 브라우저 확장 프로그램의 개발을 늦추지 않았으며 여러분께 지금 바로 선보일 준비가 되어 있습니다.

AdGuard VPN MV3 브라우저 확장 프로그램

AdGuard MV3를 개발한 경험을 바탕으로 AdGuard VPN MV3를 개발하는 것이 쉽지 않으리라 예상했고, 저희의 예상은 적중했습니다. 저희가 겪었던 문제와 해결 방법에 대해 이야기해 보겠습니다.

절전모드로 전환되는 서비스 워커(service worker)

이전 버전의 Manifest에는 설치 후 또는 브라우저를 열 때 확장 프로그램이 한 번 실행되고 백그라운드에서 계속 작동할 수 있는 백그라운드 페이지가 있었습니다. Manifest V3에서는 백그라운드 페이지가 서비스 워커로 대체됩니다.

이 덕분에 사용자가 필요로 할 때만 확장 프로그램을 백그라운드에서 실행할 수 있습니다. Google 크롬의 기본 목표는 시스템 리소스 소비를 크게 줄이는 것입니다. 그리고 자주 사용하지 않는 확장 프로그램의 경우 이 방법이 효과적입니다. 그러나 AdGuard VPN과 같이 확장 프로그램이 사용자를 지속적으로 지원해야 하는 경우, 서비스 워커가 자주 절전 모드에 들어갔다가 깨어나면 시스템 부하가 줄어들기보다는 오히려 증가할 가능성이 높습니다.

이뿐만이 아닙니다. Chrome은 서비스 워커를 면밀히 모니터링합니다. 따라서 확장 프로그램이 서비스 워커가 절전모드로 전환되는 것을 방지하기 위해 30초 이내에 API 호출이나 이벤트를 수행하지 않으면 브라우저는 스크립트를 중지합니다.

서비스 워커를 중지하면 많은 오류로 인해 AdGuard VPN 확장 프로그램이 위협을 받습니다. 예를 들어, 사용 가능한 위치, 남은 무료 트래픽, 제외된 서비스, 계정에 연결된 다른 사용자 기기, 사용 가능한 보너스에 대한 최신 정보를 받지 못하며, 액세스 키와 트레이 아이콘에 확장 프로그램 상태를 업데이트할 수 없습니다.

해결책

위에서 이벤트와 API 호출이 서비스 워커가 최대 절전 모드로 전환되는 것을 방지한다고 언급했습니다. 서비스 작업자가 최대 절전 모드로 전환되는 것을 방지하는 방법은 다음과 같습니다.

  • 확장 프로그램의 팝업, 옵션 페이지 또는 콘텐츠 스크립트의 메시지
  • 프록시 인증 요청
  • 확장 프로그램의 팝업 열기
  • 새 브라우저 탭을 열거나 기존 탭을 새로 고침하거나 브라우저 창을 전환하는 경우
  • 페이지의 컨텍스트 메뉴에서 작업 선택(제외 대상에 웹사이트 추가, 제외 모드 변경)
  • 알람 API를 통해 설정된 타이머 트리거(나중에 자세히 설명)

간단히 말해, 서비스 워커가 이벤트 리스너를 기록하고 브라우저가 이벤트를 기록하는 식으로 작동합니다. 이벤트가 발생하면 브라우저는 이 이벤트에 대한 응답으로 깨워야 하는 확장 프로그램이 있는지 확인합니다. 이 경우 확장 프로그램의 서비스 워커를 깨우고 이벤트를 리스너에게 전달합니다.

매니페스트 V2 환경에서는 백그라운드 페이지가 항상 실행 중이고 다시 시작되지 않았기 때문에 이벤트 로깅이 비동기식일 수 있었기 때문에 리스너가 코드의 어디에 있든 상관없었습니다.

매니페스트 V3에서는 이벤트가 발생하면 서비스 워커가 다시 시작됩니다. 이 경우 이벤트가 리스너에 도달하려면 코드의 최상위 레벨에 (동기식으로) 등록되어야 합니다.

아래 예제에서 팝업 메시지, 옵션 페이지 및 콘텐츠 스크립트에 대한 수신기가 어떻게 설정되어 있는지 확인할 수 있습니다:

// add the listener for messages from popup, options page and userscripts
chrome.runtime.onMessage.addListener((event) => console.log(event));

이 예는 컨텍스트 메뉴 동작 이벤트의 리스너가 어떻게 설정되는지 보여줍니다.

// add the listener for context menu actions
chrome.contextMenus.onClicked.addListener((event) => console.log(event));

탭 동작 이벤트에 대한 리스너를 설정하는 방법을 보여줍니다:

// add the listener for tab updated
chrome.tabs.onUpdated.addListener((event) => console.log(event));
// add the listener for tab activated
chrome.tabs.onActivated.addListener((event) => console.log(event));
// add the listener for tab activated from another window
chrome.windows.onFocusChanged.addListener((event) => console.log(event));

매니페스트 V3에 새로 추가된 알람 API는 setInterval 및 setTimeout과 함께 서버 작업자가 잠들면서 발생하는 일부 오류를 방지하는 데 도움이 됩니다.

/**
* Creates an alarm with the specified name and delay
* @param alarmName
* @param delay in ms
*/
const createAlarm = (alarmName: string, delay: number) => {
   chrome.alarms.create(alarmName, { when: Date.now() + delay });
};

/**
* Creates a periodic alarm with the specified name and interval
* @param alarmName
* @param interval in minutes!
*/
const createPeriodicAlarm = (alarmName: string, interval: number) => {
   chrome.alarms.create(alarmName, { periodInMinutes: interval });
};

알람 API 사용

/**
* setTimeout implementation
* @param callback
* @param timeout in ms
*/
setTimeout = (callback: () => void, timeout: number): number => {
   const timerId = this.generateId();
   alarmApi.createAlarm(`${timerId}`, timeout);
   alarmApi.onAlarmFires(`${timerId}`, callback);
   return timerId;
};

/**
* setInterval implementation
* @param callback
* @param interval in ms
*/
setInterval = (callback: () => void, interval: number): number => {
   const timerId = this.generateId();
   alarmApi.createPeriodicAlarm(`${timerId}`, this.convertMsToMin(interval));
   alarmApi.onAlarmFires(`${timerId}`, callback);
   return timerId;
};

MV3용 타이머 구현

그러나 알람 API에는 3-5-10초 단위로 작동하는 타이머를 설정할 수 없고 1분 이상으로 작동하는 타이머를 설정할 수 있다는 다소 불편한 제한이 있습니다.

서비스 워커 깨우기

서비스 워커가 최대 절전 모드로 전환되면 RAM에 저장되어 있던 데이터를 업로드합니다. 따라서 활성화된 후 확장 프로그램이 모듈을 초기화하는 데 평균 1.5-2 초라는 긴 시간이 걸립니다. 보시다시피 브라우저를 다시 시작할 때만 백그라운드 페이지가 다시 로드되었기 때문에 백그라운드 페이지에는 이러한 문제가 없었습니다.

해결책

RAM에서 각 상태 변경 사항을 저장하는 모듈을 추가했습니다. 서비스 작업자가 빠른 복구를 위해 깨어날 때 AdGuard VPN이 사용하는 마지막 저장 상태입니다. 이렇게 하면 서비스 워커 재시작 지연이 150-200ms로 줄어듭니다.

interface StateStorageInterface {
// Gets the value for the specified key from the session storage
   getItem<T>(key: StorageKey): T;

// Sets the value for the specified key in the session storage
   setItem<T>(key: StorageKey, value: T): void;

// Initializes the storage by loading the data from the session storage,
// or creating a new storage with the default data if none exists.
   init(): Promise<void>;
}

확장 상태 리포지토리 인터페이스

이 솔루션을 구현하려면 변경 가능한 필드를 클래스에서 상태 저장소로 이동해야 했습니다. 그 대신 클래스에 게터와 세터를 추가했습니다.

예를 들어 폴백 API가 다음과 같았다고 가정해 보겠습니다.

export class FallbackApi {
   fallbackInfo: FallbackInfo;

   private setFallbackInfo(fallbackInfo: FallbackInfo) {
       this.fallbackInfo = fallbackInfo;
   }

   private async getFallbackInfo(): Promise<FallbackInfo> {
       if (FallbackApi.needsUpdate(this.fallbackInfo)) {
           await this.updateFallbackInfo();
       }

       return this.fallbackInfo;
   }
}

이제 모양이 변경되었습니다.

export class FallbackApi {
   private get fallbackInfo(): FallbackInfo {
       return sessionState.getItem(StorageKey.FallbackInfo);
   }

   private set fallbackInfo(value: FallbackInfo) {
       sessionState.setItem(StorageKey.FallbackInfo, value);
   }

   private static needsUpdate(fallbackInfo: FallbackInfo): boolean {
       return fallbackInfo.expiresInMs < Date.now();
   }

   private async getFallbackInfo(): Promise<FallbackInfo> {
       if (this.fallbackInfo && FallbackApi.needsUpdate(this.fallbackInfo)) {
           await this.updateFallbackInfo();
           if (this.fallbackInfo) {
               return this.fallbackInfo;
           }
       }

       return this.fallbackInfo;
   }
}

다음은 몇 가지 예시입니다.

class Endpoints implements EndpointsInterface {
   vpnInfo?: VpnExtensionInfoInterface;
}

매니페스트 V2에서 엔드포인트 클래스에 상태 추가하기

class Endpoints implements EndpointsInterface {
   #state: EndpointsState | undefined;

   private get vpnInfo(): VpnExtensionInfoInterface | null {
       return this.state.vpnInfo;
   }

   private set vpnInfo(vpnInfo: VpnExtensionInfoInterface | null) {
       this.state.vpnInfo = vpnInfo;
       sessionState.setItem(StorageKey.Endpoints, this.state);
   }

   public get state(): EndpointsState {
       if (!this.#state) {
           throw new Error('Endpoints API is not initialized');
       }

       return this.#state;
   }

   public set state(value: EndpointsState) {
       this.#state = value;
   }
}

매니페스트 V3에서 엔드포인트 클래스에 상태 추가하기

export class Credentials implements CredentialsInterface {
   vpnToken: VpnTokenData | null;

   vpnCredentials: CredentialsDataInterface | null;
}

매니페스트 V2에서 자격 증명 클래스에 상태 추가하기

export class Credentials implements CredentialsInterface {
   state: CredentialsState;

   saveCredentialsState = () => {
       sessionState.setItem(StorageKey.CredentialsState, this.state);
   };

   get vpnToken() {
       return this.state.vpnToken;
   }

   set vpnToken(vpnToken: VpnTokenData | null) {
       this.state.vpnToken = vpnToken;
       this.saveCredentialsState();
   }

   get vpnCredentials() {
       return this.state.vpnCredentials;
   }

   set vpnCredentials(vpnCredentials: CredentialsDataInterface | null) {
       this.state.vpnCredentials = vpnCredentials;
       this.saveCredentialsState();
   }
}

매니페스트 V3에서 자격 증명 클래스에 상태 추가하기

NetworkConnectionObserver 구현

NetworkConnectionObserver는 네트워크 상태(온라인/오프라인)를 모니터링하는 클래스입니다. 앱이 서버로부터 응답을 받지 못할 때 사용되며, 사용자의 인터넷 연결이 없거나 서버 측의 문제 등 문제의 원인을 파악하는 데 도움이 됩니다. 또한 NetworkConnectionObserver는 사용자가 오프라인에서 온라인으로 전환하는 것을 모니터링합니다. 이 경우 액세스 권한이 확인되고 사용자가 오프라인 상태가 되기 전에 설정된 경우 연결이 복원됩니다.

MV2에서는 NetworkConnectionObserver가 매우 간단하게 구현되어 있습니다. 온라인이벤트를 수신 대기하고 전달된콜백`을 호출합니다.

class NetworkConnectionObserver {
   constructor(callback: () => Promise<void>) {
       window.addEventListener('online', callback);
   }
}

MV3에서는 서비스 워커가 에 액세스할 수 없으므로 구현이 더 복잡합니다. 화면 밖 문서를 사용하는 것이 해결책이 될 수 있습니다. 이렇게 하면 문서에 액세스할 수 있는 보이지 않는 페이지가 생성되고 서비스 작업자가 사용할 수 없는 작업을 수행할 수 있습니다.

그러나 Google은 앞으로 오프 스크린 문서가 서비스 작업자와 유사하게 일정 기간 동안 사용하지 않으면 절전 상태가 될 것이라고 이야기했습니다. 이런 방식으로는 NetworkConnectionObserver온라인/오프라인 이벤트를 항상 수신할 수 없어 여러 가지 문제가 발생할 수 있습니다.

이제 저희는 확실한 해결책을 사용해야 합니다. 0.5초마다 navigator.online의 상태를 확인하고 오프라인에서 온라인으로 변경된 경우 콜백을 호출합니다.

/**
* Starts checking if the network connection is online at a specified time interval.
*/
private startCheckIsOnline() {
   setInterval(() => {
       this.setIsOnline(navigator.onLine);
   }, this.CHECK_ONLINE_INTERVAL_MS);
}

/**
* Calls handler if network connection becomes online and sets isOnline value.
*/
private setIsOnline(isOnline: boolean) {
   if (isOnline && !this.isOnline) {
       this.onlineHandler(); // callback
   }
   this.isOnline = isOnline;
}

웹소켓 지원 문제

이제 매니페스트 V3에서는 웹소켓 연결을 통해 확장 프로그램이 수신한 메시지가 절전 모드인 서비스 워커를 깨우지 않습니다. 그 결과 확장 프로그램에서 백엔드 서버 액세스 키를 업데이트해야 한다는 메시지를 놓칠 수 있으며 트래픽 제한, 연결된 장치 제한 및 라우팅할 수 없는 도메인에 도달했다는 오류 메시지를 처리하지 못할 수 있습니다.

해결책

처음의 충동은 Chrome의 비전을 거스르고 잠들지 않는 서비스 작업자를 만드는 것이었습니다. 그리고 사실, 그렇게 했습니다. 하지만 Manifest V3의 규칙에 따라 서비스 작업자가 깨어 있을 때만 웹소켓에 연결하기로 결정했습니다.

해결책을 찾았음에도 불구하고 Google이 저희가 보고한 문제를 해결하고 웹소켓 연결을 통해 수신된 메시지로 서비스 작업자 수명을 연장할 수 있는 가능성을 추가하기를 정말로 바랍니다.

onAuthRequired` 핸들러의 조기 트리거

프록시 인증을 요청하는 브라우저 팝업 문제가 수정되었습니다. 이 문제는 서비스 워커가 깨어날 때 onAuthRequired 이벤트 핸들러가 실행되기 전에 세션 스토리지가 자격 증명을 로드할 수 없을 때 발생했습니다.

해결책

이 문제를 해결하기 위해 onAuthRequired 핸들러에 비동기 콜백을 추가했습니다. 이제 세션 스토리지가 자격 증명을 로드하고 입력할 때까지 기다리도록 설정되었습니다.

/**
     * Adds listener for the onAuthRequired event
     */
    private addOnAuthRequiredListener = () => {
        chrome.webRequest.onAuthRequired.addListener(
            this.onAuthRequiredHandler,
            { urls: ['<all_urls>'] },
            ['blocking'],
        );
    };

동기식 콜백이 있는 onAuthRequired 핸들러

 /**
     * Adds listener for the onAuthRequired event
     */
    private addOnAuthRequiredListener = () => {
        chrome.webRequest.onAuthRequired.addListener(
            this.onAuthRequiredHandler,
            { urls: ['<all_urls>'] },
            ['asyncBlocking'],
        );
    };

동기식 콜백이 있는 onAuthRequired 핸들러

결론

매니페스트 V3는 가장 스마트하고 안정적인 환경과는 거리가 멉니다. 그럼에도 불구하고, 이미 AdGuard VPN MV3 브라우저 확장 프로그램을 설치하여 사용해 보실 수 있습니다. 언제나 그렇듯이 저희는 피드백에 크게 의존하고 있습니다. 버그를 발견하시면 GitHub로 알려 주세요.

이 게시물을 좋아하시나요?

Windows용
AdGuard VPN

모든 브라우저 또는 앱을 사용하고 다시는 익명성에 대해 걱정하지 마세요. AdGuard VPN을 사용하면 전 세계 어디에서나 이동할 수 있습니다.
자세히 알아보기
다운로드
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다

MacOS용
AdGuard VPN

단 두 번의 클릭으로 전 세계 어디에서나 도시(65개 이상의 서버가 있음)를 선택하면 기업과 정부로부터 데이터를 안전하게 보호할 수 있습니다.
자세히 알아보기
다운로드
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다

iOS용
AdGuard VPN

AdGuard VPN을 사용하여 온라인 보호를 강화하고 원하시는 영화와 프로그램을 안전하게 시청하세요!
자세히 알아보기
App Store
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다

Android용
AdGuard VPN

어디서든 온라인 익명성을 유지할 수 있습니다. AdGuard VPN은 수십 개의 서버와 빠르고 안정적인 연결을 제공합니다.
자세히 알아보기
Google Play
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다
다운로드
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다

Chrome용
AdGuard VPN

위치를 숨기고 세계 어느 곳으로든 이동하세요. 속도 제한 없이 모든 콘텐츠를 확인하고 인터넷에서 익명을 유지합니다.
자세히 알아보기
설치
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다

Edge용
AdGuard VPN

한 번의 클릭으로 다른 위치로 이동하고 IP 주소를 숨길 수 있을 뿐만 아니라 익명으로 인터넷을 사용할 수도 있습니다.
자세히 알아보기
설치
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다

Firefox용
AdGuard VPN

개인정보를 보호하고 IP 주소를 숨길 수 있을 뿐만 아니라 VPN이 작동하는 웹 사이트와 작동하지 않는 웹 사이트를 선택할 수도 있습니다.
자세히 알아보기
설치
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다

Opera용
AdGuard VPN

Opera 브라우저를 사용하여 세계 어느 곳으로든 빠르게 이동할 수 있으며 익명을 유지할 수 있습니다.
자세히 알아보기
설치
프로그램을 내려받음으로써 라이선스 계약에 동의하게 됩니다
AdGuard VPN
다운로드가 시작되었습니다
화살표가 가리키는 버튼을 클릭하여 설치를 시작하세요.
스캔하여 모바일 장치에 AdGuard VPN 설치