본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
10장 : 인증 및 권한 관리

로그인과 로그아웃

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

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

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

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

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


서버 액션으로 로그인하기

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

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


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

로그아웃도 중앙 인증 모듈의 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부터 알맞은 버튼을 보낼 수 있습니다.

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>
  );
}

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

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


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

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

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


로그인 실패 다루기

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

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

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

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

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


구현 기준

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

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

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

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

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