본문 바로가기

개발 공부/React

Next.js에서 레이아웃 구성 (1) / SessionProvider, AuthProvider

코드 출처 : https://github.com/Cluster-Taek/next14-boilerplate

 

 

import AuthProvider from './auth-provider';
import ModalProvider from './modal-provider';
import { QueryProvider } from './query-provider';
import SessionProvider from '@/contexts/session-provider';

interface ICoreProviderProps {
  children?: React.ReactNode;
}

const CoreProvider = ({ children }: ICoreProviderProps) => {
  return (
    <SessionProvider>
      <AuthProvider>
        <QueryProvider>
          <ModalProvider>{children}</ModalProvider>
        </QueryProvider>
      </AuthProvider>
    </SessionProvider>
  );
};

export default CoreProvider;

 

SessionProvider 

'use client';

import { SessionProvider } from 'next-auth/react';

export default SessionProvider;

 

  • 'use client';:
    • Next.js의 App Directory에서 클라이언트 컴포넌트임을 명시.
    • 이 파일이 클라이언트 환경에서만 실행되도록 함.
  • SessionProvider 가져오기:
    • NextAuth.js에서 제공하는 컨텍스트 컴포넌트.
    • 세션 데이터를 컨텍스트로 공유하여 애플리케이션 전반에서 인증 상태를 확인하거나 사용자 정보를 사용할 수 있음.
  • 내보내기:
    • SessionProvider를 기본 내보내기로 제공.
    • 이렇게 하면, 별도의 커스터마이징 없이 간단히 사용할 수 있음.

사용 목적

  • 인증 상태 관리:
    • 사용자가 로그인한 상태인지 확인.
    • 세션 데이터를 통해 사용자 정보를 전역적으로 공유.
  • 클라이언트 사이드:
    • 이 코드는 클라이언트 환경에서 동작하며, 세션 데이터를 렌더링할 때 React 컴포넌트에서 직접 접근 가능.

 

장점

  • 간소화:
    • 별도의 세션 관리 로직을 작성하지 않고도 NextAuth.js와 통합 가능.
  • 유연성:
    • 세션 데이터는 useSession 훅으로 접근하여 필요한 정보를 쉽게 가져옴.
  • SSR 및 CSR 호환:
    • 서버에서 세션 데이터를 프리로드하여 빠른 초기 렌더링 제공.
    • 클라이언트에서도 상태를 동기화.

AuthProvider

'use client';

import Spinner from '@/components/common/spinner';
import { Session } from 'next-auth';
import { useSession } from 'next-auth/react';
import { usePathname, useRouter } from 'next/navigation';
import React, { PropsWithChildren, createContext, useContext, useEffect } from 'react';

interface IAuthContext {
  initialized: boolean;
  session: Session;
}

export const AuthContext = createContext<IAuthContext | null>(null);

export function useAuth() {
  const result = useContext(AuthContext);
  if (!result?.initialized) {
    throw new Error('Auth context must be used within a AuthProvider!');
  }
  return result;
}

const publicPageList = ['/login'];

const isPublicPage = (pathname: string) => {
  return publicPageList.includes(pathname);
};

const AuthProvider = ({ children }: PropsWithChildren) => {
  const router = useRouter();
  const pathname = usePathname();
  const { data: session, status } = useSession();
  const loading = status === 'loading';

  useEffect(() => {
    if (loading) {
      return;
    }

    if (session && isPublicPage(pathname)) {
      router.push('/');
    } else if (!session && !isPublicPage(pathname)) {
      router.push('/login');
    }
  }, [loading, router, session, pathname]);

  if (loading || (session && isPublicPage(pathname))) {
    return <Spinner />;
  }

  if (isPublicPage(pathname)) {
    return <>{children}</>;
  }

  if (!session?.user) {
    return <Spinner />;
  }

  return <AuthContext.Provider value={{ initialized: true, session }}>{children}</AuthContext.Provider>;
};

export default React.memo(AuthProvider);

 

주요 동작

  1. 초기 로딩:
    • next-auth가 세션 상태를 확인하는 동안 스피너가 표시됩니다.
  2. 페이지 접근 제한:
    • 인증되지 않은 사용자가 비공개 페이지에 접근하면 로그인 페이지로 리다이렉트.
    • 인증된 사용자가 로그인 페이지에 접근하면 홈('/')으로 리다이렉트.
  3. 인증 상태 제공:
    • 인증이 완료되면 AuthContext.Provider가 세션 데이터를 자식 컴포넌트에 제공.

 

AuthContext와 useAuth Hook

export const AuthContext = createContext<IAuthContext | null>(null);

