Next.js - 로그인, Context

nooori·2024년 6월 13일
post-thumbnail

인스타그램 프로젝트를 하면서 어려웠던 부분들 내 방식대로 정리

로그인

참고했던 next-auth 홈페이지
🏴 https://next-auth.js.org/getting-started/client#usesession
🏴 https://next-auth.js.org/getting-started/example#add-api-route

  • 세션(로그인 정보) 가져오기
  1. 서버 컴포넌트에서 session에 들어있는 사용자 정보 얻으려면 getServerSession(api) from /next-auth를 통해서 가져올 수 있다.
    ex) 
		const session = await getServerSession(authOptions);
		const user = session?.user;
  1. 클라이언트 컴포넌트에서 session에 들어있는 사용자 정보를 얻으려면 useSession(hook) from /next-auth/react 사용하기! 이 때 useSession은 context에서 만들어둔 AuthContext의 SessionProvier로 children을 context로 감싸야만 자식 컴포넌트에서 사용 가능하다.
	ex)
    	const {data: session} = useSession();
		const user = session?.user;

그럼 context를 사용해보자! Next.js에서도 Context를 지원하는데 다만, client component로 만들어야 한다. 서버 컴포넌트에서는 직접적으로 접근할 수가 없기 때문이다.

서버와 클라이언트 환경에서 어떻게 렌더링하는지!
🏴 https://nextjs.org/docs/app/building-your-application/rendering

Next.js Context

  • 클라이언트 컴포넌트가 되는 경우
    'use client'를 선언한 컴포넌트의 자식 컴포넌트가 자동으로 클라이언트 컴포넌트가 되려면, 자식 컴포넌트를 직접 import한 경우만 해당된다.
  • Context 컴포넌트의 경우
    children prop을 통해 자식을 전달받는 컴포넌트는 자식 컴포넌트가 원래 환경(서버 또는 클라이언트)에서 동작할 수 있다.
  • AuthContext 예시
    AuthContext 같은 컴포넌트는 자식 컴포넌트가 서버 컴포넌트인지 클라이언트 컴포넌트인지 알 수 없고, 그 역할을 결정하지도 않는다.

=> 요약하자면 자식 컴포넌트까지 클라이언트 컴포넌트가 될지 안될지의 여부는 단순히 DOM Tree 구조로 결정되는 것이 아니라 import 여부에 따라 달라진다. 또한 AuthContext는 import해오는 것이 아니라 children prop으로 전달받기 때문에 자식이 자동적으로 클라이언트 컴포넌트가 되지 않는다.

'use client'
import {SessionProvider} from 'next-auth/react'

type Props = {
  children: React.ReactNode
}

export default function AuthContext({ children }: Props) {
  return (
    <SessionProvider>
      {children}
    </SessionProvider>
  )
}

👆 'use client' 선언해서 클라이언트 컴포넌트임을 명시한 후에 AuthContext 컴포넌트를 정의했다. SessionProvider로 감싸서 하위 컴포넌트들이 인증 세션에 접근할 수 있도록 한다.

import AuthContext from '@/context/AuthContext'
import SWRConfigContext from '@/context/SWRConfigContext'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={openSans.className}>
      <body className='w-full bg-neutral-50 overflow-auto'>
        <AuthContext>
          <header className='sticky top-0 bg-white z-10 border-b'>
            <div className='max-w-screen-xl mx-auto'>
              <Navbar />
            </div>
          </header>
          <main className='w-full flex justify-center max-w-screen-xl mx-auto'>
            <SWRConfigContext>
              {children}
            </SWRConfigContext>
          </main>
        </AuthContext>
        <div id='portal' />
      </body>
    </html>
  )
}

👆 인스타그램 프로젝트에서는 사용자 정보를 표시해야 하는 header부터 mainRkwl 모두 AuthContext로 감쌌다. 이렇게 하니까 header와 main에 포함된 모든 컴포넌트들이 인증 세션이 접근할 수 있었다.

Sanity 로그인 과정

🖐 강의를 듣고 알게 된 로그인하는 과정을 상세하게 설명해두었다. 만약 next-auth가 로그인을 어떻게 가능하게 해주는지 궁금하다면 한번 읽어보는 것을 추천한다!

