웹앱과 Chrome Extension이 함께 동작하는 서비스에서 버전 관리는 단순하지 않다. 두 컴포넌트가 독립적으로 배포되기 때문이다.
웹앱은 배포 즉시 모든 사용자에게 반영된다. 반면 Chrome Extension은 Chrome 웹스토어 심사를 거쳐야 하고, 사용자가 수동으로 업데이트하거나 Chrome이 자동 업데이트할 때까지 구버전이 남아 있을 수 있다. 이 시간차가 문제다.
pixelDiff는 웹앱에서 Extension으로 캡처 명령을 보내고, Extension이 스크린샷을 찍어 돌려주는 구조다. 웹앱이 새 메시지 포맷을 사용하는데 Extension은 구버전이라면? 통신이 깨진다.
문제 정의
요구사항:
- 웹앱이 Extension 설치 여부를 감지
- Extension 버전이 웹앱 요구 버전보다 낮으면 업데이트 안내
- 사용자가 Extension을 설치/업데이트하면 즉시 감지
제약 조건:
- Extension은 웹앱 코드에 직접 접근 불가 (Chrome 보안 정책)
- 웹앱은 Extension이 설치되어 있는지 직접 확인하는 API가 없음
- 사용자가 여러 탭을 열어두고 다른 탭에서 Extension을 설치할 수 있음
버전 호환성 전략 선택
Extension 감지 방법을 고민하기 전에, 더 근본적인 질문이 있었다. 구버전 Extension 사용자를 어떻게 처리할 것인가?
처음 고민: 버전별 웹앱 분기
모바일 앱처럼 접근하려 했다. Extension 버전을 확인하고, 그 버전에 맞는 웹앱 코드를 보여주는 방식이다.
Extension v1.1.0 → 웹앱 v1.1.x 코드 제공
Extension v1.2.0 → 웹앱 v1.2.x 코드 제공
이론상 모든 사용자가 자신의 Extension 버전에 맞는 웹앱을 사용하니 호환성 문제가 없다.
하지만 현실은 달랐다.
- 레거시 버전의 동작 보장 불가: 과거 버전 웹앱이 현재 인프라(API, DB 스키마)와 호환되는지 매번 확인해야 한다. 백엔드가 바뀌면 프론트엔드 레거시도 깨질 수 있다.
- QA 부담 폭발: 웹앱 버전 N개 × Extension 버전 M개 = N×M 조합을 테스트해야 한다. 버전이 쌓일수록 감당이 안 된다.
- 버그 수정의 악몽: 보안 취약점이 발견되면 모든 레거시 버전에 패치를 배포해야 한다.
모바일 앱은 클라이언트가 모든 로직을 들고 있어서 이 방식이 가능하다. 웹앱은 서버 의존성이 높아서 다르다.
결론: 단일 버전 + 업데이트 강제
웹앱은 항상 최신 버전만 제공하고, Extension이 구버전이면 업데이트를 안내한다.
- 호환성 매트릭스 관리 불필요
- QA는 최신 버전 조합만 테스트
- 버그 수정은 한 곳에서
트레이드오프는 있다. 구버전 Extension 사용자는 업데이트 전까지 일부 기능이 제한될 수 있다. 하지만 Chrome Extension은 자동 업데이트가 기본이라 대부분 24시간 내 최신 버전으로 갱신된다. 감수할 만한 수준이다.
Extension 감지 방법
버전 전략이 정해졌으니, 이제 Extension을 어떻게 감지할지 결정해야 한다.
| 방법 | 원리 | 장점 | 한계 |
|---|---|---|---|
| Content Script 주입 감지 | Extension이 페이지에 스크립트 주입 → 전역 변수 확인 | 구현 간단 | 페이지 로드 후에만 감지, 새로고침 필요 |
externally_connectable |
Extension의 background script에 직접 메시지 전송 | 페이지 새로고침 없이 감지 | manifest에 허용 도메인 명시 필요 |
| Native Messaging | 로컬 앱을 통한 브릿지 | 완전한 제어 | 설치 복잡도 높음, 웹 서비스에 부적합 |
용어 정리:
- Content Script: Extension이 웹페이지에 주입하는 JavaScript. 페이지의 DOM에 접근할 수 있지만, 페이지가 로드될 때 주입된다.
externally_connectable: Extension이 특정 도메인의 웹페이지에서 직접 메시지를 받을 수 있게 허용하는 manifest 설정.
선택: Content Script + externally_connectable 하이브리드
Content Script만 쓰면 사용자가 Extension 설치 후 페이지를 새로고침해야 한다. 다른 탭에서 Extension을 설치하고 돌아와도 감지가 안 된다. Content Script는 페이지 로드 시점에 주입되기 때문이다.
여기서 externally_connectable이 해결책이 됐다. 이 설정을 추가하면 웹앱이 Extension의 background script에 직접 메시지를 보낼 수 있다. Content Script 주입 여부와 무관하게 동작한다.
구현
PING-PONG 프로토콜
버전 확인의 핵심은 "Extension이 응답할 수 있는가?"다. 웹앱이 PING을 보내고, Extension이 PONG과 함께 버전을 응답하는 구조다.
// 웹앱: Extension에 PING 전송 (Content Script 경유)
const pingExtension = (): Promise<{ installed: boolean; version?: string }> => {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
cleanup();
resolve({ installed: false });
}, 1000);
const handler = (event: MessageEvent) => {
if (
event.data?.type === 'PIXELDIFF_PONG' &&
event.data?.source === 'pixeldiff-extension'
) {
cleanup();
resolve({
installed: true,
version: event.data.version,
});
}
};
window.addEventListener('message', handler);
window.postMessage({
type: 'PIXELDIFF_PING',
source: 'pixeldiff-webapp',
}, '*');
});
};
1초 내 응답이 없으면 미설치로 판단한다. 응답이 오면 버전 정보를 추출한다.
Semantic Version 비교
Extension 버전이 웹앱 요구 버전보다 낮은지 확인한다.
function isVersionOutdated(current: string, required: string): boolean {
const [cMajor = 0, cMinor = 0, cPatch = 0] = current.split('.').map(Number);
const [rMajor = 0, rMinor = 0, rPatch = 0] = required.split('.').map(Number);
if (cMajor < rMajor) return true;
if (cMajor === rMajor && cMinor < rMinor) return true;
if (cMajor === rMajor && cMinor === rMinor && cPatch < rPatch) return true;
return false;
}
웹앱의 요구 버전은 package.json version을 next.config.js에서 환경변수로 주입한다.
// next.config.js
const packageJson = require('./package.json');
const nextConfig = {
env: {
NEXT_PUBLIC_APP_VERSION: packageJson.version,
},
};
이 값이 MIN_EXTENSION_VERSION이 된다. 웹앱 버전 = Extension 최소 요구 버전이라는 단순한 규칙이다. 별도 호환성 매트릭스를 관리하지 않아도 된다.
탭 전환 시 재감지
Content Script 방식의 한계를 externally_connectable로 해결한다.
Extension의 manifest.json에 허용 도메인을 명시한다.
{
"externally_connectable": {
"matches": [
"https://pixeldiff.turtle-tail.com/*",
"http://localhost:3000/*"
]
}
}
이제 웹앱은 chrome.runtime.sendMessage로 Extension의 background script에 직접 메시지를 보낼 수 있다. Content Script가 주입되지 않은 상태에서도 동작한다.
// 웹앱: background script로 직접 PING
const pingExtensionBackground = async () => {
const chromeRuntime = window.chrome?.runtime;
if (!chromeRuntime?.sendMessage) {
return { installed: false };
}
return new Promise((resolve) => {
chromeRuntime.sendMessage(
EXTENSION_ID,
{ type: 'PIXELDIFF_PING' },
(response) => {
if (response?.type === 'PIXELDIFF_PONG') {
resolve({ installed: true, version: response.version });
} else {
resolve({ installed: false });
}
}
);
});
};
visibilitychange 이벤트와 조합하면 탭 전환 시 자동 재확인이 가능하다.
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
// 디바운스: 빠른 탭 전환 시 과도한 ping 방지
await delay(500);
const result = await pingExtensionBackground();
if (result.installed && previousStatus !== 'OK') {
showExtensionInstalledToast(); // 설치 성공 알림
}
}
});
500ms 디바운스를 추가한 이유가 있다. 사용자가 탭을 빠르게 전환할 때 Extension에 과도한 요청이 가는 것을 방지한다.
상태 관리와 캐싱
Extension 상태는 세 가지로 구분한다.
| 상태 | 조건 | UX |
|---|---|---|
NOT_INSTALLED |
PING 응답 없음 | 설치 안내 토스트 + 웹스토어 링크 |
OUTDATED |
버전 < 요구 버전 | 업데이트 안내 토스트 (기능은 허용) |
OK |
버전 >= 요구 버전 | 정상 동작 |
OUTDATED 상태에서 기능을 완전히 차단하지 않은 이유가 있다. Minor/Patch 업데이트는 보통 하위 호환성을 유지한다. 경고만 표시하고 사용자 판단에 맡기는 게 UX 측면에서 낫다.
토스트 스팸을 방지하기 위해 두 가지 캐시를 적용했다.
// 상태 캐시: 3초간 재확인 생략
const healthCache = {
status: 'NOT_INSTALLED',
timestamp: 0,
cacheMs: 3000,
};
// 토스트 쿨다운: 10초간 중복 토스트 방지
let lastToastTime = 0;
const TOAST_COOLDOWN_MS = 10000;
캡처 버튼을 연타해도 토스트가 쏟아지지 않는다.
웹앱-Extension 버전 호환성 관리에서 중요한 점은 세 가지다.
- 버전별 분기보다 단일 버전: 레거시 호환을 포기하는 대신 유지보수 비용을 줄인다. 모바일 앱과 웹앱은 서버 의존도가 다르다. 웹앱에서 버전별 분기는 QA 지옥으로 가는 길이다.
- 이중 감지 채널: Content Script(
postMessage)와externally_connectable(runtime.sendMessage)을 조합. 페이지 로드 시와 탭 전환 시 모두 감지 가능하다. - Graceful Degradation: 구버전이라고 무조건 차단하지 않는다. Breaking Change가 있는 Major 업데이트만 차단하고, Minor/Patch는 경고로 처리한다.
이 패턴은 웹앱과 Extension이 메시지 기반으로 협력하는 모든 서비스에 적용할 수 있다. 핵심은 "Extension이 버전을 응답하게 하고, 웹앱이 판단한다"는 단방향 의존성이다.
프런트엔드 엔지니어, QA 엔지니어 그리고 디자이너를 위한
" ALL IN ONE " QA 서비스
https://pixeldiff.turtle-tail.com
'토이프로젝트' 카테고리의 다른 글
| postMessage로 다중 iframe 스크롤 동기화 구현하기 (0) | 2026.03.12 |
|---|---|
| 사이드 프로젝트 DB로 Supabase를 선택한 이유 (0) | 2026.03.05 |
| pnpm Workspace로 웹앱과 Chrome Extension 모노레포 구성하기 (0) | 2026.03.03 |
| 서버와 DB 리전, 같이 두면 얼마나 빨라질까? (0) | 2026.02.14 |
| Figma 스타일 스냅 가이드 시스템 구현하기 (0) | 2026.01.29 |
