[Next.js 13] Next-auth 커스텀 로그인 페이지 구현하기

S_Soo100·2023년 5월 29일
8

web

목록 보기
2/6
post-thumbnail

이전 글에서 app 디렉토리에 소셜로그인 구현하는 것 까지 기록해두었는데, 그 코드를 기반으로 진행해보자.
[Next.js 13]Next.js 13.2버젼에 Next-auth 구현하기(+ 네이버 소셜 로그인)

물론 next-auth가 자동으로 제공해주지만, 서비스 완성도 향상을 위해서는 필수적인 부분이 아닐까 싶다.

사실 여기부터는 next.js 13.2 이후 버전인거는 크게 관련 없다고 생각한다.
어디서 csr을 사용해야 하는지 생각해야 하는 정도인 것 같다.

0) authOption

  • 이전에 작성했던 코드 중, 라우터를 page에서 app으로 옮기면서 handler를 export하도록 변경 했었다.
    그러면서 기존 공식문서에서 쓰던 authOptions를 따로 선언하지 않고 NextAuth메서드 안에 그냥 넣어두었는데,
    이 부분을 외부로 빼 주자. 덤으로 export도 해줘서 외부에서 사용할 수 있게 해줘야 한다.

기존

src/app/api/auth/[...nextauth]/route.ts

```tsx
import NextAuth from "next-auth";
import NaverProvider from "next-auth/providers/naver";
...

const handler = NextAuth({
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_CLIENT_ID || "",
      clientSecret: process.env.NAVER_CLIENT_SECRET || "",
    }),
    ... //구글이랑 카카오도 써보는 중
  ],
});

export { handler as GET, handler as POST };
```

수정

src/app/api/auth/[...nextauth]/route.ts

import NextAuth, { NextAuthOptions } from "next-auth";
import NaverProvider from "next-auth/providers/naver";
...

export const authOptions: NextAuthOptions = {
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_CLIENT_ID || "",
      clientSecret: process.env.NAVER_CLIENT_SECRET || "",
    }),
    ... //구글이랑 카카오도 써보는 중
  ],
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

그 이유는 authOptions가 커스텀 로그인 페이지 및 여러 곳에서 필요하기 때문이다.

1) authOptions에 pages 파라미터 추가하기

  • 위에서 수정한 route.ts의 authOptions에 객체 타입을 받는 pages를 추가하자.
    그 안에 siginIn 이라는 파라미터를 통해 우리는 next-auth의 다양한 기능이 호출되었을 때 어디를 보여줄지 정할 수 있다.
  • 공식문서를 보면 아래와 같은 옵션들을 우리가 커스텀 페이지로 줄 수 있다고 나와있다. string으로 경로를 지정하면 해당 기능이 호출 될 때 그 페이지로 라우팅 해준다.
    ...
    	pages: {
    		signIn: '/auth/signin',
    		signOut: '/auth/signout',
    		error: '/auth/error', // Error code passed in query string as ?error=
    		verifyRequest: '/auth/verify-request', // (used for check email message)
    		newUser: '/auth/new-user' // New users will be directed here on first sign in
    	}
    ...
    로그인, 로그아웃, 에러(로그인 관련 에러), 인증 요청, 최초 가입자 페이지같은 유용한 정보들이 나와있고 error에는 에러 메세지를 ?error=뒤에 오는 query string으로 전달해준다고 한다.
  • 이제 우리 파일에도 pages를 추가하자. 라우팅은 예시랑 똑같이 '/auth/signin' 으로 하고,
    추가한 뒤에 app/auth/signin에 페이지를 만들자.

src/app/api/auth/[...nextauth]/route.ts

  import NextAuth, { NextAuthOptions } from "next-auth";
   import NaverProvider from "next-auth/providers/naver";
   ...

   export const authOptions: NextAuthOptions = {
     providers: [
       NaverProvider({
         clientId: process.env.NAVER_CLIENT_ID || "",
         clientSecret: process.env.NAVER_CLIENT_SECRET || "",
       }),
       ...
     ],
   	pages: {
       signIn: "/auth/signin", // 내가 원하는 커스텀 sign-in 페이지의 url 
     },
   };

   const handler = NextAuth(authOptions);

   export { handler as GET, handler as POST };

