[Next.js App Router] NextAuth.js를 이용한 로그인 기능 구현, 메타데이터 추가

이희령·2023년 11월 12일
0

[Chapter 15] Adding Authentication

인증(Authentication)과 인가(Authorization)의 차이점

  • 인증(Authentication): 인증은 사용자가 누구인지를 확인하는 것이다. 사용자는 아이디와 비밀번호를 통해 자신의 신원을 증명한다.
  • 인가(Authorization): 사용자의 신원이 확인되면, 인가는 해당 사용자가 애플리케이션의 어떤 부분에 접근 가능한지를 결정한다.

NextAuth.js란?

  • NextAuth.js는 세션 관리, 로그인 및 로그아웃 등 인증 과정을 단순화하여 Next.js 애플리케이션에서 인증을 위한 통합 솔루션을 제공한다.

NextAuth.js 설치

npm install next-auth@beta bcrypt
  • Next.js 14에서 사용할 수 있는 NextAuth.js 베타 버전과 비밀번호 해쉬화를 위한 bcrypt 라이브러리를 설치한다.
AUTH_SECRET=your-secret-key
AUTH_URL=http://localhost:3000/api/auth
  • npm 설치 후 .env 파일에 openssl rand -base64 32로 생성한 시크릿 키와 URL을 추가한다.

Next.js Middleware로 라우팅 보호하기

// /auth.config.ts
import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  providers: [],
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
} satisfies NextAuthConfig;
  • auth.config.ts 파일을 루드 디렉토리에 생성하여 authConfig를 설정한다.
  • pages 옵션으로 로그인 할 때 NextAuth.js에서 기본적으로 제공하는 로그인 페이지로 이동하는 것이 아닌 커스텀 로그인 페이지로 이동하도록 설정한다.
  • authorized 콜백 함수를 이용해서 로그인 요청에 Next.js Middleware를 통해 페이지에 접근할 수 있는 권한을 부여받았는지 확인한다.
// /middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export default NextAuth(authConfig).auth;
 
export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ['/((?!api|_next/static|_next/image|.png).*)'],
};
  • 루트 디렉토리에 middleware.ts 파일을 생성한다.
  • authConfig 객체로 NextAuth.js룰 초기화하고 이를 export 한다. 또한 matcher 옵션을 추가해서 미들웨어가 특정 경로에서만 실행되도록 설정할 수 있다.
  • 미들웨어를 사용하면 미들웨어가 인증을 확인할 때까지 보호된 경로의 렌더링이 시작되지 않아서 애플리케이션의 보안과 성능이 모두 향상된다는 장점이 있다.
  • 미들웨어 파일 생성 후 이미지가 엑박으로 뜰 경우 다음 게시물을 참고해주세요! 링크

로그인 기능 추가하기

// /auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { sql } from '@vercel/postgres';
import { z } from 'zod';
import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';

async function getUser(email: string): Promise<User | undefined> {
  try {
    const user = await sql<User>`SELECT * from USERS where email=${email}`;
    return user.rows[0];
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw new Error('Failed to fetch user.');
  }
}

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);

        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null;

          const passwordsMatch = await bcrypt.compare(password, user.password);
          if (passwordsMatch) return user;
        }

        console.log('Invalid credentials');
        return null;
      },
    }),
  ],
});
  • authorize 함수를 사용해서 인증 로직을 관리할 수 있다.
  • zod를 사용해서 사용자가 DB에 존재하는지 확인하기 전에 이메일과 비밀번호의 유효성 검사를 수행한다.
  • 유효성 검사를 마치면 getUser 함수를 실행해 DB에서 해당하는 사용자의 데이터를 찾고, bcrypt.compare를 실행해서 사용자가 입력한 비밀번호와 DB에 해쉬화되어 저장된 비밀번호가 일치하는지 확인한다.
  • 비밀번호까지 일치한다면 사용자의 정보를 반환하고, 일치하지 않는다면 사용자가 로그인 할 수 없도록 null을 반환한다.

로그인 form 업데이트하기

'use client'

import { useFormState } from 'react-dom';
import { authenticate } from '@/app/lib/actions';
 
