Next.js

[Next.js] SSR 페이지에서 session pre-fetch 하기 (next-auth)

lheunoia 2023. 5. 1. 01:41

 

 

 

Groom Blog Logo

 

 

 

 

현재(2023.04.30) Next.js 프레임워크를 사용하여 블로그를 개발 중에 있습니다. 블로그에서의 사용자 인증은 next-auth 라이브러리로 구현했습니다. next-auth 라이브러리에서 제공하는 NextAuth는 로그인 시 signIn 메소드를 통해 사용자의 정보를 session에 저장하기 때문에 사용자의 정보를 가져오거나 로그인/로그아웃 상태를 판별할 때 session을 사용하면 편리합니다. 이번 포스트에서는 Next.js 페이지에 적용한 렌더링 방식과 그 렌더링 방식에서 sessionpre-fetch하는 방법에 대해 알아 보겠습니다. 🙂

 

 

 

 

 

🌳 Why SSG -> SSR?

 

 

Next.js 공식 문서 > Pages

 

 

Next.js 공식 문서에서는 "성능 상의 이유로 서버 측 렌더링을 통한 정적 생성 방식을 사용하는 것을 권장한다. 정적으로 생성된 페이지는 성능 향상을 위해 별도의 구성 없이 CDN에 의해 캐시될 수 있으나 Server-side Rendering 방식이 유일한 옵션일 수 있다."고 설명합니다. 글쓴이의 경우, 개발 중인 공유 블로그의 성향과 기존 방식(SSG)의 제약 사항때문에 SSR 방식을 선택했습니다.

 

 

 

 

1. 공유 블로그의 성향

개발 중인 블로그는 개인 블로그가 아닌, velog와 같이 여러 사람이 함께 사용할 수 있도록 만든 블로그입니다. 여러 사용자들이 함께 사용하는 블로그이니 만큼 페이지마다 데이터의 변화가 잦고, 블로그의 특성상 검색 엔진 최적화(SEO)를 요구합니다.

 

 

 

 

2. 기존 방식(SSG)의 문제점

getStaticProps 함수의 context 파라미터에는 req 속성이 존재하지 않기 때문에 session을 pre-fetch 할 수 없습니다.

session을 pre-fetch하지 못해 발생하는 문제는 아래와 같습니다.

 

 

1. 페이지 재요청마다 사용자 관련 데이터를 포함한 컴포넌트가 깜빡인다.

    ➡️  사용자의 경험을 감소시킨다.

 

 

2. 페이지 재요청마다 session을 서버에 요청힌다.

    ➡️ 불필요한 리소스 요청으로 서버의 성능을 저하시킨다.

 

 

session API

 

 

 

 

 

💡 SSR 방식에서 session pre-fetch 하기

 

getServerSideProps 함수의 context 파라미터에는 req 속성이 존재하기 때문에 sessionpre-fetch 할 수 있습니다! 🎉

 

 

 

1.  getSession 메소드로 session pre-fetch

 

 

pages/index.tsx

import { GetServerSideProps } from 'next';
import { getSession } from 'next-auth/react';
import { dehydrate, QueryClient } from '@tanstack/react-query';

import getUserWithEmail from '../apis/user/getUserWithEmail';

const Home = () => {
	// 생략...
};

// Home 컴포넌트보다 먼저 실행
export const getServerSideProps: GetServerSideProps = async (context) => {
  const session = await getSession(context);
  const email = session ? session.user.email : null;

  const queryClient = new QueryClient();
  context.res.setHeader('Cache-Control', 'public, max-age=59');

  // 유저 정보를 캐시
  await queryClient.prefetchQuery(['user', email], () => getUserWithEmail(email));

  return {
    props: {
      session,
      dehydratedState: dehydrate(queryClient),
    },
  };
};

