
1. 이 전 코드
이전에는 클라이언트용만 사용하고 있었다
React Native에서는 아래와 같이 토큰을 웹뷰측에 넘겨주고 있었다
export default function HomeScreen() {
const webviewRef = useRef<WebView>(null);
const params = useLocalSearchParams();
const { accessToken, refreshToken } = params;
const injectSession = () => {
if (webviewRef.current && accessToken && refreshToken) {
const message = JSON.stringify({
type: "SESSION",
accessToken,
refreshToken,
});
webviewRef.current.postMessage(message);
}
};
return (
<View style={{ flex: 1 }}>
<WebView
ref={webviewRef}
onLoadEnd={() => {
setTimeout(injectSession, 0);
}}
// ...
/>
</View>
);
}
로그인 성공후 네이티브쪽에서 리다이렉트가 이루어지고,
params로 받은 토큰들을 postMessage를 통해서
웹뷰 로드가 끝나는 시점(onLoadEnd)에 넘겨주고 있다
그리고 웹뷰(Next.js)에서는 브릿지를 만들어서
페이지 접근 시 아래 코드가 실행되게 만들었다
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import { useState, useRef, useEffect } from "react";
import { QueryClient } from "@tanstack/react-query";
import { supabase } from "./supabase";
export const useAuthBridge = (queryClient: QueryClient) => {
const [isReady, setIsReady] = useState(false);
const isSettingSession = useRef(false);
useEffect(() => {
const handleMessage = async (event: any) => {
let data;
try {
data =
typeof event.data === "string" ? JSON.parse(event.data) : event.data;
} catch (e) {
return;
}
if (data?.type === "SESSION") {
isSettingSession.current = true;
const { accessToken, refreshToken } = data;
const { error } = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
});
if (error) {
console.error("세션 주입 실패:", error.message);
(window as any).ReactNativeWebView?.postMessage(
JSON.stringify({ type: "LOGOUT_REQUIRED" }),
);
} else {
// 세션 주입 성공 시 쿼리 초기화
await queryClient.invalidateQueries();
setIsReady(true); // ★ 세션 주입 성공 시점에만 true로 변경
}
isSettingSession.current = false;
}
};
// 안전장치: 앱에서 메시지가 안 오더라도 기존 로컬 세션이 있다면 진행
const safetyTimer = setTimeout(() => {
// 이미 세션 주입에 성공해서 준비가 끝났다면 아무것도 하지 않음
if (isReady) return;
supabase.auth.getSession().then(({ data: { session } }) => {
if (session) {
setIsReady(true);
} else {
// 2초가 지났는데 로컬 세션도 없고, 앱에서도 SESSION 메시지가 아직 안 왔을 때만 실행
console.warn("2초간 세션 확인 불가: 로그아웃 요청 전송");
(window as any).ReactNativeWebView?.postMessage(
JSON.stringify({ type: "LOGOUT_REQUIRED" }),
);
}
});
}, 1000);
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(async (event, session) => {
// 1. 세션 설정 중일 때는 로그아웃 체크 무시
if (isSettingSession.current) return;
// 2. 중요: 아직 세션 주입 시도가 완료되지 않았다면(isReady가 false라면)
// SIGNED_OUT 이벤트가 발생해도 로그아웃 처리하지 않음 (첫 진입 시 튕김 방지)
if (!isReady && (event === "SIGNED_OUT" || !session)) {
console.log("세션 주입 대기 중... 로그아웃 체크 유예");
return;
}
// 3. 세션이 완전히 없는 경우 (이미 주입 시도가 끝난 후라면 진짜 로그아웃임)
if (event === "SIGNED_OUT" && !session) {
(window as any).ReactNativeWebView?.postMessage(
JSON.stringify({ type: "LOGOUT_REQUIRED" }),
);
return;
}
});
window.addEventListener("message", handleMessage);
document.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
document.removeEventListener("message", handleMessage);
subscription.unsubscribe();
clearTimeout(safetyTimer);
};
}, [queryClient, isReady]); // isReady를 의존성에 추가하여 상태 변화 감지
return { isReady };
};
이 로직이 왜 이렇게 됐냐면...
일단 네이티브쪽 Supabase SDK와 웹뷰쪽 Supabase SDK는 별개다
그래서 서로 동기화를 해줘야해서 위와 같이 브릿지 역할이 필요했다
근데 생각보다 동기화가 간단하진 않았다
내가 여기서 새롭게 깨달은 점은 웹뷰는 내가 로그아웃 또는 토큰이 만료 되었다고 해서
리다이렉트 해줬다고 웹뷰 브라우저가 자동으로 종료되거나 메모리에서 해제되는게 아니었던 것이다
그러다보니 같은 사용자가 ID만 달리 쓰게될 경우를 대비해 데이터를 수동으로 날려줘야했었다
그래서 로그아웃 후 다른 ID로 로그인할때 기존에 캐싱된 데이터를 모두 무효화 해야하다보니 코드도 방대해졌고
당시에 웹뷰 로드 시점과 API 호출 시 간극이 있었던걸로 기억한다
(API 호출 시점이 더 빨라서 세션이 없다고 판단해서 로그인 페이지로 무한 리다이렉트 됐었나 그랬었다)
그래서 안전장치 로직도 더해지면서 사용자가 기다려야하는 문제도 발생했었다
이게 출시 후 갑자기 발견한 이슈여서 네이티브쪽 코드를 안건드리고 해결하려다 보니
(그럼 다시 심사 받아야해서)
Next.js쪽 코드만 죽어라 조지면서 이 지경이 된거다
아무튼 이 코드는 클라이언트용이며 로컬 스토리지 저장이 기반이기에
서버 컴포넌트에서 Supabase를 호출할 경우 토큰을 읽을 수 없는 문제가 발생한다
이제 SSR로 바꿔보자
2. WebView(Next.js) Supabase SSR 설정
가장 먼저 웹뷰의 본채인 Next.js에 Supabase SSR 설정을 해줘야한다
이건 공식문서에 매우 친절이 자세히 설명이 되어있다 [공식문서]
Creating a Supabase client for SSR | Supabase Docs
Configure your Supabase client to use cookies
supabase.com
참고로 proxy.ts 파일은 Next.js 프로젝트 최상위에 위치해야한다
이전에 난 미들웨어로 알고 있었는데 또 그 사이에 업데이트 돼서 바뀐듯 하다
3. React Native Supabase 수정
React Native에서 라이브러리 없이 쿠키를 설정할 수 있으나
네이티브쪽 코드를 직접 작성해야한다는 단점이 있다
저는 네이티브쪽 언어 모르기에^^
초간단하게 라이브러리를 사용했다
npm install react-native-nitro-cookies react-native-nitro-modules
기존에 유명했던 라이브러리는 중단되었고
위 라이브러리 쓰라고 하더군요
<View style={{ flex: 1 }}>
<WebView
ref={webviewRef}
sharedCookiesEnabled={true}
thirdPartyCookiesEnabled={true}
webviewDebuggingEnabled={true}
// ...
/>
</View>
그리고 웹뷰에 cookie가 공유될 수 있도록 위와 같이 옵션을 추가합니다
export const setSupabaseCookie = async (session) => {
const url = "/* ... */";
const projectId = "/* ... */";
const cookieName = `sb-${projectId}-auth-token`;
try {
const jsonString = JSON.stringify(session);
const cookieValue = encodeURIComponent(jsonString);
await NitroCookies.set(url, {
name: cookieName,
value: cookieValue,
path: "/",
secure: false, // http 환경이므로 false
httpOnly: false, // SDK가 읽어야 하므로 false
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toUTCString(),
});
} catch (e) {
console.error(e);
}
};
일단 성공한 코드입니다
4. 결과까지의 과정
사실 이 짧은 코드 하나를 찾는데 오래걸렸습니다...
맨 처음에 AI가 cookie 값으로 아래와 같이 설정하라고 하더군요
const cookieValue = encodeURIComponent(JSON.stringify([session.access_token, session.refresh_token]));
그랬더니 아래와 같이 Next.js Supabase에서 에러가 났습니다
Error [AuthApiError]: Invalid Refresh Token: Refresh Token Not Found
그리고 쿠키에 저장된 값도 base64-로 시작되는데 이게 맞나 싶었습니다
근데 제미나이는 맞다고 했음...ㅋ
수파베이스에서 쿠키값을 어떤 규격으로 저장 해야하는지 검색을 해도 뜨는게 없어서 답답했습니다
(내가 검색력이 떨어지는걸까...)
그러면서 제미나이가 쿠키값 로직을 아래처럼도 바꿔보라고 함
const tokenArray = [session.access_token, session.refresh_token];
const base64Value = Buffer.from(JSON.stringify(tokenArray)).toString('base64');
const cookieValue = `base64-${base64Value}`;
// 이런 로직도 시킴
const rawJsonArray = JSON.stringify(tokenArray);
const jsonWithQuotes = `"${rawJsonArray.replace(/"/g, '\\"')}"`;
const finalValueToEncode = JSON.stringify(rawJsonArray);
const base64Value = btoa(finalValueToEncode);
const cookieValue = `base64-${base64Value}`;
아무튼 뭔가 쿠키값도 이상한 것 같고,
갑자기 웹뷰쪽 브라우저에 쿠키값이 저장조차도 안되기 시작했음
분명 네이티브쪽에서 잘 보내고 있고, Next.js 프록시에도 찍히는데
쿠키가 저장이 안되는 현상이 발생하고,
내가 어거지로 저장해도 새로고침하면 사리지거나 이상 현상 발생...!
(아마 내가 브라우저 단에서 직접 강제로 주입한 쿠키가 계속 사라지는건
Supabase SDK가 이상한 쿠키다라고 생각하며 강제 삭제하는 로직을 내부적으로 탄게 아닐까 싶다)
근데 웃긴건 제미나이는 그 와중에 액세스 토큰이랑
리프레시 토큰을 분리해서 다른 이름으로 각각 쿠키에 저장해보라는 말을 함...
그러다가 한번은 프록시에 로직을 추가하라고 하는데..
(진심 계속 제미나이는 이상한 로직 엄청 뱉고, 더 산으로 가고, 맞다고 계속 우기는데,
검색해도 뭔가 뜨는게 없어서 답답했었습니다)
const cookieName = "sb-royrraqcazneifejftrk-auth-token";
const authCookie = request.cookies.get(cookieName);
if (authCookie?.value && authCookie.value.startsWith("base64-")) {
try {
const rawData = authCookie.value.replace("base64-", "");
const decoded = Buffer.from(rawData, "base64").toString();
const [at, rt] = JSON.parse(decoded);
// ✅ 서버가 쿠키를 받자마자 SDK 세션에 강제로 꽂아넣습니다.
// 이렇게 하면 SDK의 자동 파싱 로직이 실패해도 유저 정보를 가져올 수 있습니다.
await supabase.auth.setSession({
access_token: at,
refresh_token: rt,
});
console.log("🛠 쿠키 감지: 수동 세션 복구 성공");
} catch (e) {
console.error("🛠 세션 복구 실패:", e);
}
}
제미나이가 제안한 위 코드는 한마디로 강제 수동으로 주입하는 형태
이러니까 Next.js Supabase SDK가 웹뷰쪽 브라우저에 쿠키를 설정함
근데 전 이해가 안갔습니다
네이티브쪽에 쿠키를 보내면 웹뷰 프록시가 그걸 받아서 쿠키를 설정하고
Supabase SDK는 그 쿠키를 인식하고 API 호출시 쓰는게 정상 아닌가?
강제로 주입할거면 쿠키는 굳이 왜 만들어서 보내지?? 라는 의문이 생기더라고요
이럴거면 네이티브쪽에서 쿠키 안 만들고 토큰만 받아서 웹뷰에서 쿠키를 굽고말지
이런 설정을 왜 했어 그럼 내가??? 이런 생각이 들어서
어떻게든 고쳐보기 시작합니다
그 결과 무조건 쿠키값이 문제다란 생각이 들었고
쿠키값 포맷을 알기위해 Supabase SSR example을 검색해서
깃허브에 플젝 하나 clone해서 테스트를 해봤습니다
그렇게 설정된 쿠키값을 제미나이에게 던졌고
결론은 (다시 위 로직으로 돌아가서)
발급받은 session 데이터 전체를 받아 URL 인코딩된 JSON을 그대로 주입하는 것!
해당 함수(setSupabaseCookie)를 이젠 각각 필요한 곳에 호출하면 된다
로그인 후 세션 발급 받아서 웹뷰쪽으로 리다이렉트 될 때 호출하는 등... 그러면 되고
(코드 순서는 리다이렉트 전에 위치해야함)
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(async (event, session) => {
if (session) {
// 세션이 생기거나 갱신되면 쿠키 업데이트
await setSupabaseCookie(session);
} else if (event === "SIGNED_OUT") {
// 로그아웃 시 웹뷰 쿠키도 삭제 로직이 필요하다면 여기서 수행
}
});
return () => {
subscription.unsubscribe();
};
}, []);
네이티브쪽 최상위 페이지 컴포넌트에 위와 같은 코드를 추가해놓는 것도 좋다
여기까지 React Native, Next.js 세팅이 완벽히 되어있다면

