Supabase 세션 관리가 복잡해진 이유: 함수 호출마다 setSession을 했던 실수

Supabase 세션 관리가 복잡해진 이유: 함수 호출마다 setSession을 했던 실수

2026년 2월 2일

한스푼 익스텐션에서 Supabase로 데이터를 동기화할 때, 세션 설정 코드가 여러 곳에 흩어져 있었습니다.

처음에는 “필요한 곳에서 그냥 호출하면 되지”라고 생각했지만, 호출 지점이 늘어나면서 문제가 드러났습니다. 이 글은 세션 관리를 중앙화하고, 타입 추론 문제를 해결하면서 배운 것들을 정리한 글입니다.

문제 상황

한스푼 익스텐션은 사용자가 웹페이지에서 하이라이트한 내용을 Supabase에 동기화하는 기능이 있다. 이를 위해서는 Supabase 클라이언트에 사용자 세션(access_token, refresh_token)을 설정해야 한다.

초기 구현에서는 syncPostToSupabase 함수 내부에서 직접 세션을 설정했다:

export async function syncPostToSupabase(
  postId: string,
  session: Session | null,
) {
  if (!session) {
    throw new Error("로그인 세션이 없습니다");
  }

  const { error: sessionError } = await supabase.auth.setSession({
    access_token: session.access_token,
    refresh_token: session.refresh_token,
  });

  if (sessionError) {
    console.error("Supabase 세션 설정 실패:", sessionError);
    return;
  }

  // 이하 동기화 로직...
}

이 방식은 동기화 함수가 하나뿐일 때는 문제가 없었다.

하지만 기능이 늘어나면서

  1. 하이라이트 삭제 시에도 동기화 필요
  2. 공유 링크에서 하이라이트 추가 시에도 동기화 필요
  3. background script에서도 세션 접근 필요

결국 setSession을 호출해야 하는 곳이 계속 늘어났고, 동일한 세션 설정 로직이 여러 파일에 중복되기 시작했다.

내가 처음 한 생각

“Supabase API를 호출하기 전에 setSession만 해주면 되니까, 필요한 곳에서 각자 호출하면 되겠지.”

이 생각에는 두 가지 맹점이 있었다.

  1. 중복 코드 문제: 세션 설정 로직이 파일마다 반복됨
  2. 타입 추론 문제: browser.storage.local.get()의 반환 타입이 any에 가까워서, Session 타입을 매번 직접 캐스팅해야 했음
// 매번 이렇게 타입을 지정해야 했다
const { session } = await browser.storage.local.get<{ session: Session }>(
  "session",
);

실제로 해 본 선택

  1. initSupabaseSession으로 초기화 로직 분리

세션 설정을 한 곳에서 관리하도록 initSupabaseSession 함수를 만들었다:

async function setSupabaseSession(session: Session | null) {
  if (!session) {
    await supabase.auth.signOut();
    return;
  }

  const { error } = await supabase.auth.setSession({
    access_token: session.access_token,
    refresh_token: session.refresh_token,
  });

  if (error) {
    console.error("Supabase 세션 설정 실패:", error);
  }
}

export async function initSupabaseSession() {
  const { session } = await browser.storage.local.get<{ session: Session }>(
    "session",
  );

  if (session) {
    await setSupabaseSession(session);
  }

  // 세션 변경 감지 - 로그인/로그아웃 시 자동 반영
  browser.storage.onChanged.addListener((changes) => {
    if ("session" in changes) {
      setSupabaseSession(changes.session.newValue as Session | null);
    }
  });
}

이제 background script 초기화 시점에 initSupabaseSession()을 한 번만 호출하면, 이후에는 세션이 자동으로 유지된다. 개별 함수에서는 더 이상 setSession을 호출할 필요가 없다.

  1. 타입 안전한 브라우저 스토리지 래퍼

browser.storage.local의 타입 추론 문제를 해결하기 위해 제네릭 래퍼를 만들었다.

