크롬 익스텐션에서 BrowserRouter가 작동하지 않는 이유

크롬 익스텐션에서 BrowserRouter가 작동하지 않는 이유

2026년 2월 24일

recall은 코딩 테스트 문제를 기록하고 복습하는 크롬 익스텐션이다.

내부에 여러 페이지가 있어 라우팅이 필요했다. 웹 앱에서 늘 쓰던 BrowserRouter를 붙였는데, 팝업을 열자마자 빈 화면이 나왔다.

1. 문제 상황

recall 익스텐션은 /, /problems, /live, /settings 등 여러 화면을 전환해야 한다.

// Router.tsx
export function AppRouter() {
  return (
    <Layout>
      <Routes>
        <Route path="/" element={<DashboardPage />} />
        <Route path="/problems" element={<ProblemsPage />} />
        <Route path="/problems/:id" element={<ProblemDetailPage />} />
        <Route path="/live" element={<LiveCodingPage />} />
        <Route path="/settings" element={<SettingsPage />} />
      </Routes>
    </Layout>
  );
}

SPA에서 라우팅이 필요하면 React Router를 쓴다. 웹 앱이면 BrowserRouter를 감싸면 끝이다. 익스텐션도 React로 만든 SPA니까 당연히 똑같이 하면 되겠지, 라고 생각했다.

2. 내가 처음 한 생각

// 처음에 이렇게 붙였다
import { BrowserRouter } from "react-router";

export function App() {
  return (
    <BrowserRouter>
      <AppRouter />
    </BrowserRouter>
  );
}

BrowserRouter는 HTML5 History API(pushState, replaceState)를 사용한다. 주소창 URL이 /about처럼 바뀌고, 뒤로가기도 된다.

웹 앱에서는 이게 자연스럽다. 그런데 익스텐션 팝업을 열면 주소창에는 이런 게 찍힌다.

chrome-extension://abcdefg1234567890/popup.html

pushState로 URL을 바꾸면 어떻게 될까? 브라우저는 이 URL을 실제로 서빙할 서버를 찾으려 한다. 익스텐션 팝업에는 서버가 없다. 결과는 빈 화면이었다.

3. 실제로 해 본 선택

React Router의 라우터는 크게 세 가지다.

라우터URL 동기화 방식서버 필요 여부
BrowserRouterHTML5 History API필요 (SEO 및 경로 유지 목적)
HashRouter#/path (URL 해시)불필요
MemoryRouter메모리 내부 관리불필요

BrowserRouter

window.location과 History API를 완전히 동기화한다. URL이 깔끔하지만(/about), 서버에서 해당 경로로 오는 요청을 처리할 수 없으면 404가 난다.

HashRouter URL 해시 부분(#)을 라우팅에 활용한다. https://example.com/#/about 형태다. 해시 뒤 부분은 서버로 전송되지 않아 서버 설정이 필요 없다. 익스텐션에서 쓸 수도 있지만, 팝업이 닫혔다가 다시 열리면 해시가 초기화된다.

MemoryRouter 라우팅 상태를 URL이 아닌 메모리에만 저장한다. 주소창에 아무것도 반영되지 않는다. 브라우저 환경이 아니거나 URL을 쓸 수 없는 환경(React Native, 테스트, 크롬 익스텐션)을 위해 만들어졌다.

익스텐션 팝업에서 URL은 아무 의미가 없다. 팝업은 닫히면 상태가 사라지고, URL을 바꿔도 저장되지 않는다. 그래서 MemoryRouter가 정답이다.

// App.tsx — 현재 구조
import { MemoryRouter } from "react-router";

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MemoryRouter initialEntries={["/"]}>
        <AppRouter />
      </MemoryRouter>
    </QueryClientProvider>
  );
}

initialEntries={[”/”]} 옵션으로 팝업이 열릴 때 항상 대시보드부터 시작하게 했다.

여담: React Router v6의 새 철학

React Router v6.4부터 “Data Router”라는 개념이 생겼다. createBrowserRouter로 라우터를 만들면 loader, action,

같은 데이터 패칭 기능이 라우터 레벨에서 통합된다. Remix에서 가져온 철학이다.

// v6.4+ Data Router 방식
const router = createBrowserRouter([
  {
    path: "/",
    element: <Dashboard />,
    loader: async () => fetchData(), // 라우트 전환전에 데이터 로드
  },
]);

v5까지는 라우터가 “어디로 가는가”만 담당했다면, v6.4 이후는 “어디로 가면서 무엇을 가져오는가”를 같이 담당한다.

다만 MemoryRouter는 Data Router 방식이 아니라 컴포넌트 방식이다. 익스텐션처럼 URL이 의미 없는 환경에서는 컴포넌트 방식으로도 충분하다.

4. 결과 (성공)

MemoryRouter로 바꾸자 팝업이 정상적으로 열렸다.

/, /problems, /live, /settings 모두 잘 전환됐고, useNavigate, useParams 같은 훅도 그대로 쓸 수 있었다. API는 BrowserRouter와 완전히 동일하다.

한 가지 주의할 점이 있다. MemoryRouter는 팝업이 닫히면 히스토리가 사라진다. /settings 페이지를 열어두고 팝업을 닫았다가 다시 열면 /로 돌아온다.

이건 의도한 동작이다. 팝업의 라이프사이클상 자연스럽다. 만약 마지막 위치를 기억하고 싶다면 chrome.storage에 현재 경로를 저장하고 initialEntries에 복원하는 방식을 써야 한다.

5. 지금 다시 해본다면?

처음부터 환경을 먼저 확인했을 것이다.

웹 앱과 크롬 익스텐션은 겉보기에 둘 다 React SPA지만, 실행 환경이 다르다. 익스텐션 팝업에는 서버도, 의미있는 URL도 없다.

“React Router를 써야 한다”는 생각보다 “이 환경에서 URL이 어떻게 동작하는가”를 먼저 물었다면 삽질 없이 바로 MemoryRouter를 선택했을 것이다.

라우터 선택 기준은 단순하다.

  • URL을 서버와 동기화해야 한다 → BrowserRouter
  • URL 해시만 쓰고 서버 설정은 하기 싫다 → HashRouter
  • URL이 아예 의미 없는 환경이다 → MemoryRouter

크롬 익스텐션은 세 번째다.