NextAuth 인증 시스템 콜백(callbacks)
🏴 https://next-auth.js.org/configuration/callbacks

import { addUser } from '@/service/user';
import NextAuth, { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({ 
      clientId: process.env.GOOGLE_OAUTH_ID || '',
      clientSecret: process.env.GOOGLE_OAUTH_SECRET || '',
    }),
  ],
  callbacks: {
    async signIn({ user: { id, name, image, email } }) {
      if (!email) {
        return false;
      }
      addUser({
        id,
        name: name || '',
        image,
        email,
        username: email.split('@')[0]
      });
      return true
    },
    async session({ session, token }) {
      const user = session?.user;
      if (user) {
        session.user = {
          ...user,
          username: user.email?.split('@')[0] || '',
          id: token.id as string,
        };
      }
      return session
    },
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    }
  },
  pages: {
    signIn: '/auth/signin',
  }
}

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
  • 로그인을 하기 위해 먼저 프론트엔드가 백엔드에게 '로그인하고 싶다'라고 요청을 보낸다. 로그인을 하지 않은 상태로 API 요청을 하게 되면 그 요청 안에는 당연히 로그인 정보가 없다. 요청과 반응에는 항상 header 부분에 cookie를 설정할 수 있는데 그 cookie 안에는 로그인한 사용자의 정보를 더해서 보낸다. 따라서 처음에 로그인 요청을 하면 그 요청 header안에는 아무것도 들어있지 않다. request 요청이 성공적으로 처리가 되어서 로그인이 된다면 백엔드 서버가 프론트엔드에게 요청에 대한 반응을 보내준다. 이 때'로그인 성공적으로 동작했고 보내준 반응 header 안에 cookie라는 객체 정보가 들어있어. 그 객체 안에 사용자의 로그인된 정보인 jwt token이 들어있어. 앞으로 API 요청을 할 때 이 jwt token을 사용해서 백엔드 서버에서 너를 유효한 사용자로 인식할게'하면서 하면서 보내준다. 이런 구현 사항들 덕분에 serverless가 가능하다. 모든 로그인한 사용자의 세션정보를 서버에서 별도로 저장하고 관리하지 않아도 되기 때문에 (=상태를 서버상에 보관하지 않아도 되기 때문에) 각각의 API route가 serverless로 동작할 수 있다.
  • jwt token을 http only option: true로 설정하면 로그인에 대한 모든 정보를 response header cookie 안에 넣어서 보내준다. 그럼 http only가 true이기 때문에 브라우저가 자동적으로 안전한 곳에 사용자의 로그인 정보를 저장해둔다. 그 후에 다른 API 요청을 할 때 이 요청의 request header에 jwt token과 함께 사용자 로그인 세션에 대한 정보가 함게 포함해서 자동으로 묶어서 하게 된다. 참고로 이 자동으로 묶는 일은 브라우저가 해준다. 그럼 백엔드 서버에서 header 안에 들어있는 정보를 토대로 로그인한 사용자인지 아닌지 확인 할 수 있다. 위 코드에서 방금 설명한 과정을 자동으로 해준다. 로그인하고 싶은 사용자가 오면 그 header에 있는 정보들을 기반으로 해서 누구인지 파악하고 => async signin session에서도 로그인한 사용자가 있다면 그 사용자 header에 있는 데이터를 사용해서 누군인지 확인한다. => async session 내부적으로 어떻게 구현하는지 개발자가 알 필요가 없는 이유는 next-auth에서 자동으로 해주기 때문이다. 또한, client에서도 우리가 실제 사용자 정보를 request header에서 읽어왔는지 아닌지를 확인하지 않아도 되는 이유도 next-auth 라이브러리가 자체적으로 useSession이라는 hook을 제공해주고 그 hook이 알아서 해주기 때문이다.
  • 마지막으로 사용자 로그인이 성공적으로 되었다면 sanity--client를 사용해서 content lake에 사용자를 추가했다. next-auth에서 사용자가 로그인했다는 이벤트가 발생하면 addUser() 함수를 통해 sanity에게 데이터를 업데이트 해달라고 한다.

0개의 댓글