본문 바로가기

개발 공부/React

Next.js LoginForm

 

login-form.tsx

"use client";

import { lusitana } from "@/app/ui/fonts";
import {
  AtSymbolIcon,
  KeyIcon,
  ExclamationCircleIcon,
} from "@heroicons/react/24/outline";
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import { Button } from "./button";
import { useActionState } from "react";
import { authenticate } from "@/app/lib/actions";
import { useSearchParams } from "next/navigation";

export default function LoginForm() {
  const searchParams = useSearchParams();
  const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
  const [errorMessage, formAction, isPending] = useActionState(
    authenticate,
    undefined
  );
  return (
    <form action={formAction} className="space-y-3">
      <div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
        <h1 className={${lusitana.className} mb-3 text-2xl}>
          Please log in to continue.
        </h1>
        <div className="w-full">
          <div>
            <label
              className="mb-3 mt-5 block text-xs font-medium text-gray-900"
              htmlFor="email"
            >
              Email
            </label>
            <div className="relative">
              <input
                className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
                id="email"
                type="email"
                name="email"
                placeholder="Enter your email address"
                required
              />
              <AtSymbolIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
            </div>
          </div>
          <div className="mt-4">
            <label
              className="mb-3 mt-5 block text-xs font-medium text-gray-900"
              htmlFor="password"
            >
              Password
            </label>
            <div className="relative">
              <input
                className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
                id="password"
                type="password"
                name="password"
                placeholder="Enter password"
                required
                minLength={6}
              />
              <KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
            </div>
          </div>
        </div>
        <input type="hidden" name="redirectTo" value={callbackUrl} />
        <Button className="mt-4 w-full" aria-disabled={isPending}>
          Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
        </Button>
        <div className="flex h-8 items-end space-x-1">
          {errorMessage && (
            <>
              <ExclamationCircleIcon className="h-5 w-5 text-red-500" />
              <p className="text-sm text-red-500">{errorMessage}</p>
            </>
          )}
        </div>
      </div>
    </form>
  );
}
  1. "use client";
    • 해당 컴포넌트가 클라이언트에서 실행됨을 명시합니다. (Next.js의 서버 컴포넌트와 구분됨)
  2. import 문
    • lusitana: 특정 폰트를 적용하기 위해 불러옴.
    • AtSymbolIcon, KeyIcon, ExclamationCircleIcon, ArrowRightIcon: Heroicons 라이브러리에서 가져온 아이콘.
    • Button: 버튼 컴포넌트.
    • useActionState: Next.js의 useActionState 훅을 사용하여 상태를 관리.
    • authenticate: 로그인 인증을 처리하는 액션 함수.
    • useSearchParams: URL 쿼리 문자열에서 callbackUrl을 가져오기 위해 사용됨.
  3. callbackUrl 설정
    • useSearchParams를 사용하여 URL에서 callbackUrl 값을 가져오며, 기본값은 /dashboard로 설정.
  4. useActionState 훅 사용
    • authenticate 액션을 실행하면서 반환된 상태를 관리.
    • errorMessage: 로그인 실패 시 오류 메시지를 표시하는 상태.
    • formAction: 폼이 제출될 때 호출되는 함수.
    • isPending: 로그인 요청이 진행 중인지 나타내는 상태.
  5. <form> 태그
    • action={formAction}: 로그인 액션을 실행하는 폼.
    • className="space-y-3": Tailwind CSS를 사용하여 요소 간 간격 조정.
  6. 이메일 입력 필드
    • id="email", type="email", name="email": 이메일 입력을 위한 필드.
    • 플레이스홀더 및 필수 입력 필드(required).
    • 입력창 왼쪽에 AtSymbolIcon(@ 기호 아이콘) 추가.
  7. 비밀번호 입력 필드
    • id="password", type="password", name="password": 비밀번호 입력 필드.
    • minLength={6}: 최소 6자 이상 입력하도록 설정.
    • 입력창 왼쪽에 KeyIcon(열쇠 아이콘) 추가.
  8. 숨겨진 입력 필드 (<input type="hidden">)
    • 로그인 성공 후 사용자를 callbackUrl로 리디렉션하기 위해 hidden 필드 사용.
  9. 로그인 버튼 (<Button>)
    • aria-disabled={isPending}: 로그인 요청 중이면 버튼 비활성화.
    • 버튼 클릭 시 ArrowRightIcon(오른쪽 화살표 아이콘) 표시.
  10. 오류 메시지 출력
    • 로그인 실패 시 errorMessage가 존재하면 ExclamationCircleIcon(경고 아이콘)과 함께 오류 메시지 표시.

 


 

