Skip to content
yona on air
Go back

Next.js + Supabase SSR 인증 트러블슈팅(1)

Updated:
Edit page

Next.js + Supabase SSR 인증 트러블슈팅

증상


근본 원인

createBrowserClient로 로그인하면 세션이 localStorage에만 저장된다. 서버(Server Component, middleware)는 localStorage를 읽을 수 없기 때문에 항상 “로그인 안 된 상태”로 판단 → 무한 리디렉션 루프 발생.

해결 방법: 로그인 후 서버 콜백(/api/auth/callback)을 거쳐 세션을 쿠키에 저장해야 서버가 인증 상태를 읽을 수 있다.


최종 파일 구조

src/
├── app/
│   ├── layout.tsx                        # RootLayout — ThemeProvider만
│   ├── page.tsx                          # 로그인 여부로 /tasks 또는 /login redirect
│   ├── (auth)/
│   │   └── login/
│   │       └── page.tsx                  # 로그인 폼
│   ├── api/
│   │   └── auth/
│   │       └── callback/
│   │           └── route.ts             # ← 핵심! 세션을 쿠키에 저장
│   └── (dashboard)/
│       ├── layout.tsx                    # Header/Footer + 인증 체크
│       └── tasks/
│           └── page.tsx
├── components/
│   ├── common/
│   │   └── ThemeRegistry.tsx
│   └── layout/
│       ├── Header.tsx
│       └── Footer.tsx
└── lib/
    └── supabase/
        ├── client.ts                     # 브라우저용
        └── server.ts                     # 서버용 (async)
middleware.ts                             # 프로젝트 루트 — 세션 갱신만

각 파일 역할

middleware.ts (프로젝트 루트)

세션 쿠키 갱신만 담당. redirect 로직 없음. middleware에서 redirect하면 /login도 잡혀서 루프 발생하므로 제거.

export async function middleware(request: NextRequest) {
  // supabase 클라이언트 생성 + getUser() 호출로 세션 갱신만
  await supabase.auth.getUser();
  return response;
}

src/app/api/auth/callback/route.ts ← 핵심

로그인 성공 후 이 라우트를 거쳐야 세션이 쿠키에 저장된다.

export async function GET(request: NextRequest) {
  const code = searchParams.get("code");
  if (code) {
    const supabase = await createClient();
    await supabase.auth.exchangeCodeForSession(code);
  }
  return NextResponse.redirect(`${origin}/tasks`);
}

src/app/(auth)/login/page.tsx

로그인 성공 후 router.push 대신 window.location.href 사용. → full reload로 서버가 새 쿠키를 읽도록 강제.

const { error } = await supabase.auth.signInWithPassword({ email, password });
if (!error) {
  window.location.href = "/tasks"; // router.push 사용 금지
}

src/app/(dashboard)/layout.tsx

인증 체크는 여기서만 담당.

const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");

src/app/layout.tsx

ThemeProvider만. 인증 로직 없음. 이 파일에 인증 체크가 있으면 /login도 redirect 대상이 되어 루프 발생.

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        <ThemeRegistry>{children}</ThemeRegistry>
      </body>
    </html>
  );
}

로그아웃

클라이언트 signOut()은 localStorage만 지우고 서버 쿠키는 못 지움. API route에서 서버 측 signOut을 호출해야 함.

// src/app/api/logout/route.ts
export async function POST() {
  const supabase = await createClient();
  await supabase.auth.signOut();
  return NextResponse.redirect("/login");
}

// Header.tsx
const handleLogout = async () => {
  await fetch("/api/logout", { method: "POST" });
  window.location.href = "/login";
};

Supabase 대시보드 설정

Authentication → URL Configuration

Site URL: http://localhost:3000
Redirect URLs: http://localhost:3000/api/auth/callback

문제 요약

문제결과해결
app/layout.tsx에 인증 체크/login도 redirect 대상이 되어 루프(dashboard)/layout.tsx로 이동
middleware에서 redirect/login 접근 시 루프middleware는 세션 갱신만
router.push("/tasks") 로 로그인 후 이동서버가 세션 쿠키 못 읽음window.location.href 사용
클라이언트 signOut() 후 이동서버 쿠키 미삭제 → 루프API route에서 서버 signOut
callback route 없이 로그인세션이 localStorage에만 저장/api/auth/callback route 필수

Edit page
Share this post on:

Previous Post
AWS EC2로 실시간 채팅 앱 배포하기 (2)
Next Post
AWS EC2 + RDS로 실시간 채팅 앱 배포하기 (1) - 설계