본문 바로가기

개발 공부/React

Next.js에서 레이아웃 구성 (3) / ModalProvider

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

 

GitHub - Cluster-Taek/next14-boilerplate

Contribute to Cluster-Taek/next14-boilerplate development by creating an account on GitHub.

github.com

 

 

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;

 

ModalProvider

'use client';

import React, { createContext, useCallback, useEffect, useState } from 'react';

export interface ModalComponentType {
  id: React.Key;
  component: React.ReactNode;
}

interface ModalContextType {
  openedModals: ModalComponentType[];
  openModal: (modalComponent: ModalComponentType) => void;
  closeModal: (id: React.Key) => void;
}

interface ModalContextProps {
  children: React.ReactNode;
}

export const ModalContext = createContext<ModalContextType>({} as ModalContextType);

const ModalProvider: React.FC<ModalContextProps> = ({ children }) => {
  const [openedModals, setOpenedModals] = useState<ModalComponentType[]>([]);

  const openModal = useCallback((props: ModalComponentType) => {
    setOpenedModals((modals) => {
      return [...modals, { ...props }];
    });
  }, []);

  const closeModal = useCallback((id: React.Key) => {
    setOpenedModals((modals) => {
      return modals.filter((modal) => modal.id !== id);
    });
  }, []);

  useEffect(() => {
    document.body.style.overflow = openedModals.length > 0 ? 'hidden' : 'auto';
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, [openedModals]);

  useEffect(() => {
    const handleRouteChange = () => {
      setOpenedModals([]);
    };

    window.addEventListener('popstate', handleRouteChange);
    return () => {
      window.removeEventListener('popstate', handleRouteChange);
    };
  }, []);

  return (
    <ModalContext.Provider
      value={{
        openedModals,
        openModal,
        closeModal,
      }}
    >
      {children}
    </ModalContext.Provider>
  );
};

export default ModalProvider;

 

전역적으로 모달의 열림/닫힘 상태를 관리하고, 동적 모달 컴포넌트를 쉽게 추가 및 제거할 수 있습니다.

 

 

 

ModalComponentType 인터페이스

'use client';

import React, { createContext, useCallback, useEffect, useState } from 'react';

export interface ModalComponentType {
  id: React.Key;
  component: React.ReactNode;
}
  • 모달의 ID와 렌더링할 React 컴포넌트를 정의.
  • id: 각 모달을 고유하게 식별.
  • component: 렌더링할 실제 모달 컴포넌트.

 

ModalContextType 인터페이스

interface ModalContextType {
  openedModals: ModalComponentType[];
  openModal: (modalComponent: ModalComponentType) => void;
  closeModal: (id: React.Key) => void;
}
  • 컨텍스트에서 제공할 데이터와 메서드 정의.
  • openedModals: 현재 열려 있는 모달의 배열.
  • openModal: 새로운 모달 열기 함수.
  • closeModal: 특정 모달 닫기 함수.

 

ModalContext 생성

export const ModalContext = createContext<ModalContextType>({} as ModalContextType);
  • 모달 상태와 관련된 데이터 및 메서드를 공유하기 위한 컨텍스트.

ModalProvider 컴포넌트

const ModalProvider: React.FC<ModalContextProps> = ({ children }) => {
  const [openedModals, setOpenedModals] = useState<ModalComponentType[]>([]);
  • openedModals: 현재 열려 있는 모달을 상태로 관리.

 


openModal

  const openModal = useCallback((props: ModalComponentType) => {
    setOpenedModals((modals) => [...modals, { ...props }]);
  }, []);
  • 새로운 모달을 openedModals 배열에 추가.

 

closeModal

  const closeModal = useCallback((id: React.Key) => {
    setOpenedModals((modals) => modals.filter((modal) => modal.id !== id));
  }, []);
  • 특정 ID를 가진 모달을 openedModals 배열에서 제거.

useEffect로 스크롤 비활성화

  useEffect(() => {
    document.body.style.overflow = openedModals.length > 0 ? 'hidden' : 'auto';
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, [openedModals]);
  • 모달이 열려 있을 때 body 스크롤을 비활성화.
  • 모달이 닫히면 스크롤을 다시 활성화.

 

useEffect로 라우트 변경 시 모달 닫기

  useEffect(() => {
    const handleRouteChange = () => {
      setOpenedModals([]);
    };

    window.addEventListener('popstate', handleRouteChange);
    return () => {
      window.removeEventListener('popstate', handleRouteChange);
    };
  }, []);
  • 브라우저 popstate 이벤트(뒤로 가기/앞으로 가기) 발생 시 모든 모달을 닫음.

 

 

 

ModalContext.Provider

  return (
    <ModalContext.Provider
      value={{
        openedModals,
        openModal,
        closeModal,
      }}
    >
      {children}
    </ModalContext.Provider>
  );
  • openedModals, openModal, closeModal 메서드를 컨텍스트로 제공.
  • 하위 컴포넌트에서 이 데이터를 사용할 수 있음.