본문으로 건너뛰기

안동민 개발노트

본문 시작

로그인과 로그아웃

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

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

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

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

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

HTML 다이어그램: /docs/next/ch10/ch10-2/1.html

서버 액션으로 로그인하기

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에 그대로 사용하면 외부 사이트로 유도하는 취약점이 생길 수 있습니다.

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

HTML 다이어그램: /docs/next/ch10/ch10-2/2.html

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

로그아웃도 중앙 인증 모듈의 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()은 세션 쿠키를 무효화한 뒤 지정한 경로로 이동합니다.

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

HTML 다이어그램: /docs/next/ch10/ch10-2/3.html

세션에 따라 버튼 바꾸기

서버 컴포넌트에서 세션을 읽으면 로딩 상태 없이 첫 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>
  );
}

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

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

HTML 다이어그램: /docs/next/ch10/ch10-2/4.html

클라이언트 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가 있어야 합니다.

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

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

HTML 다이어그램: /docs/next/ch10/ch10-2/5.html

로그인 실패 다루기

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

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

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

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

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

HTML 다이어그램: /docs/next/ch10/ch10-2/7.html

구현 기준

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

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

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

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

HTML 다이어그램: /docs/next/ch10/ch10-2/6.html

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

HTML 다이어그램: /docs/next/ch10/ch10-2/8.html