본문 바로가기

개발 공부/React

Next.js 에서 모달 띄우기와 서버작업을 통한 유저 정보 받기 및 등록 (2)

코드 출처 : 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

 

users/page.tsx

'use client';

const Page = () => {
  const pageStyle = PageSva();
  const { openModal } = useModals();

  const handleClickCreateButton = () => {
    openModal({
      id: MODAL.USER_CREATE,
      component: <UserCreateFormModal />,
    });
  };

  return (
    <Box className={pageStyle.wrapper}>
      <Button onClick={handleClickCreateButton}>Create</Button>
    </Box>
  );
};

export default Page;

 

useModals.ts

import { ModalContext } from '@/contexts/modal-provider';
import { useContext } from 'react';

const useModals = () => {
  const { openModal, closeModal } = useContext(ModalContext);

  return { openModal, closeModal };
};

export default useModals;

 

 

 

openModal({
  id: MODAL.USER_CREATE,
  component: <UserCreateFormModal />,
});
  • openModal 함수는 모달을 열 때 필요한 설정을 객체 형태로 받습니다.
    • id: MODAL.USER_CREATE
      • 이 ID는 모달의 고유 식별자입니다.
      • MODAL.USER_CREATE는 constants/modal-key-constants에서 정의된 값으로, 특정 모달을 식별하기 위한 키 역할을 합니다.
    • component: <UserCreateFormModal />
      • 이 속성은 열릴 모달에 렌더링할 컴포넌트를 지정합니다.
      • 여기서는 <UserCreateFormModal /> 컴포넌트를 모달 내부에 렌더링합니다.
  • 버튼을 클릭하면 openModal이 호출되어 MODAL.USER_CREATE라는 ID를 가진 모달이 열리고, 모달 내부에는 <UserCreateFormModal /> 컴포넌트가 렌더링됩니다.
  • 기존에 ModalProvider를 통해 모달을 생성합니다.

 

 

1. 모달을 띄우는 곳

1. 최상단의 layout.tsx 의 <Modal />

import './globals.css';
import Modal from '@/components/common/modal';
import CoreProvider from '@/contexts/core-provider';
import dayjs from 'dayjs';
import 'dayjs/locale/ko';
import type { Metadata } from 'next';
import localFont from 'next/font/local';

dayjs.locale('ko');

const pretendard = localFont({
  src: '../fonts/PretendardVariable.woff2',
  weight: '45 920',
  variable: '--font-pretendard',
});

export const metadata: Metadata = {
  title: 'Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${pretendard.className} font-sans`}>
        <CoreProvider>
          {children}
          <Modal />
        </CoreProvider>
      </body>
    </html>
  );
}

 

 

 

  • ModalProvider
    • CoreProvider 내부에서 ModalProvider가 사용되며, ModalContext를 통해 애플리케이션의 모든 하위 컴포넌트들이 모달 상태에 접근할 수 있게 됩니다.
    • 이 컨텍스트는 모달을 열고 닫는 로직을 제어하는 데 사용됩니다.
  • Modal
    • Modal은 실제로 모달 UI를 렌더링하는 컴포넌트입니다.
    • ModalProvider에서 관리하는 상태인 openedModals를 참조하여 활성화된 모달의 목록을 화면에 렌더링합니다.
  • 왜 Modal이 RootLayout에 위치하는가?
    • Modal은 UI 출력의 위치를 정하는 컴포넌트입니다. 보통 모달은 애플리케이션의 루트나 레이아웃 수준에서 렌더링되어야 합니다.
    • 이유는 모달이 다른 UI 요소들 위에 표시되어야 하기 때문에, DOM 트리 상단에 위치해야 합니다.
    • z-index가 높아도, DOM 트리 하위에 있으면 다른 컴포넌트의 CSS나 레이아웃에 영향을 받을 수 있습니다.
    • 반면, ModalProvider는 상태 관리를 위한 로직을 제공하는 역할이므로 애플리케이션의 컨텍스트 트리 안쪽에 있어도 문제가 되지 않습니다.
  • 요약
    • ModalProvider는 모달 상태를 관리하는 컨텍스트 제공자이고,
    • Modal은 해당 컨텍스트를 소비하여 모달을 렌더링하는 UI 컴포넌트입니다.
    • 따라서 Modal은 루트 레벨에서 렌더링되어야 다른 UI 위에 렌더링될 수 있으며, ModalProvider는 CoreProvider 내부에 있어도 전혀 문제가 없습니다.

modal.tsx

'use client';

import { MOTION } from '@/constants/motion-constants';
import { ModalContext } from '@/contexts/modal-provider';
import useModals from '@/hooks/use-modals';
import { sva } from '@/styled-system/css';
import { Box } from '@/styled-system/jsx';
import { motion } from 'motion/react';
import { useContext, useEffect } from 'react';

const Modal = () => {
  const modalStyle = ModalSva();
  const { openedModals } = useContext(ModalContext);
  const { closeModal } = useModals();

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

  return (
    <>
      {openedModals.map((modal, index) => {
        const { id, component } = modal;

        return (
          <Box className={modalStyle.wrapper} key={index}>
            <Box className={modalStyle.dimmed} onClick={() => closeModal(id)} />
            <motion.div className={modalStyle.modal} {...MOTION.POP}>
              {component}
            </motion.div>
          </Box>
        );
      })}
    </>
  );
};

export default Modal;

const ModalSva = sva({
  slots: ['wrapper', 'dimmed', 'modal'],
  base: {
    wrapper: {
      position: 'fixed',
      left: 0,
      top: 0,
      zIndex: 10,
      display: 'flex',
      height: '100dvh',
      width: '100vw',
      alignItems: 'center',
      justifyContent: 'center',
      overflow: 'hidden',
    },
    dimmed: {
      position: 'fixed',
      top: 0,
      left: 0,
      width: '100dvw',
      height: '100dvh',
      overflow: 'hidden',
      backgroundColor: 'black',
      opacity: 0.48,
    },
    modal: {
      position: 'relative',
    },
  },
});

 

 

  • ModalSva()는 스타일을 반환하는 함수입니다. 스타일은 styled-system을 사용하여 관리됩니다.
  • openedModals는 ModalContext에서 관리되는 배열로, 현재 열려 있는 모달들의 목록을 포함합니다.
  • closeModal은 모달을 닫는 함수입니다.
  • useEffect는 모달이 열리면 document.body.style.overflow를 'hidden'으로 설정해 페이지의 스크롤을 막고, 모달이 닫히면 'auto'로 돌아가게 합니다.
  • openedModals.map()은 열려 있는 모달들을 하나씩 렌더링하며, 각 모달의 배경을 클릭하면 해당 모달이 닫히도록 onClick 이벤트를 설정합니다.
  • sva는 스타일을 정의하는 함수로, 스타일을 객체 형태로 반환합니다. slots는 스타일을 적용할 각 요소의 이름을 정의하고, base는 각 요소에 대한 스타일을 정의합니다.
  • wrapper: 모달 전체를 감싸는 요소로, 화면 전체를 차지하고 중앙에 정렬됩니다.
  • dimmed: 모달의 배경을 흐리게 처리하는 요소입니다. 배경은 검은색이고 투명도를 48%로 설정하여, 모달이 강조되도록 합니다.
  • modal: 실제 모달 내용이 들어가는 요소로, 상대적인 위치로 설정되어 있습니다.