구글 플라이트용 비자 요구 오버레이 제작 — 크롬 확장 프로그램 작동 방식
출처: Dev.to
멀티 국가 여행을 계획할 때마다 나는 같은 루프에 빠진다. 외교부 웹사이트를 열고, 3단계 드롭다운을 탐색한 뒤, 올바른 여권/목적지 조합을 찾고, 일정에 있는 모든 국가에 대해 이 과정을 반복한다.
이 과정이 여러 탭을 열고 5분 정도 클릭을 해야 할 이유는 없다. 그래서 나는 EntryCheck라는 Chrome 확장 프로그램을 만들었다. 이 확장은 Google Flights 결과에 비자 요구 사항 배지를 직접 삽입한다.
EntryCheck는 Google Flights를 탐색하는 동안 목적지 카드 위에 색상 배지를 오버레이한다. 팝업에서 국적을 한 번만 설정하면, 나머지는 확장 프로그램이 알아서 처리한다.
다섯 가지 상태
- 🟢 비자 면제 — 비자 없이 입국 가능
- 🟡 도착 비자 — 국경에서 스탬프를 받음
- 🔵 eVisa — 비행 전 온라인으로 신청
- 🟠 비자 필요 — 대사관에서 신청
- 🔴 입국 금지 — 입국이 허용되지 않음
비자 데이터는 확장 프로그램에 번들된 visa-matrix.json 파일에 로컬로 저장된다. 190개 이상의 여권/목적지 조합을 두 단계 조회 구조로 가지고 있다:
export async function getVisaRequirement(
passport: string,
destination: string,
): Promise {
const matrix = await loadVisaMatrix(); // 최초 호출 시에만 로드, 이후 캐시
const result = matrix[passport.toUpperCase()]?.[destination.toUpperCase()];
if (result) return result;
// EU 블록: 목적지가 EU 회원국이면 EU 항목을 사용
if (isEUCountry(destination)) return matrix[passport.toUpperCase()]?.['EU'] ?? null;
return null;
}
Lazy‑loading 덕분에 200KB 크기의 JSON 파일이 시작 시점에 콘텐츠 스크립트를 차단하지 않는다. 첫 번째 조회에서 파싱 비용을 지불하고, 이후 조회는 캐시된 객체를 바로 사용한다.
외부 API가 없으므로 지연 시간도 없고, 속도 제한도 없으며, 확장 프로그램은 오프라인에서도 동작한다—공항에서도 유용하다.
Google Flights는 싱글 페이지 앱이다. 목적지 이름이 나타났다 사라졌다가 전체 페이지가 새로고침되지 않기 때문에 DOMContentLoaded 이벤트는 한 번만 발생한다.
해결 방법은 500ms 디바운스를 적용한 MutationObserver이다:
const observer = new MutationObserver(() => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => injectBadges(nationality), 500);
});
observer.observe(document.body, { childList: true, subtree: true });
디바운스가 중요한 이유는 Google Flights가 검색 애니메이션과 자동완성 중에 초당 수십 번의 마이크로 변화를 발생시키기 때문이다. throttling 없이 하면 하나의 검색에 대해 수백 번 injectBadges()가 호출된다.
Google의 DOM은 A/B 테스트에 따라 자동 생성된 클래스명을 사용한다. 나는 폴백 체인을 갖는 다중 선택자 워터폴을 사용한다:
const selectors = [
'[jsname="ik4t5"]', // flight destination text (most stable)
'[role="listitem"] h2', // Travel explore cards
'[role="listitem"] h3',
'[aria-label*="Flight to"]', // aria-label match
'[data-destination-name]', // data attribute variant
];
각 매치된 요소는 처리 후 data-entrycheck-injected 속성을 부여해 다음 관찰 사이클에서 건너뛰게 한다—그렇지 않으면 매 DOM 변형마다 배지가 다시 삽입된다.
목적지 텍스트는 사람에게 친숙한 이름(“Japan”, “Paris”, “United States”)으로 제공된다. 비자 매트릭스는 ISO 3166‑1 alpha‑2 코드(“JP”, “FR”, “US”)를 사용한다. countryNameToCode 함수는 번들된 사전을 이용해 두 형식 사이를 매핑한다.
부분 매칭도 도움이 된다: Google은 때때로 국가 대신 도시 이름을 표시한다(“Tokyo” → “JP”, “Paris” → “FR”). 사전이 두 경우를 모두 매핑한다.
관찰자가 더 이상 필요 없을 때는 반드시 해제한다:
window.addEventListener('unload', () => {
observer.disconnect();
});
이것을 잊어버리는 것이 콘텐츠 스크립트에서 가장 흔한 메모리 누수 원인이다. subtree: true 옵션으로 document.body를 감시하면, 지속적인 관찰자가 내부 bookkeeping을 많이 쌓는다.
Chrome 웹 스토어: https://chromewebstore.google.com/detail/gfbdmeieifamgheecbmdpadniofodkfa
국제 여행을 예약하면서 비자가 필요한지 20분 이상 고민한 적이 있다면, 한 번 사용해 보길 바란다.