export const storage = {
  get: async function get<T>(key: string): Promise<T | undefined> {
    const result: Record<string, T> = await browser.storage.local.get(key);
    return result[key];
  },
  set: async function set<T>(key: string, value: T): Promise<void> {
    await browser.storage.local.set({ [key]: value });
  },
  remove: async function remove(key: string): Promise<void> {
    await browser.storage.local.remove(key);
  },
  subscribe: function subscribe<T>(
    key: string,
    listener: (newValue: T | undefined, oldValue: T | undefined) => void,
  ): () => void {
    const handleChange = (
      changes: Record<string, { oldValue?: T; newValue?: T }>,
    ) => {
      if (key in changes) {
        listener(changes[key].newValue, changes[key].oldValue);
      }
    };
    browser.storage.onChanged.addListener(handleChange);
    return () => browser.storage.onChanged.removeListener(handleChange);
  },
};

이제 세션을 가져올 때 타입이 자동으로 추론된다.

// Before
const { session } = await browser.storage.local.get<{ session: Session }>(
  "session",
);

// After
const session = await storage.get<Session>("session");

지금 다시 해본다면?

처음부터 이런 사고의 흐름으로 갔어야 했나라는 생각이 스치기도 하였다. 하지만 생각해보면 초반에는 딱 한 군데에서만 사용했기에 굳이 setSession을 따로 중앙화까지는 할 필요는 없었을 것 같다.

대신 browser.storage.local의 타입 추론 문제는 내가 놓쳤던 부분이었기 때문에, 앞으로 이런 경우 랩퍼를 미리 구축해두어야 겠다는 생각을 했다.

비고

  • 세션(Session)이 필요한 이유

    • Supabase는 Row Level Security(RLS)를 통해 “이 데이터는 이 사용자만 접근 가능”을 DB 레벨에서 강제한다. 이를 위해 API 요청 시 사용자가 누구인지 증명해야 하는데, 이때 사용되는 것이 세션이다.
  • 세션에는 다음이 포함된다.

    • access_token: JWT 형태, 사용자 ID와 권한 정보를 담고 있음. 짧은 만료 시간(보통 1시간)
    • refresh_token: access_token이 만료됐을 때 새 토큰을 발급받기 위한 토큰. 긴 만료 시간
  • 브라우저 익스텐션에서의 세션 저장

    • 일반 웹앱에서는 Supabase 클라이언트가 localStorage에 세션을 자동 저장한다.
    • 하지만 브라우저 익스텐션은…
      • content script, background script, popup 등 여러 컨텍스트가 분리되어 있음
      • 각 컨텍스트 간 상태 공유가 자동으로 되지 않음
      • browser.storage.local을 사용해야 컨텍스트 간 세션 공유 가능

추가 이슈

subscribe 패턴을 사용하는 이유

처음에는 async 컴포넌트에서 await storage.get()으로 세션을 가져왔다.

// 문제가 있던 코드
export const PostCard = async ({ post }: { post: LocalPost }) => {
  const session = await storage.get("session");
  const isLoggedIn = !!session;
  // ...
};

하지만 이 방식은 컴포넌트가 마운트될 때 한 번만 값을 가져온다. 이후 사용자가 로그인해서 storage의 session 값이 변경되어도, 이미 마운트된 컴포넌트는 re-render되지 않는다. React 상태가 아니기 때문이다.

이를 해결하려면 storage 변경을 감지해서 React 상태를 업데이트해야 한다.

// 수정된 코드
export const PostCard = ({ post }: { post: LocalPost }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    // 초기값 설정
    storage.get("session").then((session) => {
      setIsLoggedIn(!!session);
    });

    // storage 변경 시 상태 업데이트 → re-render
    return storage.subscribe("session", (newValue) => {
      setIsLoggedIn(!!newValue);
    });
  }, []);
  // ...
};

storage.subscribe가 storage 변경을 감지하고 setState를 호출하면, React가 컴포넌트를 re-render하여 UI가 업데이트된다.

실제 변경 내역