next-auth/react 라이브러리의 getSession 메소드를 통해 session을 응답받고, session.user.email을 query key로 하여 유저 정보를 캐시합니다. (Home 컴포넌트를 포함한 자식 컴포넌트에서 StaleTime 이내에 동일한 쿼리를 참조했을 경우, 캐시된 유저 정보를 응답받기 위해) 응답받은 sessionqueryClient를 반환하여 최상위 페이지(_app.ts) props로 전달합니다.

 

 

 

 

2. 자식 컴포넌트에 session 내려주기

 

 

pages/_app.tsx

import React, { useEffect, useState } from 'react';
import Head from 'next/head';
import { SessionProvider } from 'next-auth/react';
import { RecoilRoot } from 'recoil';
import { QueryClientProvider, QueryClient, Hydrate } from '@tanstack/react-query';

const App = ({ Component, pageProps: { session, ...pageProps } }) => {
  const [queryClient] = useState(() => new QueryClient());
	
  // 콘솔에 session이 잘 찍히는지 확인해 보세요.
  console.log('session', session);

  return (
    <>
      <SessionProvider session={session}> // Component 컴포넌트에 session 내려줌
        <QueryClientProvider client={queryClient}>
          <Hydrate state={pageProps.dehydratedState}>
            <RecoilRoot>
                <Head>
                  <title>Groom</title>
                </Head>
                <Component {...pageProps} /> // pre-fetch된 session 사용 가능
            </RecoilRoot>
          </Hydrate>
        </QueryClientProvider>
      </SessionProvider>
    </>
  );
};

export default App;

페이지 컴포넌트에서 전달한 sessionSessionProvider 컴포넌트의 session 속성에 전달합니다. 그러면 SessionProvider로 감싼 Component 컴포넌트 중 session을 전달한 컴포넌트를 포함한 하위 컴포넌트에서 pre-fetch된 session을 사용할 수 있습니다.

 

 

 

 

3. useSession 훅으로 session 사용하기

 

 

components/main/UserProfile.tsx

import React, { useEffect } from 'react';
import { useSession, signOut } from 'next-auth/react';\

import { useGetUser, useGetUserWithEmail } from '../../hooks/query/user';
import SkeletonUserProfile from '../skeleton/SkeletonUserProfile';
import * as S from '../../styles/ts/components/main/UserProfile';

const UserProfile = () => {
  const { data: session } = useSession(); // pre-fetch된 session
  
  // 캐시된 유저 정보가 존재하므로 서버에 요청하지 않음
  const { data: user, isLoading } = session ? useGetUserWithEmail(session.user.email) : useGetUser();

  useEffect(() => {
    if (session?.user === null) {
      alert('세션이 만료되었습니다.');
      signOut({ redirect: false });
    }
  }, [session]);

  return (
   <S.UserProfileWrapper>
      {isLoading ? ( // 캐시된 유저 정보가 있으므로 isLoading: false
        <SkeletonUserProfile />
      ) : (
        // 생략...
      )}
    </S.UserProfileWrapper>
  );
};

export default UserProfile;

session을 전달한 컴포넌트를 포함한 하위 컴포넌트에서 useSession 훅을 사용하면 pre-fetchsession을 사용할 수 있습니다. index.tsx에서 getUserWithEmail 함수를 호출하여 유저 정보를 캐시해 놓았고, ['user', email] 쿼리 키의 staleTimeinfinity로 설정해 두었기 때문에 index 페이지를 재요청해도(새로고침해도) 유저 정보를 새로 불러오지 않고, 캐시한 데이터를 재사용합니다.

 

 

 

 

4. 결과

캐시한 데이터를 재사용하기 때문에 컴포넌트가 깜빡임없이 동작합니다. 🙂

 

 

 

기존 방식 (SSG) - 페이지 재요청 시 서버에 유저 정보 요청

SSG

 

 

 

 

수정한 방식 (SSR) - 페이지 재요청 시 캐시된 유저 정보 재사용

SSR

 

 

 

 

 