export function useAuth() {
  const result = useContext(AuthContext);
  if (!result?.initialized) {
    throw new Error('Auth context must be used within a AuthProvider!');
  }
  return result;
}
  • AuthContext:
    • 인증 정보를 공유하기 위해 생성된 컨텍스트.
    • IAuthContext 타입을 통해 initialized 상태와 session 정보를 포함.
  • useAuth Hook:
    • 컨텍스트 데이터를 쉽게 사용할 수 있도록 제공.
    • AuthContext가 초기화되지 않은 상태에서 호출하면 에러를 던짐.

 

공개 페이지 확인 로직

const publicPageList = ['/login'];

const isPublicPage = (pathname: string) => {
  return publicPageList.includes(pathname);
};

 

  • publicPageList:
    • 인증이 필요하지 않은 공개 페이지들의 경로 목록.
    • 여기서는 '/login'만 포함되어 있음.
  • isPublicPage 함수:
    • 현재 경로(pathname)가 공개 페이지인지 확인.

 

인증 로직과 상태 관리

const AuthProvider = ({ children }: PropsWithChildren) => {
  const router = useRouter();
  const pathname = usePathname();
  const { data: session, status } = useSession();
  const loading = status === 'loading';

  useEffect(() => {
    if (loading) {
      return;
    }

    if (session && isPublicPage(pathname)) {
      router.push('/');
    } else if (!session && !isPublicPage(pathname)) {
      router.push('/login');
    }
  }, [loading, router, session, pathname]);

 

 

  • useSession:
    • next-auth의 훅으로 세션 데이터(session)와 상태(status)를 제공.
    • status가 'loading'이면 세션 상태를 아직 확인 중인 상태.
  • useEffect:
    • 인증 상태에 따라 페이지를 리다이렉트.
    • 동작:
      1. 세션이 있고 현재 페이지가 공개 페이지 → router.push('/').
      2. 세션이 없고 현재 페이지가 공개 페이지가 아님 → router.push('/login').

 

로딩 처리와 UI 렌더링

if (loading || (session && isPublicPage(pathname))) {
  return <Spinner />;
}

if (isPublicPage(pathname)) {
  return <>{children}</>;
}

if (!session?.user) {
  return <Spinner />;
}

return <AuthContext.Provider value={{ initialized: true, session }}>{children}</AuthContext.Provider>;

 

 

  • 로딩 중: 스피너(Spinner) 컴포넌트 렌더링.
  • 공개 페이지 접근 중: 그대로 children을 렌더링.
  • 세션이 없는 경우: 추가 확인을 위해 스피너 렌더링.
  • 세션이 유효한 경우: AuthContext.Provider를 통해 컨텍스트 데이터 제공.

AuthContext 에서 export default React.memo(AuthProvider) 하는 이유

  • AuthProvider 컴포넌트의 불필요한 재렌더링을 방지하여 성능을 최적화하기 위함입니다.

React.memo란?

  • React.memo는 React의 고차 컴포넌트(HOC, Higher-Order Component)로, 컴포넌트가 동일한 props로 호출될 때 렌더링을 생략할 수 있도록 최적화를 제공합니다.
  • 즉, 부모 컴포넌트가 재렌더링되더라도, React.memo로 감싼 자식 컴포넌트는 props가 변경되지 않았다면 재렌더링되지 않습니다.

왜 AuthProvider에 React.memo를 적용했을까?

  1. AuthProvider의 props가 변하지 않으면 재렌더링할 필요가 없음
    • AuthProvider의 역할은 인증 상태를 관리하고, 컨텍스트를 제공하는 것입니다.
    • 만약 children이나 AuthProvider 내부에서 사용하는 상태(useSession, usePathname 등)가 변하지 않았다면, 컴포넌트를 다시 렌더링할 이유가 없습니다.
    • React.memo를 사용함으로써, props가 변하지 않는 한 재렌더링을 방지할 수 있습니다.
  2. 컨텍스트 재생성을 줄여 성능 최적화
    • AuthProvider가 다시 렌더링되면, 내부의 AuthContext.Provider도 다시 생성됩니다.
    • 이로 인해, useAuth로 컨텍스트를 사용하는 하위 컴포넌트들이 불필요하게 다시 렌더링될 수 있습니다.
    • React.memo는 이러한 문제를 줄여줍니다.
  3. 애플리케이션 성능 최적화
    • AuthProvider는 전역 상태와 인증 로직을 관리하기 때문에, 애플리케이션에서 자주 사용될 가능성이 큽니다.
    • 불필요한 렌더링을 줄이면 성능이 크게 개선될 수 있습니다.
  4. children이 변경되지 않으면 렌더링 방지
    • React.memo는 AuthProvider에 전달된 children을 props로 감시합니다.
    • children이 변경되지 않으면 AuthProvider도 다시 렌더링되지 않으므로 효율적인 렌더링이 가능합니다.