3) signin 페이지 구현하기

  • 우선 글씨만 나오는 페이지를 만들어서 제대로 작동하는지 확인해보자
export default function SigninPage() {
  return <div>Sign in Page</div>
}
  • 로그인 버튼을 누르면 상위 url이 /api/auth/signin으로 변경된걸 확인할 수 있다. 이제 이 페이지 안에 providers를 가지고 와서 하나씩 버튼으로 나열해주자.

4) 기능 연결하기: OAuth Sign in

  • 우리같이 소셜로그인 provider를 사용하는걸 OAuth Sign in이라고 부른다. 여기도 공식문서를 보면서 page를 구현해보자. pages 디렉토리 버전 공식문서(app 디렉토리 버전으로는 아직 공식문서가 업데이트 되지 않은거 같다.)
  • 우선 무식하게 예시 코드를 그냥 넣어보자.
    authOptions에 대한 import 문만 우리 버전으로 바꾸면 된다.
    (문서 맨 처음에 그거부터 진행한 이유가 바로 여기 있다.)

app/api/auth/signin/page.tsx

import type {
      GetServerSidePropsContext,
      InferGetServerSidePropsType
    } from 'next'
    import { getProviders, signIn } from 'next-auth/react'
    import { getServerSession } from 'next-auth/next'
    import { authOptions } from '../[...nextauth]/route'
    
    export default function SignInPage({
      providers
    }: InferGetServerSidePropsType<typeof getServerSideProps>) {
      return (
        <>
          {Object.values(providers).map(provider => (
            <div key={provider.name}>
              <button onClick={() => signIn(provider.id)}>
                Sign in with {provider.name}
              </button>
            </div>
          ))}
        </>
      )
    }
    
    export async function getServerSideProps(context: GetServerSidePropsContext) {
      const session = await getServerSession(context.req, context.res, authOptions)
    
      if (session) {
        return { redirect: { destination: '/' } }
      }
    
      const providers = await getProviders()
    
      return {
        props: { providers: providers ?? [] }
      }
    }
  • 그러면 바로 에러가 뜬다

    💡 ****Failed to compile****
    
    ./app/api/auth/signin/page.tsx
    ReactServerComponentsError:
    
    "getServerSideProps" is not supported in app/.
  • 무슨 내용인지 알겠으니 하나씩 해결해보자. 우선 getServerSideProps는 이제 쓰지 않는다. 그리고 예시 코드에서 서버 사이드 프롭스가 하던 일은 아래 3가지이다.
    //session 정보 가져오기
    const session = await getServerSession(context.req, context.res, authOptions)
    
    //session이 있으면 Home으로 보내기
    if (session) {
      return { redirect: { destination: '/' } }
    }
    
    //authOption에서 providers받아오기
    const providers = await getProviders()
    return {
      props: { providers: providers ?? [] }
    }
  • 이제 이거를 우리 컴포넌트에서 하면 되겠지?
    app/api/auth/signin/page.tsx
    ```tsx
    import { getProviders, signIn } from 'next-auth/react'
    import { getServerSession } from 'next-auth/next'
    import { authOptions } from '../[...nextauth]/route'
    
    export default async function SignInPage() {
      const session = await getServerSession(authOptions)
      if (session) {
        return { redirect: { destination: '/' } }
      }
    
      const providers = await getProviders()
    
      return (
        <>
          {Object.values(providers).map(provider => (
            <div key={provider.name}>
              <button onClick={() => signIn(provider.id)}>
                Sign in with {provider.name}
              </button>
            </div>
          ))}
        </>
      )
    }
    ```
    💡 **Unhandled Runtime Error**
    
    Error: Event handlers cannot be passed to Client Component props.
      <button onClick={function} children=...>
                      ^^^^^^^^^^
    If you need interactivity, consider converting part of this to a Client Component.
  • 하지만 여전히 에러가 남아있다. 부분이 CSR이 필요하다.
    이제 컴포넌트를 나눌 차례가 되었다.
    우선 데이터가 잘 들어오는지 확인해야 하니 <button/>태그를 없애고 그냥 provider 이름만 나열하게 해보자
    ```tsx
    <div className='m-4'>
        {Object.values(providers).map(provider => (
          <div key={provider.name}>Sign in with {provider.name}</div>
        ))}
    </div>
    ```

  • 데이터가 잘 들어오고 있다. 이제 SocialSigninButton컴포넌트를 만들어서 넣어주자. app/components/SocialSigninButton.tsx
    'use client';
    
    import { signIn } from 'next-auth/react';
    
    type IProps = {
      providers
    }
    
    export default function SocialSigninButton({ providers }: IProps) {
      return (
        <div>
          {Object.values(providers).map(provider => (
            <div key={provider.name}>
              <button onClick={() => signIn(provider.id)}>
                Sign in with {provider.name}
              </button>
            </div>
          ))}
        </div>
      );
    }
  • 하지만 이제 providers의 타입 때문에 에러가 난다. next-auth에서 주는 providers의 타입이 무엇인지 분석해보자.
    Record<LiteralUnion<BuiltInProviderType, string>, ClientSafeProvider> 우선, 앞에 있는 리터럴 유니온은 다음 코드 처럼 타입을 선택할 수 있게 해주는 타입스크립트의 기능이다.
    type BuiltInProviderType = 'A' | 'B' | 'C';
    type MyType = LiteralUnion<BuiltInProviderType, 'D'>;
    string과 묶여있고, signin함수 내에 provider는 string으로 집어넣으니 여기서는 string이라고 하자. 뒤의 ClientSafeProvider는 next-auth에서 만들어준 타입이니 그냥 그대로 집어넣자. 이걸 Record에 담아주자. app/components/SocialSigninButton.tsx
    'use client'
    
    import { ClientSafeProvider, signIn } from 'next-auth/react'
    
    type IProps = {
      providers: Record<string, ClientSafeProvider>
    }
    
    export default function SocialSigninButton({ providers }: IProps) {
      return (
        <div>
          {Object.values(providers).map(provider => (
            <div key={provider.name} className='m-4 bg-slate-200'>
              <button onClick={() => signIn(provider.id)}>
                Sign in with {provider.name}
              </button>
            </div>
          ))}
        </div>
      )
    }

  • 자 이제 드디어 잘 들어왔다.. 하나 눌러서 로그인 해보자.
  • …? 로그인은 되었는데 signin 페이지에 그대로 남아있다 왜지?
    홈(’/’)으로 리다이렉트 해줘야 할 것 같다.

 

  • 여기서부터는 찾아보다가 인도인 형님의 도움을 받았다.

  • 아 이거네.. callbackUrl을 useSearchParams에서 꺼내오고 있다.

    useSearchParams는 next.js의 라우터(네비게이션) 패키지에서 제공하는 훅으로, URL에서 쿼리 파라미터를 꺼내오고 조작하게 해준다.

    signin 페이지에서 url 창을 보면 아래와 같이 써있는걸 볼 수 있다.

    http://localhost:3000/api/auth/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F

    useSearchParams()가 여기서 callbackUrl을 받아서 signin메서드에 전달해주면 된다.

    app/components/SocialSigninButton.tsx

    'use client'
    
    import { useSearchParams } from 'next/navigation'
    import { ClientSafeProvider, signIn } from 'next-auth/react'
    
    type IProps = {
      providers: Record<string, ClientSafeProvider>
    }
    
    export default function SocialSigninButton({ providers }: IProps) {
      const searchParams = useSearchParams()
      const callbackUrl = searchParams.get('callbackUrl')
    
      console.log(searchParams)
      console.log(callbackUrl)
      console.log(callbackUrl)
      console.log(callbackUrl)
      console.log(callbackUrl)
    
      return (
        <div>
          {Object.values(providers).map(provider => (
            <div key={provider.name} className='m-4 bg-slate-200'>
              <button onClick={() => signIn(provider.id, { callbackUrl })}>
                Sign in with {provider.name}
              </button>
            </div>
          ))}
        </div>
      )
    }

이제 아주 평화롭고 깔끔하게 home으로 이동했다!

profile
플러터, 리액트

0개의 댓글