Programming/Client

[React] 전역에서 사용 가능한 모달 만들기

Jiwoo 2023. 1. 8. 17:03

📌 모달 구현

1. 모달의 오픈 여부를 알 수 있는 상태값이 있는 slice 만들기

// ModalSlice.jsx
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
    modal: false,
};

export const modalSlice = createSlice({
    name: 'modal',
    initialState,
    reducers: {
        showModal: (state) => {
            state.modal = true;
        },
        closeModal: (state) => {
            state.modal = false;
        },
    },
});

export default modalSlice.reducer;
export const { showModal, closeModal } = modalSlice.actions;

2. store에 만든 모달 slice 추가

// Store.jsx
import { configureStore } from "@reduxjs/toolkit";
import modalReducers from "./ModalSlice";

const store = configureStore({
  reducer: {
    modal: modalReducers,
  },
});

export default store;

3. 모달 component 만들기

  • style의 width, height는 prop로 지정하게 해 재사용성 높임

  • ReactDom의 createPortal를 사용하여 가장 상단에 위치하도록 함

react-modal 라이브러리 사용

라이브러리의 Modal 태그를 사용하면 isOpne, onRequestClose, stytle 등 자체 속성을 통해
여닫힘, 바깥 부분 클릭시 닫힘 등의 기능을 사용할 수 있다.

import React from 'react';
import styled from 'styled-components';
import { IoClose } from 'react-icons/io5'; // 아이콘 추가
import Modal from 'react-modal';
import { createPortal } from 'react-dom';
import { useSelector, useDispatch } from 'react-redux';
import { closeModal } from '../store/ModalSlice';

const DialogBox = styled.div``;

const FormContainer = styled.div``;

const CloseButton = styled.button` // 닫기 버튼 설정
    position: absolute;
    right: 3%;
    top: 5%;
    border: none;
    background-color: inherit;
    font-size: 1rem;
    cursor: pointer;
`;

ModalContainer.defaultProps = { // 모달 props 기본값
    children: '',
    w: '50%',
    h: '70%',
    overflow: 'auto',
};

function ModalContainer({ children, w, h, overflow }) {
    const modalOpen = useSelector((state) => state.modal.modal);
    const dispatch = useDispatch();

    return createPortal(
        <Modal
            isOpen={modalOpen}
            onRequestClose={() => dispatch(closeModal())}
            ariaHideApp={false}
            style={{
                overlay: {
                    position: 'fixed',
                    backgroundColor: 'rgba(255, 255, 255, 0.75)',
                },
                content: {
                    top: '50%',
                    left: '50%',
                    transform: 'translate(-50%, -50%)',
                    padding: '2rem',
                    zIndex: 100,
                    width: `${w}`,
                    height: `${h}`,
                    overflow: `${overflow}`,
                },
            }}
        >
            <DialogBox>
                <CloseButton
                    onClick={() => {
                        dispatch(closeModal());
                    }}
                >
                    <IoClose size={25} />
                </CloseButton>
                <FormContainer>{children}</FormContainer>
            </DialogBox>
        </Modal>,
        document.getElementById('modal'),
    );
}

export default ModalContainer;

라이브러리 미사용

import { useEffect, useRef } from "react";

import { createPortal } from 'react-dom';

import { useSelector, useDispatch } from 'react-redux';

import { closeModal } from '../store/ModalSlice';

export default function Modal({ child, w ='50%', h='70%' }) {
      const modalOpen = useSelector((state) => state.modal.modal);
    const dispatch = useDispatch();

    const modalRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        // 모달창 바깥 영역 클릭시, 모달창 닫기
        const handleClickBackground = (event: MouseEvent) => {
            if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
                dispatch(closeModal())
            }
        };

        document.addEventListener('mousedown', handleClickBackground);

        // 모달 컴포넌트 사라질 때, 이벤트핸들러 제거
        return () => {
            document.removeEventListener('mousedown', handleClickBackground);
        };
    });

    return createPortal(
      { isModalOpen && (<div
            ref={modalRef}
            style={`width:${w}, height:${h}`}>
            <button
                type="button"
                onClick={() => dispatch(closeModal())}
            >X</button>
            <div>
                {children}
            </div>
        </div> 
        )}
    )
}

index.html에 모달 넣기

모든 페이지보다 상위에 넣어야 하므로 root 바로 아래에 넣어줌

...
<body>
    <div id="root"></div>
    <div id="modal"></div>
</body>

📌 모달 사용법

1. 상단에 사용할 훅 불러오기

import ModalContainer from "./../components/Modal";
import { useDispatch, useSelector } from "react-redux";
import { showModal } from "../store/ModalSlice";

2. 컴포넌트에 dispatch와 modal상태값을 불러오기

const dispatch = useDispatch();
const modalOpen = useSelector((state) => state.modal.modal);

3. 모달을 띄울 버튼에 onClick 넣어서 모달 상태 변경하기

onClick={() => dispatch(showModal())}

4. 띄울 모달을 리턴 부분에 추가하기

<ModalContainer w="세로길이" h="가로길이">모달안에 들어갈 내용</ModalContainer>

* w, h를 넘겨주지 않으면 기본 50% * 70% 크기

* 한 화면에서 모달 여러 개를 사용한다면 해당 컴포넌트 안에서 각 모달의 상태를 관리할 것