ECS cloudwatch nextjs 서버 로그
미들웨어와 API 라우트(혹은 또 다른 서버 코드)가 동시에 supabase.auth.getUser()를 호출하면 같은 리프레시 토큰을 두 번 쓰려다 충돌합니다. 초기 로드에 RSC(Server Components) 가 쿠키를 쓰기(write) 해야 하는 상황이 아니라면, 미들웨어에서의 인증 갱신을 아예 제거하고 단일 엔드포인트(/api/auth/user)에서만 getUser() → 갱신하게 만들면 깔끔하게 해결됩니다.
Supabase의 공식 가이드는 Next.js App Router에서 Server Components는 쿠키를 쓸 수 없다는 제약 때문에, 만료된 토큰을 미들웨어가 먼저 갱신하고 이를 요청/응답 쿠키에 반영하라고 안내합니다. 이렇게 하면 서버 컴포넌트는 새 토큰을 읽기만 하면 되죠. 또한 서버 측 보호에는 getUser()를 쓰라고 명시합니다. (서버 코드에서 getSession()만 믿지 말 것)
대표적인 사례가 GitHub 이슈 supabase/ssr #68입니다. 미들웨어에서 갱신을 걸어 두면 하루쯤 지나 만료 시점에 페이지를 열 때 미들웨어가 갱신을 시도하고 동시에 다른 서버 코드(레이아웃, API 라우트 등)도 getUser()를 호출하면서 같은 리프레시 토큰을 중복 사용하게 됩니다.
그 결과 “Invalid Refresh Token: Refresh Token Not Found” 같은 에러가 발생합니다.
비슷한 류의 보고에서 두 요청이 동시에 리프레시를 시도하면 한쪽이 성공하고 다른 한쪽은 “Already Used” 로 실패한다는 분석도 있습니다.
즉, 리프레시 토큰은 1회성이라 동시 갱신에 취약합니다.
// 🚫 여러 곳에서 동시에 토큰 갱신 시도
// middleware.ts
export async function middleware(request: NextRequest) {
const supabase = createServerClient(...);
const { data: { user } } = await supabase.auth.getUser(); // 첫 번째 갱신 시도
// ...
}
// api/auth/user/route.ts
export async function GET() {
const supabase = createServerClient(...);
const { data: { user } } = await supabase.auth.getUser(); // 두 번째 갱신 시도
// ...
}
// 결과 - 한쪽이 "Invalid Refresh Token" 에러 발생
아키텍처 수준에서 보면 문제는 단순합니다.
supabase.auth.getUser() → 갱신 시도/api/auth/user 를 호출하고 그 안에서도 getUser() → 다시 갱신 시도// ✅ 단일 엔드포인트에서만 토큰 갱신
// middleware.ts - 간단한 라우트 보호만 수행
export async function middleware(request: NextRequest) {
const protectedRoutes = ["/******"];
if (protectedRoutes.includes(request.nextUrl.pathname)) {
const supabase = createServerClient(...);
const { data: { session } } = await supabase.auth.getSession(); // 갱신 없이 세션만 확인
if (!session) {
return NextResponse.redirect(new URL("/signin", request.url));
}
}
return NextResponse.next();
}
// /api/auth/session/route.ts - 여기서만 토큰 갱신 처리
export async function GET() {
const supabase = await supabaseServer();
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ session: null }, { status: 200 });
}
// 토큰 갱신이 필요한 경우 여기서만 수행
const { data: { user } } = await supabase.auth.getUser();
// ...
}
RSC에서 초기 서버 렌더에 사용자 정보를 꼭 써야 한다면 미들웨어를 유지할 수밖에 없습니다.
getUser()를 호출하는 API 라우트/레이아웃/다른 서버 코드를 없애거나 뒤로 미루기이 문제의 핵심은 “리프레시 토큰은 단 한 번만 안전하게 쓸 수 있다”**는 사실입니다. **동시에 두 군데에서 getUser()를 호출하면 한쪽은 성공하고, 다른 한쪽은 “Refresh Token Not Found / Already Used”로 실패하면서 사용자가 로그아웃되는 경쟁 조건이 생깁니다.
초기 로드에서 갱신 주체를 하나로 만들고(미들웨어 또는 특정 API 라우트) 나머지는 읽기 전용으로 설계하면 문제는 깔끔하게 사라집니다.