문제: 캐스팅하고 관리자 사이트로 가면 다시 로그인
Voidx AI 플랫폼은 사용자가 자신의 AI 에이전트를 캐스팅(생성)하는 메인 사이트(voidx.ai)와, 캐스팅한 에이전트를 관리·운영하는 어드민(admin.voidx.ai)으로 나뉘어 있습니다. 사용자 입장에서는 한 제품이지만, 기술적으로는 별개의 Next.js 앱이고 도메인도 다릅니다.
문제는 캐스팅을 마치고 "내 에이전트 관리"로 넘어가는 순간이었습니다. 어드민 도메인에서 NextAuth 세션이 따로 잡혀 있다 보니, 사용자에게는 같은 서비스인데 다시 로그인 화면이 뜨는 경험이 발생했죠.
정식 SSO(OAuth Authorization Code Flow, SAML)를 도입하기엔 부담이 컸습니다.
내부 서비스 두 개를 잇는 일에 이만한 인프라는 과했습니다. "같은 루트 도메인을 쓰는 두 앱이라면, 정식 SSO 없이도 사실상 같은 세션을 공유할 수 있지 않을까?" 이 질문에서 출발해 만든 것이 지금의 간소화된 SSO입니다.
해결의 핵심 3가지
핵심 아이디어는 세 가지의 조합입니다.
.voidx.ai에 쿠키를 박아 두 앱이 동일한 토큰을 보게 한다1. 루트 도메인 공유 쿠키
백엔드가 로그인 응답에서 access_token, refresh_token 쿠키를 발급할 때 Domain=.voidx.ai로 설정합니다.
Set-Cookie: access_token=...; Domain=.voidx.ai; Path=/; HttpOnly; Secure; SameSite=None
이 한 줄 덕분에 voidx.ai에서 받은 쿠키가 admin.voidx.ai에서도 그대로 읽힙니다. 브라우저가 도메인 매칭 규칙으로 알아서 처리해 줍니다.
SameSite=None이 필수인 이유는 두 앱이 서로의 API를 호출할 때 크로스 사이트로 분류되기 때문입니다. 동시에 Secure가 강제되므로 HTTPS 환경에서만 동작합니다.
2. BFF 부트스트랩 패턴
쿠키가 있다고 NextAuth 세션이 저절로 생기지는 않습니다. NextAuth는 자신의 세션 토큰(next-auth.session-token)을 별도로 관리하니까요. 그래서 앱이 마운트되는 시점에 "공유 쿠키가 있는데 내 NextAuth 세션은 비어있다면 세션을 복구하자"는 부트스트랩 컴포넌트를 띄웁니다.
// SessionAutoBootstrap.tsx (의사코드)
useEffect(() => {
if (hasNextAuthSession) return;
if (!hasSharedCookie('access_token')) return;
fetch('/next-server/voidx-auth/refresh') // BFF
.then((res) => res.json())
.then((tokens) => signIn('credentials', tokens, { redirect: false }));
}, []);
/next-server/voidx-auth/refresh는 같은 도메인의 Next.js Route Handler입니다. 브라우저가 가진 쿠키를 백엔드로 그대로 전달하고, 새 access/refresh token을 받아 NextAuth 세션으로 승격시키는 BFF(Backend for Frontend) 역할을 합니다.
같은 엔드포인트를 어드민에서도 동일한 형태로 두면, 두 앱 모두 동일한 부트스트랩 흐름을 공유합니다.
3. BroadcastChannel로 탭 간 로그아웃 동기화
문제 하나가 더 남습니다. 사용자가 메인 탭에서 로그아웃하면 어드민 탭은 어떻게 알까요? 쿠키가 지워져도 어드민 탭의 NextAuth 세션은 메모리에 남아 있어 한동안 "로그인된 상태"처럼 보입니다.
해결책은 의외로 가벼웠습니다. BroadcastChannel로 같은 origin의 다른 탭에 신호를 보내는 것.
// authBroadcast.ts (의사코드)
const channel = new BroadcastChannel('voidx-auth');
export function broadcastLogout() {
channel.postMessage({ type: 'logout' });
}
// AuthBroadcastListener.tsx
channel.onmessage = (event) => {
if (event.data.type === 'logout') {
signOut({ redirect: false }); // 쿠키는 이미 지워짐, 세션만 정리
}
};
크로스 도메인은 안 되지만, 메인 앱 내부 탭들끼리는 즉시 반응합니다. 어드민 쪽은 다음 라우트 이동 시 BFF가 쿠키 부재를 감지해 세션을 만료시키는 것으로 충분했습니다.
전체 흐름
[사용자] │ ① voidx.ai에서 로그인 ▼ [백엔드 API] ──── Set-Cookie: Domain=.voidx.ai ────▶ 브라우저 쿠키 저장소 │ │ ② admin.voidx.ai로 이동 (새 탭/리다이렉트) ▼ [admin 앱 마운트] │ │ SessionAutoBootstrap이 공유 쿠키 감지 ▼ [BFF: /next-server/voidx-auth/refresh] │ │ 쿠키 → 백엔드 → 새 토큰 ▼ [NextAuth 세션 자동 생성] ──▶ 사용자: 다시 로그인 없이 진입
트레이드오프와 한계
이 설계는 만능이 아닙니다. 받아들인 제약은 분명합니다.
voidx.ai와 admin.voidx.ai처럼 공통 상위 도메인이 있어야만 성립. 외부 파트너 도메인은 불가대신 얻은 것은 명확합니다. IdP 없이, 추가 인프라 없이, NextAuth 설정만 가지고 두 도메인의 세션을 사실상 하나로 묶었습니다. 사용자는 한 번만 로그인하고, 개발자는 두 앱에서 동일한 패턴으로 인증을 다룹니다.
다음 단계
지금의 구조는 "내부 서비스 두세 개를 잇는 정도"에서 가장 잘 작동합니다. 저수준의 SSO만으로 사용자 경험을 일관되게 제공할 수 있음을 검증하였습니다. 앞으로 서드파티 통합이 생기거나 모바일 앱이 합류하면 자연스럽게 정식 OAuth로 진화시킬 계획입니다.