export default function LoginForm() {
  const [code, action] = useFormState(authenticate, undefined);
 
  return (
    <form action={action}>
    // ...
    {code === 'CredentialSignin' && (
       <p aria-live="polite" className="text-sm text-red-500">
       	  Invalid credentials
       </p>
     )}
  )
  • 리액트에서 제공하는 useFormState 훅을 사용해서 서버 액션을 호출하고 에러를 핸들링 할 수 있다.
import { useFormStatus } from 'react-dom';

function LoginButton() {
  const { pending } = useFormStatus();
 
  return (
    <Button className="mt-4 w-full" aria-disabled={pending}>
      Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
    </Button>
  );
}
  • 또한 useFormStatus 훅에서 반환하는 pending 값을 사용해서 로그인 중인 경우 로그인 버튼에 disabled 설정을 할 수 있다.

로그아웃 기능 구현하기

import { signOut } from '@/auth';
 
export default function SideNav() {
  return (
    <form
      action={async () => {
      'use server';
      await signOut();
      }}
	>
  );
}
  • auth.ts 파일에서 export 한 signOut 함수를 이용해서 로그아웃 기능을 구현한다.

[Chapter 16] Adding Metadata

메타데이터란?

  • 메타데이터는 웹 페이지에 추가적인 정보를 전달하며 보통 페이지의 <head> 태그 안에 위치한다.
  • 메타데이터는 웹 페이지의 SEO를 향상시키는 데 중요한 역할을 하며, 검색 엔진과 소셜 미디어 플랫폼에 대한 접근성과 이해도를 높인다.
  • 적절한 메타데이터는 검색 엔진이 웹 페이지를 효율적으로 인덱싱해서 검색 결과 순위를 향상시키는 데 도움을 준다.
  • 또한 오픈그래프와 같은 메타데이터는 소셜 미디어에서 링크를 공유할 때 사용자에게 더욱 매력적이고 유익한 컨텐츠를 제공한다.

메타데이터의 종류

<title>Page Title</title>
<meta name="description" content="A brief description of the page content." />
<meta name="keywords" content="keyword1, keyword2, keyword3" />

<meta property="og:title" content="Title Here" />
<meta property="og:description" content="Description Here" />
<meta property="og:image" content="image_url_here" />

<link rel="icon" href="path/to/favicon.ico" />
  • Title: 브라우저 탭에 나타나는 웹 페이지의 제목을 나타낸다. 검색 엔진이 해당 웹 페이지를 파악하는 데 도움을 주기 때문에 SEO에 매우 중요하다.
  • Description: 웹 페이지의 내용에 대한 간략한 개요를 제공하며 검색 엔진 결과에 표시된다.
  • Keyword: 웹 페이지의 내용과 관련된 키워드를 포함하여 검색 엔진이 페이지를 인덱싱하는 데 도움을 준다.
  • Open Graph: 제목, 상세 내용, 미리보기 이미지 등을 제공함으로써 웹 페이지가 소셜 미디어에서 공유될 때 표현되는 방식을 향상시긴다.
  • Favicon: 브라우저의 주소 표시줄이나 탭에서 표시되는 파비콘(작은 아이콘)을 설정한다.

메타데이터 추가하기

파비콘과 오픈 그래프

  • /app 폴더의 루트 디렉토리에 favicon.icoopengraph-image.jpg 파일을 추가한다.
  • Next.js는 이 파일들을 자동으로 식별하여 파비콘과 오픈 그래프 이미지로 사용한다.

페이지 title과 description

// /app/layout.tsx
import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Acme Dashboard',
  description: 'The official Next.js Course Dashboard, built with App Router.',
};
  • layout.js 혹은 page.js 파일에 title, description 등의 정보를 담은 메타데이터 객체를 포함하면 Next.js는 자동으로 애플리케이션에 메타데이터를 추가한다.
// /app/layout.tsx
import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: {
    template: '%s | Acme Dashboard',
    default: 'Acme Dashboard',
  },
  description: 'The official Next.js Learn Dashboard built with App Router.',
};

// /app/dashboard/invoices/page.tsx
export const metadata: Metadata = {
  title: 'Invoices',
};
  • 만약에 페이지마다 커스텀 타이틀을 추가하고 싶다면 해당 page.tsx에 메타데이터 객체를 추가하면 된다.
  • 하지만 타이틀 중에서도 회사 이름과 같이 고정적인 부분이 있다면 title.template 필드에 %s를 설정해서 해당 부분의 타이틀만 변경되도록 할 수 있다.
  • 에를 들면 상단의 예시 코드에서 invoices 페이지의 타이틀은 Invoices | Acme Dashboard가 될 것이다.
profile
Small Steps make a Big Difference.🚶🏻‍♀️

0개의 댓글