본문으로 건너뛰기

안동민 개발노트

본문 시작

로그인과 로그아웃

Auth.js 서버 액션과 클라이언트 API로 로그인·로그아웃 UI를 구현하고 세션 경계를 구분합니다.

인증 설정을 마쳤다면 사용자가 로그인과 로그아웃을 시작할 수 있는 화면을 만듭니다.

Auth.js에서는 서버 액션과 클라이언트 API 두 방식이 있습니다.

기본 선택은 서버 액션입니다.

브라우저 상태에 따라 즉시 바뀌는 UI가 필요할 때만 클라이언트 API를 사용합니다.

로그인과 로그아웃은 Server Action으로 연결한다

폼 제출이 서버에서 Auth.js 함수를 호출하도록 만든다.

  1. Form 사용자

    로그인 또는 로그아웃을 요청한다.

  2. Server Action 서버 경계

    입력과 목적지를 확인한다.

  3. Auth.js signIn 또

    signOut을 실행한다.

  4. 이동 허용된 내부 경로

    전환한다.


서버 액션으로 로그인하기

src/auth.ts에서 내보낸 signIn을 서버 액션에서 호출합니다.

src/components/sign-in-button.tsx
import { signIn } from '@/auth';

export function SignInButton() {
  return (
    <form
      action={async () => {
        'use server';
        await signIn('github', { redirectTo: '/account' });
      }}
    >
      <button type="submit">GitHub로 로그인</button>
    </form>
  );
}

버튼을 누르면 서버 액션이 GitHub 인증 흐름을 시작합니다.

인증이 끝나면 redirectTo에 지정한 /account로 이동합니다.

외부 입력을 redirectTo에 그대로 사용하면 외부 사이트로 유도하는 취약점이 생길 수 있습니다.

이동 경로는 애플리케이션 내부의 허용된 값으로 제한합니다.

redirectTo는 내부 경로만 허용한다

로그인 뒤 이동할 주소가 외부 사이트로 빠지지 않게 제한한다.

  1. redirectTo 입력 문자열

    읽고 앱 기준 URL로 해석한다.

  2. 내부 경

    같은 origin이며 허용 목록에 있으면 사용한다.

  3. 외부·잘못된 값 기본 내부 경로

    대체한다.


서버 액션으로 로그아웃하기

로그아웃도 중앙 인증 모듈의 signOut을 호출합니다.

src/components/sign-out-button.tsx
import { signOut } from '@/auth';

export function SignOutButton() {
  return (
    <form
      action={async () => {
        'use server';
        await signOut({ redirectTo: '/' });
      }}
    >
      <button type="submit">로그아웃</button>
    </form>
  );
}

signOut()은 세션 쿠키를 무효화한 뒤 지정한 경로로 이동합니다.

브라우저 저장소의 값을 직접 지우는 방식으로 세션을 흉내 내지 않습니다.

signOut은 세션 종료와 이동을 함께 다룬다

버튼 표시만 바꾸지 말고 서버에서 실제 로그인 상태를 끝낸다.

  1. 로그아웃 폼 사용자 의도

    서버로 보낸다.

  2. signOut Auth.js

    현재 세션을 무효화한다.

  3. Redirect 공개 페이지

    이동한다.


세션에 따라 버튼 바꾸기

서버 컴포넌트에서 세션을 읽으면 로딩 상태 없이 첫 HTML부터 알맞은 버튼을 보낼 수 있습니다.

src/components/auth-menu.tsx
import { auth } from '@/auth';
import { SignInButton } from './sign-in-button';
import { SignOutButton } from './sign-out-button';

export async function AuthMenu() {
  const session = await auth();

  if (!session?.user) {
    return <SignInButton />;
  }

  return (
    <div>
      <span>{session.user.name ?? session.user.email}</span>
      <SignOutButton />
    </div>
  );
}

이 컴포넌트는 서버에서 실행되므로 세션이 브라우저에서 준비될 때까지 기다리지 않습니다.

