브라우저 확장 프로그램을 지원하기 위해 웹 앱에서 Google 로그인을 제거한 이유
요약
브라우저 확장 프로그램과 웹 앱 간의 인증 동기화 문제를 해결하기 위해 Google 로그인을 제거한 기술적 배경을 다룹니다. Manifest V3 환경에서 확장 프로그램 ID 고정 문제와 Google OAuth 구현의 복잡성을 설명합니다.
핵심 포인트
- 확장 프로그램은 웹 앱과 별개의 Origin과 세션을 가짐
- Manifest V3에서 Google OAuth 구현 시 확장 프로그램 ID 고정 필요
- ID 고정을 위해 manifest.json의 key 필드 설정이 필수적임
- 인증 흐름의 복잡성으로 인해 사용자 경험을 위해 로그인을 제거하는 결정
MyPrompt는 프롬프트 관리자입니다. 프롬프트를 작성하고 정리하는 웹 대시보드와, 이를 ChatGPT, Claude, Gemini, Perplexity에 주입하는 브라우저 확장 프로그램으로 구성됩니다. 두 부분은 모두 동일한 Firestore 데이터베이스와 통신하며, 두 부분 모두 동일한 사용자가 로그인되어 있어야 합니다.
웹 앱이 먼저 출시되었으며, 이메일과 비밀번호, 그리고 "Google로 계속하기"와 같이 예상할 수 있는 로그인 옵션이 제공되었습니다. 확장 프로그램은 나중에 출시되었습니다. 확장 프로그램이 작동할 때쯤에는 Google 로그인이 사라진 상태였습니다. 단순히 확장 프로그램에서 누락된 것이 아니라, 웹 앱에서도 삭제되었습니다.
이는 역행하는 것처럼 들립니다. 보조 클라이언트를 수용하기 위해 작동 중인 제품에서 기능을 제거하는 것은 대개 그렇습니다. 이것이 우리가 그 결정을 내리게 된 논리이며, MV3가 우리에게 강제한 사항이기도 합니다.
문제: 확장 프로그램은 별개의 클라이언트입니다
브라우저 확장 프로그램은 웹사이트의 탭이 아닙니다. 자체적인 Origin, 자체적인 Storage, 자체적인 모든 것을 가집니다. 사용자가 웹 앱에서 가지고 있는 세션은 확장 프로그램에서 보이지 않습니다. 따라서 확장 프로그램은 독립적으로 인증해야 하며, 결과적으로 동일한 사용자에 대한 Firebase Auth 세션을 보유해야 합니다. 그렇지 않으면 Firestore 보안 규칙 (Security Rules)이 해당 사용자의 프롬프트를 전달하는 것을 올바르게 거부할 것입니다.
사용자가 Google로 가입했다면, 확장 프로그램에 입력할 비밀번호가 없습니다. 그들의 유일한 진입 경로는 Google입니다. 이는 확장 프로그램에 작동하는 Google OAuth 흐름이 필요함을 의미하며, 그렇지 않으면 해당 사용자들은 제품의 절반을 사용하지 못하게 됩니다.
그렇다면: Manifest V3 확장 프로그램 내에서 Google 로그인을 구현하는 것이 얼마나 어려울까요?
충분히 어려우며, 유난히 짜증 나는 이유 때문입니다
당연한 도구는 chrome.identity입니다. 하지만 두 가지 문제가 겹쳐 있습니다.
첫째, 확장 프로그램 ID (extension ID)입니다. "Chrome Extension" 유형의 Google OAuth 클라이언트(client)는 특정 확장 프로그램 ID에 등록됩니다. 이 ID는 확장 프로그램의 서명 키(signing key)로부터 파생됩니다. 개발 과정에서 압축 해제된(unpacked) 확장 프로그램은 해당 경로로부터 생성된 ID를 갖게 되는데, 이는 동료의 머신에서 생성된 ID와 다를 뿐만 아니라, Chrome 웹 스토어(Chrome Web Store)에 게시할 때 할당되는 ID와도 또 다릅니다. 이를 고정하기 위해서는 .pem 파일에서 공개 키(public key)를 추출하여 manifest.json의 key 필드에 붙여넣어야 합니다. 저희 README에서 해당 필드가 없을 때 어떤 일이 발생하는지 여전히 설명하고 있는 이유가 바로 이것입니다:
만약 본인의 빌드를 Chrome 웹 스토어에 게시할 계획이라면, 직접 서명 키를 생성하십시오 (
manifest.json의key필드가 없으면 Chrome이 자동으로 새로운 확장 프로그램 ID를 할당합니다).
이 중 어느 것도 문서화되지 않은 내용은 아닙니다. 단지 인증(auth) 코드를 한 줄이라도 작성하기 전에 수동으로 풀어내야 하는 순환 의존성(circular dependency)일 뿐입니다. 즉, OAuth 클라이언트는 ID를 필요로 하고, ID는 키에서 나오며, 키는 ID가 의미를 갖기 전에 먼저 생성되어 고정되어야 합니다. 처음 이 작업을 접하면 ID와 리다이렉트 URL(redirect URL)이 임의적인 것처럼 느껴질 수 있는데, 그 시점에서는 실제로 그렇기 때문입니다. 아직 그것들을 안정적으로 만드는 단계를 수행하지 않았기 때문입니다.
둘째, 그리고 결정적인 이유: 저희는 Firefox에서도 배포합니다. 저희의 매니페스트(manifest)에는 browser_specific_settings.gecko 블록이 포함되어 있으며, 웹 앱은 브라우저에 따라 두 스토어 모두에 링크를 연결합니다:
const CHROME_URL = 'https://chromewebstore.google.com'
const FIREFOX_URL = 'https://addons.mozilla.org/en-US/firefox/addon/myprompt/'
chrome.identity.getAuthToken은 Chrome 전용이므로, 가장 간단한 경로는 즉시 제외되었습니다. launchWebAuthFlow는 두 브라우저 모두에서 사용할 수 있지만, 두 브라우저 간에 서로 다른 자체적인 리다이렉트 URL(redirect-URL) 설정이 필요합니다. 이는 앞서 언급한 ID 작업이 한 번으로 끝나는 것이 아니라, 브라우저마다 개별적으로 수행해야 하는 작업임을 의미합니다.
따라서 실제 비용은 결코 "OAuth 설정"이 아니었습니다. 그것은 "한 명의 개발자가 운영하는 제품을 위해, 안정적인 식별자(stable identifiers)를 원하는 제3자 콘솔을 대상으로, 두 개의 브라우저에서 두 개의 서로 다른 OAuth 경로를 설정하고 유지 관리하는 것"이었습니다.
결정: 어디서나 하나의 인증 방식 사용
우리는 웹 앱에서 Google 로그인을 제거하고, 양쪽 모두 이메일과 비밀번호 방식으로 표준화했습니다.
솔직하게 말해야 할 부분은 이것입니다. 이는 가입 전환율(sign-up conversion) 측면에서 하향 조정(downgrade)이라는 점입니다. "Google로 계속하기"는 클릭 한 번이면 되고 새로 만들 비밀번호도 필요 없습니다. 반면 이메일과 비밀번호는 양식(form)을 작성해야 하며, 무료 도구의 경우 이러한 마찰(friction)은 퍼널(funnel) 상단에서 사용자를 잃게 만듭니다.
그럼에도 불구하고 우리는 클릭 횟수보다 더 중요하다고 판단된 두 가지 이유 때문에 이 결정을 내렸습니다.
- ID 설정 비용은 결코 멈추지 않았습니다. OAuth 클라이언트(client), 서명 키(signing key), 확장 프로그램 ID(extension ID), 그리고 리다이렉트 URL(redirect URL)을 일치시키는 것이 개념적으로 어려운 것은 아니지만, 모든 연결 고리가 정확해야 하며 브라우저마다 전체 과정을 다시 수행해야 하는 체인(chain)과 같습니다. 두 명의 사이드 프로젝트 팀에게 이 작업은 자연스러운 끝이 없으며, 사용자가 눈으로 확인할 수 있는 결과물도 만들어내지 못합니다.
- 계정이 서로 어긋났습니다. 자격 증명(credentials)을 명시적으로 연결하지 않는 한, Google 사용자 계정과 이메일 사용자 계정은 서로 다른 Firebase Auth 레코드가 됩니다. 계정 연결(account linking)이 없으면, 웹에서 Google로 가입한 사용자는 확장 프로그램에 접속할 방법이 전혀 없었으며, 이 실패는 우리 측에서 조용히 일어납니다. 그들은 버그를 신고하지 않습니다. 그냥 삭제할 뿐입니다.
인증 방식이 하나라는 것은 확장 프로그램의 로그인이 단 네 줄이면 된다는 뜻이며, 웹 앱과 동기화가 어긋날 일도 없다는 것을 의미합니다:
export async function login(email: string, password: string): Promise<void> {
await signInWithEmailAndPassword(auth, email, password)
}
만약 제품이 비용을 정당화할 수 있는 시점(실제 사용자, 실제 수요가 발생하는 시점)이 온다면, 처음부터 계정 연결(account linking)을 설계하여 양쪽 모두에 Google 로그인을 동시에 다시 도입할 것입니다. 이것은 우리가 못을 박아 영원히 닫아버린 문이 아닙니다.
MV3가 우리에게 강제한 것
OAuth를 포기했다고 해서 확장 프로그램 내의 Firebase Auth가 공짜가 된 것은 아니었습니다. 여전히 처리해야 할 두 가지가 남아 있었습니다.
웹 확장 프로그램 엔트리 포인트(entry point)를 사용하세요. Manifest V3는 원격 코드(remote code) 로딩을 금지하며, 표준 Firebase Auth 빌드는 원격으로 호스팅되는 헬퍼(helpers)를 호출합니다. SDK는 정확히 이 문제를 위해 별도의 엔트리 포인트를 제공합니다:
import { getAuth, connectAuthEmulator } from 'firebase/auth/web-extension'
이는 MV3를 준수하며, 원격에서 아무것도 로드하지 않고, 기본적으로 IndexedDB에 데이터를 유지(persist)합니다. 이는 단 한 줄의 변경 사항이지만 놓치기 쉬우며, 놓칠 경우 혼란스러운 오류를 발생시킵니다.
Firebase 앱 인스턴스에 이름을 지정하세요. 확장 프로그램의 콘텐츠 스크립트(content scripts)는 그 자체로 Firebase를 실행 중일 수 있는 페이지에서 동작합니다. 이름이 지정되지 않은 기본 앱은 호스트 페이지의 앱과 충돌할 수 있습니다:
export const firebaseApp = initializeApp(firebaseConfig, 'myprompt-extension')
아무도 기록하지 않는 버그: 팝업 플래시(popup flash)
이 문제는 확장 프로그램에 특화된 것이며, 수정하는 것보다 알아차리는 데 더 오랜 시간이 걸렸습니다.
사용자가 툴바 아이콘을 클릭할 때마다 팝업은 파괴되고 다시 생성됩니다. UI 상태를 유지하는 웜 프로세스(warm process)가 없습니다. 한편 onAuthStateChanged는 비동기적(asynchronous)입니다. 콜드 스타트(cold start) 시점에 Firebase는 아직 IndexedDB에서 세션을 복구하지 않았으므로, 몇 백 밀리초 동안 확장 프로그램은 아무도 로그인하지 않았다고 판단합니다.
단순하게 렌더링할 경우, 이는 로그인된 사용자가 팝업을 열 때마다 로그인 화면이 번쩍이는(flash) 현상을 보게 된다는 것을 의미합니다. 이는 제품이 고장 난 것처럼 느껴지게 만드는 작은 문제입니다.
해결책은 동기식에 가까운 빠른 경로(fast path)를 사용하는 것입니다. chrome.storage.local에 최소한의 사용자 레코드를 캐시(cache)하고, Firebase가 해결(resolve)되기 전에 이를 읽은 다음, onAuthStateChanged가 이를 수정하도록 하는 것입니다.
// 빠른 경로: Firebase가 비동기로 해결되기 전에 스토리지에서 캐시된 상태를 읽음
chrome.storage.local.get('mp_auth_user').then((result) => {
currentUser.value = (result['mp_auth_user'] as StoredUser | null) ?? null
...
그 후 팝업은 무엇을 렌더링할지 결정하기 전에 단일 authReady 플래그(flag)를 기다립니다. 따라서 첫 번째 페인트(paint)는 실제 화면이거나 의도된 로딩 상태 중 하나가 되며, 결코 잘못된 화면이 나타나지 않습니다.
캐시된 복사본은 편의를 위한 것일 뿐, 인증 정보(credential)가 아닙니다. 여기에는 uid, 이메일, 그리고 표시 이름(display name)이 포함되어 있습니다. Firestore 규칙은 Firebase ID 토큰만을 신뢰하며 그 외의 것은 신뢰하지 않으므로, 조작된 캐시는 공격자에게 오해를 불러일으키는 팝업을 보여줄 수는 있어도 데이터는 전혀 얻지 못하게 합니다.
핵심 요약 (Takeaway)
기존 웹 앱에 브라우저 확장 프로그램(browser extension)을 추가하려는 경우, 확장 프로그램 코드를 작성하기 전에 로그인 방식(sign-in methods)을 먼저 점검하십시오. 웹에서 지원하는 모든 인증 제공자(auth provider)는 이제 당신이 배포하는 모든 브라우저에서, 영구적으로, 서로 다른 보안 모델을 가진 환경에서도 지원해야 하는 제공자가 됩니다.
때로는 지원 범위를 줄이는 것이 올바른 선택일 수 있습니다.
MyPrompt는 MIT 라이선스이며, 계정 없이 Firebase 에뮬레이터(emulator)에서 실행됩니다: github.com/It-Codes-Two/my-prompt
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기