📍 React Query 사용하지 않는 경우

+ session 속성 재정의하기

react-query 라이브러리를 사용하지 않는다면 오로지 session을 통해서만 유저 정보를 얻을 수도 있습니다.

 

 

session 객체

 

기존 session의 user 객체에는 email, name 속성만 존재하기 때문에 User 모델에 정의되어 있는 속성을 사용하려면 몇 가지 설정을 해주어야 합니다.

 

 

 

 

1. interface 키워드를 통해 Session 객체 확장

 

 

@types/next-auth.d.ts

import NextAuth from 'next-auth';
import { PostItem, UserType } from '../types';

// session.user 속성 재정의
declare module 'next-auth' {
  interface Session {
    user: {
      id: number;
      email: string;
      name: string;
      imageUrl: string;
      posts: PostItem[];
      neighbors: UserType[];
    };
  }
}

@types 폴더 안에 next-auth.d.ts 이름의 파일을 생성한 후 위와 같이 코드를 작성합니다. User 모델의 속성 중 사용하려는 속성을 작성하여 Sessionuser 객체를 선언적으로 확장합니다. (interface의 선언적 확장은 여기에서 확인해 보세요!)

 

 

 

 

2. 로그인된 유저의 정보 가져오기

 

 

pages/api/auth/[...nextauth].ts

import NextAuth from 'next-auth';
import prisma from '../../../lib/prisma';

export default NextAuth({
  providers: [
    // 생략...
  ],
  callbacks: {
    async session({ session }) {
    // 데이터베이스에서 유저의 정보 가져오기
      const user = await prisma.user.findUnique({
        where: { email: session.user?.email },
        select: {
          id: true,
          email: true,
          name: true,
          imageUrl: true,
          posts: {
            select: {
              id: true,
              title: true,
              category: {
                select: {
                  name: true,
                },
              },
              isPublic: true,
              allowComments: true,
            },
          },
          neighbors: {
            select: {
              id: true,
            },
          },
        },
      });

      session.user = user;
      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
});

위와 같이 session 콜백 함수를 추가하면 session 콜백 함수가 호출될 때마다 select한 속성을 포함한 유저의 정보를 가져옵니다.

 

 

🙌🏻 session 콜백 함수가 호출되는 경우

  • /api/session endpoint 호출
  • useSession 사용 (only Client Side)
  • getSession 사용 (only Client Side)

 

 

 

 

3. 재정의된 속성이 추가된 session.user

 

재정의된 session.user

 

session을 콘솔에 출력하여 재정의된 속성이 추가되어 잘 나타나는지 확인해 보세요!

이제 session 객체만으로 유저의 정보를 사용할 수 있습니다.

 

 

 

 

 

💭 개선할 점

SSR 방식에서 session을 pre-fetch하여 기존의 문제점들을 해결했으나 속도가 느린 이슈가 존재합니다.

 

 

 

SSG 방식에서의 index.json 로드

 

SSG - index.json

 

 

SSR 방식에서의 index.json 로드

 

SSR - index.json

 

 

 

두 방식의 Waiting for server response를 비교해보면 SSG 방식이 SSR 방식보다 약 416배 빠른 것을 알 수 있습니다. SSR 방식에서는 Cache-Control 속성을 설정하여 캐시의 유효 기간을 설정할 수 있는데요. 현재는 max-age=59로 설정해 두었기 때문에 59초 이내의 요청에 대해서는 캐시된 HTML 파일을 재사용합니다.

 

 

 

59초 이내 요청의 경우

 

SSR - from disk cache

 

SSR - from disk cache

 

 

59초 내에 페이지를 재요청하면 memory 또는 disk에 캐시된 데이터를 곧바로 내려주기 때문에 훨씬 빠른 응답을 받을 수 있습니다.

다음 포스터에서는 Cache-Control에 대해 자세히 다루어 보도록 하겠습니다. 💪🏻

 

 

 

 

 

반응형