로그인과 로그아웃
Auth.js 서버 액션과 클라이언트 API로 로그인·로그아웃 UI를 구현하고 세션 경계를 구분합니다.
인증 설정을 마쳤다면 사용자가 로그인과 로그아웃을 시작할 수 있는 화면을 만듭니다.
Auth.js에서는 서버 액션과 클라이언트 API 두 방식이 있습니다. 이 절은 앞 절의 Auth.js v5와 Next.js App Router 설정을 이어 사용합니다. 서버 액션은 @/auth에서, 클라이언트 이벤트는 next-auth/react에서 가져온 함수를 호출합니다. 두 모듈의 같은 이름을 섞어 쓰지 않습니다.
기본 선택은 서버 액션입니다.
브라우저 상태에 따라 즉시 바뀌는 UI가 필요할 때만 클라이언트 API를 사용합니다.
서버 액션으로 로그인하기
src/auth.ts에서 내보낸 signIn을 서버 액션에서 호출합니다.
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로 이동합니다. 버튼을 누른 시점에는 GitHub 인증을 시작했을 뿐이며, 동의 취소나 공급자 오류가 생기면 로그인 성공을 전제로 다음 작업을 실행해서는 안 됩니다.
기본 redirect 콜백은 상대 경로와 같은 출처의 URL로 이동을 제한합니다. 이 예제는 고정된 내부 경로를 사용합니다. 목적지를 입력으로 받거나 콜백을 바꿀 때도 허용할 내부 경로를 명시적으로 제한합니다.
서버 액션으로 로그아웃하기
로그아웃도 중앙 인증 모듈의 signOut을 호출합니다.
import { signOut } from '@/auth';
export function SignOutButton() {
return (
<form
action={async () => {
'use server';
await signOut({ redirectTo: '/' });
}}
>
<button type="submit">로그아웃</button>
</form>
);
}signOut()은 현재 브라우저의 세션 쿠키를 지우고 지정한 경로로 이동합니다. 앞 절의 JWT 전략에서 이 동작이 이미 복사된 토큰까지 서버에서 일괄 폐기하는 것은 아닙니다.
앱 세션과 공급자 세션은 별개입니다. 이 앱에서 로그아웃해도 GitHub 웹사이트의 로그인 상태까지 종료하지는 않습니다. 따라서 다시 GitHub 로그인을 시작했을 때 비밀번호 입력 화면 없이 돌아올 수 있습니다. 이것만으로 앱의 로그아웃이 실패했다고 판단하지 않습니다. 앱의 보호된 요청이 새 세션 없이 거부되는지 확인합니다. Auth.js 로그인·로그아웃 안내는 공급자 세션과 앱 세션의 범위를 구분합니다.
브라우저 저장소의 값을 직접 지우는 방식으로 세션을 흉내 내지 않습니다.
세션에 따라 버튼 바꾸기
서버 컴포넌트에서 세션을 읽으면 브라우저가 별도로 세션을 조회하기 전에 알맞은 버튼을 서버 렌더 결과에 포함할 수 있습니다. 서버의 세션 조회 자체에는 대기 시간이 있을 수 있습니다.
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>
);
}사용자 이름이 null 또는 undefined이면 이메일을 사용합니다. 두 필드 모두 없거나 빈 문자열인 경우까지 표시 이름을 보장하는 코드는 아닙니다.
예시
이름이 빈 문자열이고 이메일이 [email protected]이어도 name ?? email의 결과는 빈 문자열입니다. 빈 문자열은 null이나 undefined가 아니기 때문입니다. 빈 이름도 대체하려는 화면 정책이라면 별도의 빈 값 검사가 필요합니다. 표시 문구의 보완으로 실제 서버 권한이 바뀌지는 않습니다.
확인 문제
ClientAuthButton에 이전 session.user 값이 남아 있지만 status는 loading이라고 가정합니다. 어떤 버튼을 먼저 반환하나요? 로딩이 끝난 뒤 사용자 값이 없다면 어떤 API를 어느 모듈에서 호출하나요? 원 조건문의 순서만 따라 판단해 보세요.
로딩 분기와 API 위치 해설
첫 번째 조건에서 비활성화된 확인 중 버튼을 반환하므로 이전 사용자 값에 따른 로그아웃 버튼까지 내려가지 않습니다. 로딩 종료 뒤 사용자가 없으면 클릭 시 next-auth/react의 signIn을 호출합니다. 서버 폼에서 사용하는 @/auth의 signIn과 기능 이름은 같아도 import와 실행 위치가 다릅니다.
클라이언트 API 사용하기
모달 안에서 로그인 버튼을 제어하거나 세션 갱신 상태를 바로 보여 줘야 한다면 클라이언트 API가 필요할 수 있습니다.
useSession()을 사용하는 컴포넌트는 클라이언트 SessionProvider 아래에 있어야 합니다. 아직 Provider가 없다면 다음 래퍼를 만들고, 서버 layout.tsx에서 로그인 상태를 표시할 영역을 이 컴포넌트로 감쌉니다. 서버 레이아웃 전체를 'use client'로 바꿀 필요는 없습니다.
'use client';
import { SessionProvider } from 'next-auth/react';
import type { ReactNode } from 'react';
export function AuthSessionProvider({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}서버에서 auth()만 호출하는 컴포넌트에는 이 Provider가 필요하지 않습니다. Provider는 브라우저의 표시 상태를 공유하며, 이후의 API 요청을 자동으로 허가하는 장치는 아닙니다. Auth.js React API의 클라이언트 사용 범위를 따릅니다.
'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 공급자가 오류를 반환하거나 사용자가 동의를 취소할 수 있습니다.
/login 페이지를 직접 만든 경우 설정에 경로를 지정합니다. 다음은 설정 추가 예시이며 기존 NextAuth, GitHub import와 필요한 세션·콜백 설정은 유지합니다. 경로를 설정하는 것만으로 페이지 파일이 생성되지는 않습니다.
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
pages: {
signIn: '/login',
error: '/login',
},
});오류 화면에는 공급자의 원문 오류나 비밀 정보를 그대로 노출하지 않습니다.
사용자에게는 다시 시도할 방법을 안내하고, 상세 원인은 서버 로그에서 확인합니다.
리다이렉트와 오류를 구분합니다. 서버의 signIn()·signOut()은 기본 설정에서 페이지 이동으로 흐름을 끝냅니다. 성공 후 항상 다음 줄로 돌아오는 일반 함수처럼 사용하지 않습니다. 나중에 서버 액션에 try/catch를 넣는다면 인증 실패인 AuthError만 처리하고 그 밖의 예외는 다시 던집니다. Next.js의 리다이렉트도 예외로 제어 흐름을 전달하므로 모든 예외를 “로그인 실패”로 삼키면 정상 이동이 막힐 수 있습니다. Auth.js 서버 API의 오류 처리 예시와 Next.js redirect 규칙을 함께 확인합니다.
로그인 API의 실행 위치와 세션 UI를 구분합니다
AuthButton이 세션 loading 동안 버튼을 막고, 세션 해석 뒤 로그인 또는 로그아웃 UI를 선택합니다. 같은 버튼의 초기 상태와 해석 완료 상태를 따라가면 과거 세션 값으로 잘못 표시하는 이유를 이해할 수 있습니다.
NEXT · 10.2 · 로그인과 로그아웃
로그인 API의 실행 위치와 세션 UI를 구분합니다
같은 로그인·로그아웃 기능도 import와 실행 위치가 다릅니다. 두 방식을 동시에 거치는 순서가 아니라, 교재의 서버 버튼과 ClientAuthButton이라는 두 구현을 비교합니다.
서버 액션 · @/auth
- form 제출 → 서버 액션
- SignInButton은 use server 액션에서 signIn("github")를 호출합니다. 인증 완료 후 /account로 이동합니다.
- 로그아웃 → 현재 앱 세션 종료
- SignOutButton은 signOut을 호출하고 /로 이동합니다. JWT 전략에서 복사된 모든 토큰을 일괄 폐기하는 구현은 아닙니다.
- AuthMenu → 서버의 세션 분기
- auth()의 session.user가 없으면 로그인 버튼, 있으면 사용자 표시와 로그아웃 버튼을 렌더링합니다.
클라이언트 API · next-auth/react
- 전제 · SessionProvider
- 상위 트리에서 세션을 제공하고 useSession()으로 data와 status를 읽습니다.
- status === "loading"
- 확인 중 버튼을 비활성화합니다. 이 상태를 로그아웃 상태로 간주하지 않습니다.
- 로딩 종료 · session.user 유무
- 있으면 signOut, 없으면 signIn을 onClick에서 호출합니다. 이동 목적지는 서버 버튼과 같습니다.
세션 상태와 버튼 숨김은 UI 표현입니다. 데이터 변경 권한은 서버에서 다시 검사합니다. OAuth 오류·동의 취소는 성공 이동과 다른 경로이며, 커스텀 /login 페이지는 별도로 구현해야 합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 원 ClientAuthButton의 조건문 순서다. status가 loading이면 이전 session.user 값과 무관하게 disabled 확인 중 버튼에서 끝난다. 로딩이 끝난 경우에만 session.user를 판정하여 로그아웃 또는 로그인 버튼을 반환한다. 화살표는 버튼을 선택하는 제어 흐름이고 signIn/signOut은 각 버튼을 클릭할 때 브라우저의 next-auth/react API로 호출된다. 버튼 출력은 데이터 변경 권한 검사가 아니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 세션 확인 중 status가 loading이면 예전 session.user와 무관하게 확인 중 버튼을 반환합니다. | status: loading · 버튼: disabled 확인 중 · session.user: 이 분기에서 무시 | 세션 해석 전 클릭과 이전 사용자 표시를 막습니다. |
| 세션 해석 완료 loading이 끝나야 session.user를 판단합니다. | status: loading 아님 · 판정: session.user 유무 | 사용자 유무가 UI 선택 입력이 됩니다. |
| 로그인 또는 로그아웃 UI user 유무에 맞는 버튼이 선택되고 클릭 때 signIn 또는 signOut을 호출합니다. | user 있음: 로그아웃 · user 없음: 로그인 · 권한 검사: 버튼 출력과 별도 | 세션 결과가 실제 클릭 가능한 UI로 반영됩니다. |
로그인 API의 실행 위치와 세션 UI를 구분합니다
AuthButton이 세션 loading 동안 버튼을 막고, 세션 해석 뒤 로그인 또는 로그아웃 UI를 선택합니다. 같은 버튼의 초기 상태와 해석 완료 상태를 따라가면 과거 세션 값으로 잘못 표시하는 이유를 이해할 수 있습니다.
NEXT · 10.2 · 로그인과 로그아웃
로그인 API의 실행 위치와 세션 UI를 구분합니다
같은 로그인·로그아웃 기능도 import와 실행 위치가 다릅니다. 두 방식을 동시에 거치는 순서가 아니라, 교재의 서버 버튼과 ClientAuthButton이라는 두 구현을 비교합니다.
서버 액션 · @/auth
- form 제출 → 서버 액션
- SignInButton은 use server 액션에서 signIn("github")를 호출합니다. 인증 완료 후 /account로 이동합니다.
- 로그아웃 → 현재 앱 세션 종료
- SignOutButton은 signOut을 호출하고 /로 이동합니다. JWT 전략에서 복사된 모든 토큰을 일괄 폐기하는 구현은 아닙니다.
- AuthMenu → 서버의 세션 분기
- auth()의 session.user가 없으면 로그인 버튼, 있으면 사용자 표시와 로그아웃 버튼을 렌더링합니다.
클라이언트 API · next-auth/react
- 전제 · SessionProvider
- 상위 트리에서 세션을 제공하고 useSession()으로 data와 status를 읽습니다.
- status === "loading"
- 확인 중 버튼을 비활성화합니다. 이 상태를 로그아웃 상태로 간주하지 않습니다.
- 로딩 종료 · session.user 유무
- 있으면 signOut, 없으면 signIn을 onClick에서 호출합니다. 이동 목적지는 서버 버튼과 같습니다.
세션 상태와 버튼 숨김은 UI 표현입니다. 데이터 변경 권한은 서버에서 다시 검사합니다. OAuth 오류·동의 취소는 성공 이동과 다른 경로이며, 커스텀 /login 페이지는 별도로 구현해야 합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 원 ClientAuthButton의 조건문 순서다. status가 loading이면 이전 session.user 값과 무관하게 disabled 확인 중 버튼에서 끝난다. 로딩이 끝난 경우에만 session.user를 판정하여 로그아웃 또는 로그인 버튼을 반환한다. 화살표는 버튼을 선택하는 제어 흐름이고 signIn/signOut은 각 버튼을 클릭할 때 브라우저의 next-auth/react API로 호출된다. 버튼 출력은 데이터 변경 권한 검사가 아니다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 세션 확인 중 status가 loading이면 예전 session.user와 무관하게 확인 중 버튼을 반환합니다. | status: loading · 버튼: disabled 확인 중 · session.user: 이 분기에서 무시 | 세션 해석 전 클릭과 이전 사용자 표시를 막습니다. |
| 세션 해석 완료 loading이 끝나야 session.user를 판단합니다. | status: loading 아님 · 판정: session.user 유무 | 사용자 유무가 UI 선택 입력이 됩니다. |
| 로그인 또는 로그아웃 UI user 유무에 맞는 버튼이 선택되고 클릭 때 signIn 또는 signOut을 호출합니다. | user 있음: 로그아웃 · user 없음: 로그인 · 권한 검사: 버튼 출력과 별도 | 세션 결과가 실제 클릭 가능한 UI로 반영됩니다. |
구현 기준
화면에서 버튼을 숨겼다는 사실만으로 권한이 보호되지는 않습니다.
다음 절에서는 auth()를 서버 경계에 적용해 실제 라우트를 보호합니다.
연습: 화면 상태와 세션의 범위 판단하기
앞 절의 JWT 전략과 이 절의 기본 리다이렉트 설정을 사용한다고 가정합니다. 서버의 거부 목록이나 전역 토큰 폐기 기능은 추가하지 않았습니다.
- 로그인한 앱에서
signOut()이 완료된 뒤 GitHub 웹사이트는 여전히 로그인 상태입니다. 두 관찰은 모순인가요? 앱의 보호된 API에 세션 없이 보내는 요청은 어느 경계에서 거부해야 하나요? - 과거에 발급된 JWT의 유효한 사본이 만료 전까지 남아 있습니다. 현재 브라우저의 쿠키 삭제만으로 이 사본까지 즉시 무효화되었다고 결론 낼 수 있나요?
ClientAuthButton이status === 'loading'인 동안에는 어떤 버튼을 보여 주나요? 서버 요청의 인증·권한 여부도 이 버튼의 상태로 결정하나요?- 서버 액션에서
signIn()호출을try/catch로 감싸 모든 예외를 무시한 뒤 “성공” 메시지를 출력했습니다. 정상 이동과 인증 실패를 왜 잘못 구분할 수 있나요?
세션 범위와 제어 흐름 풀이
- 앱과 GitHub의 세션은 별개이므로 모순이 아닙니다. 앱의 API는 요청마다 서버에서
auth()가 확인한 세션을 검사해야 합니다. 로그인 화면이 보였는지나 GitHub 웹사이트 상태로 허가하지 않습니다. - 결론 낼 수 없습니다. 쿠키 삭제는 현재 브라우저가 그 쿠키를 보내지 않게 하는 동작입니다. 사본의 조기 무효화가 필요하면 만료 정책 외에 서버의 거부 목록·세션 버전 검사 같은 별도 설계가 필요합니다. Auth.js 세션 전략도 JWT 조기 폐기의 제약을 설명합니다.
- 비활성화된 확인 중 버튼을 보여 주고, 사용자를 확인한 뒤 로그인 또는 로그아웃 버튼을 선택합니다. 이 분기는 UI 처리입니다. 서버 요청은 서버가 확인한 신원과 그 요청의 권한을 따로 검사합니다.
- 정상 리다이렉트가 전달한 제어 흐름까지 잡아서 없앨 수 있고, 실제 실패도 성공처럼 보이게 할 수 있습니다. 처리할 인증 오류만 구분하고 다른 예외는 다시 던지며, 무조건 이어지는 성공 메시지에 의존하지 않습니다.