useSearchParams를 사용한 callbackUrl 설정

const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";

useSearchParams

  • useSearchParams는 Next.js의 next/navigation 모듈에서 제공하는 클라이언트 전용 훅입니다.
  • 현재 URL의 쿼리 문자열(query parameters) 을 읽을 수 있도록 해줍니다.
  • 예를 들어, https://example.com/login?callbackUrl=/profile

 

  • 여기서 callbackUrl=/profile이라는 쿼리 문자열이 포함되어 있습니다.
  • useSearchParams()를 사용하면 callbackUrl의 값을 가져올 수 있습니다.

 

 


useActionState를 사용한 로그인 상태 관리

const [errorMessage, formAction, isPending] = useActionState(
  authenticate,
  undefined
);

useActionState

  • useActionState는 Next.js의 최신 React 기능 중 하나로, 서버 액션(Server Actions)의 상태를 클라이언트에서 관리할 수 있도록 해줍니다.
  • 서버 액션이 실행되는 동안의 상태(로딩 중인지, 오류가 발생했는지 등) 를 관리하는 데 사용됩니다.

useActionState(authenticate, undefined)의 의미

  • authenticate: 실행할 서버 액션 함수.
  • undefined: 초기 상태 값(여기서는 상태 초기화).
  • 반환 값(const [errorMessage, formAction, isPending])은 다음과 같습니다:
    1. errorMessage: 로그인 실패 시 오류 메시지 (string 또는 null).
    2. formAction: 이 함수를 폼의 action 속성에 넣으면, 폼 제출 시 authenticate 액션이 실행됨.
    3. isPending: 로그인 요청이 진행 중이면 true, 완료되면 false.

 

  • 사용자가 로그인 버튼 클릭 → 폼 제출
    • action={formAction}이므로, 폼이 제출되면 formAction이 실행됨 → 즉, authenticate 함수가 호출됨.
  • <form action={formAction} className="space-y-3">
  • 서버 액션 실행 → isPending = true
    • useActionState는 내부적으로 서버 액션(authenticate)이 실행되면 isPending을 true로 변경함.
    • 이 상태에서 버튼이 **비활성화(aria-disabled={isPending})**될 수도 있음.
  • 서버 액션 완료 → isPending = false
    • authenticate가 실행을 마치고 에러 없이 끝나면 isPending이 false로 변경됨.
    • 만약 에러가 발생하면 errorMessage가 업데이트됨.

 

 

useActionState가 동작하는 과정

  1. 사용자가 로그인 폼을 제출하면, formAction이 실행되면서 authenticate 서버 액션이 호출됨.
  2. 로그인 요청이 처리되는 동안 isPending이 true로 설정됨 → 버튼 비활성화(aria-disabled={isPending}).
  3. 로그인 성공 시, 사용자는 callbackUrl로 리디렉션됨.
  4. 로그인 실패 시, errorMessage에 오류 메시지가 저장되어 UI에 표시됨.

 

 


 

실제 동작 흐름

정상적인 로그인 (성공 시 callbackUrl로 이동)

  1. 로그인 버튼 클릭 → <form> 제출
  2. formAction(authenticate, formData) 실행 → isPending = true
  3. authenticate에서 signIn("credentials", formData) 호출
  4. 인증 성공 → 리디렉션 (redirectTo = "/dashboard")
  5. isPending = false, 로그인 후 페이지 이동

잘못된 로그인 (비밀번호 틀림)

  1. 로그인 버튼 클릭 → <form> 제출
  2. formAction(authenticate, formData) 실행 → isPending = true
  3. authenticate에서 signIn("credentials", formData) 호출
  4. authorize(credentials)에서 검증 실패 → return "Invalid credentials."
  5. isPending = false, errorMessage = "Invalid credentials." 표시됨

'개발 공부 > React' 카테고리의 다른 글

Next.js loading.tsx  (0) 2025.03.23
React Focus Lock  (0) 2025.03.22
Next.js Metadata  (0) 2025.03.10
Next.js Middleware  (0) 2025.03.09
NextAuth.js의 보안 시크릿 키  (0) 2025.03.07