사용자 이름이 없을 수 있으므로 이메일을 대체 값으로 사용합니다.

서버 AuthMenu는 세션에 따라 메뉴를 고른다

초기 HTML부터 로그인 상태에 맞는 동작만 렌더한다.

  1. auth() 서버

    세션을 한 번 읽는다.

  2. 세션 있음 사용자 정보

    로그아웃 폼을 렌더한다.

  3. 세션 없음 로그인 폼

    렌더한다.


클라이언트 API 사용하기

모달 안에서 로그인 버튼을 제어하거나 세션 갱신 상태를 바로 보여줘야 한다면 클라이언트 API가 필요할 수 있습니다.

src/components/client-auth-button.tsx
'use client';

import { signIn, signOut, useSession } from 'next-auth/react';

export function ClientAuthButton() {
  const { data: session, status } = useSession();

  if (status === 'loading') {
    return <button disabled>확인 중</button>;
  }

  if (session?.user) {
    return <button onClick={() => signOut({ redirectTo: '/' })}>로그아웃</button>;
  }

  return <button onClick={() => signIn('github', { redirectTo: '/account' })}>로그인</button>;
}

이 방식은 상위 트리에 SessionProvider가 있어야 합니다.

클라이언트의 세션 상태는 화면 표현을 위한 값입니다.

데이터 저장이나 관리자 기능 같은 보안 판단은 서버에서 다시 검사해야 합니다.

클라이언트 세션은 필요한 섬에만 둔다

상호작용 컴포넌트만 SessionProvider와 useSession 경계 안에 넣는다.

  1. SessionProvider 클라이언트 세션 문맥

    필요한 하위 트리에 제공한다.

  2. useSession loading

    authenticated, unauthenticated를 읽는다.

  3. Interactive UI 실시간 메뉴나 모달

    표시만 갱신한다.


로그인 실패 다루기

OAuth 공급자가 오류를 반환하거나 사용자가 동의를 취소할 수 있습니다.

커스텀 로그인 페이지를 사용한다면 설정에 경로를 지정합니다.

src/auth.ts
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [GitHub],
  pages: {
    signIn: '/login',
    error: '/login',
  },
});

오류 화면에는 공급자의 원문 오류나 비밀 정보를 그대로 노출하지 않습니다.

사용자에게는 다시 시도할 방법을 안내하고, 상세 원인은 서버 로그에서 확인합니다.

OAuth 실패는 경계별로 원인을 나눈다

같은 로그인 실패 화면도 원인은 서로 다를 수 있다.

  1. 설정 실패 AUTH_* 누락 또

    잘못된 값

  2. Provider 거부 callback 불일치 또

    권한 거부

  3. Callback 실패 code 교환이나 계정

    연결 실패

  4. Session 실패 cookie 또

    세션 저장 문제


구현 기준

세션을 읽는 기본 위치는 서버 컴포넌트입니다.

로그인과 로그아웃의 기본 구현은 서버 액션입니다.

브라우저 상호작용이 필요한 UI만 next-auth/react를 사용합니다.

화면에서 버튼을 숨겼다는 사실만으로 권한이 보호되지는 않습니다.

UI 분기와 보안 판정은 책임이 다르다

버튼을 숨기는 것은 사용성이고 데이터 접근 차단은 서버 보안이다.

  1. UI 경계 메뉴

    버튼의 표시를 세션에 맞춘다.

  2. 보안 경계 서버

    인증, 역할, 소유권을 다시 검사한다.

다음 절에서는 auth()를 서버 경계에 적용해 실제 라우트를 보호합니다.

세션 소비 위치는 필요한 동작으로 결정한다

처음부터 클라이언트로 보내지 말고 가장 가까운 서버 경계를 고른다.

  1. Server Component 초기 화면

    서버 데이터 분기

  2. Server Action 로그인, 로그아웃, 변경

    요청

  3. Client Component 즉시 반응하

    세션 UI

  4. Route Handler HTTP 응답의 401

    403 판정