이와 같이 사파리 켜서 시뮬레이터 디버깅을 해보면 쿠키가 잘 저장되어 있을 것이다
5. 회고
지금 와서 보면 코드가 그리 어려운게 아닌데
뭘 그렇게 헤매고, 롤백까지 해가고 그랬을까 싶다...
이게 다 어떤 순서로 코드가 실행되고 세팅되고의 그런 선행과정들을 내가 너무 생략해서 일어난 일인 것 같다
AI한테도 결과를 해달라고 말하기보단 이 결과가 나오려면 뭐가 먼저 되어있어야 해?
뭐가 세팅이 되어야해? 그 값이 어디에 어떻게 위치되어야지 그 다음이 인식 하는거야?
지금 이런 상태인데 그래서 그 다음이 안되는거야? 그럴려면 어디 코드를 봐야해?
근데 원래 이 과정은 이렇게 동작해야 하는거 아니야? 지금 그건 편법아니야?
라는 식으로 과정을 먼저 물으니 해결할 수 있었던 것 같다
제발 조급해 하지말고 찬찬히 해보자...!
답이 없음 어떻게든 해보자라며 새벽까지 보지말고
차라리 산책 좀 하고 환기 시키고 다시 해보자...
2주전에 롤백한 시도가 지금은 되잖니..!!!ㅠㅠㅠ
그 다음으로는 React Query를 걷어내고 Next.js 캐싱으로 전환하는 과정을 진행할 예정입니다
'Project > 로그트립' 카테고리의 다른 글
| TanStack Query를 Next.js 캐싱 전략으로 바꾸기 (1) | 2026.03.08 |
|---|---|
| 하이브리드 앱 구축 및 출시 회고 | React Native + Next.js + Supabase 조합 (0) | 2026.03.04 |
댓글