리액트

[실패의 기록] Modal 컴포넌트를 재사용하면서, 각각의 다른 children을 렌더링 하고 싶었다..

always_yoki 2023. 6. 15. 00:53
import { useEffect } from "react";
import React from "react";
import classes from "./Modal.module.css";

function Modal({ id, children, closeModal }) {
  //모달창 실행시, 백그라운드 스크롤은 중지
  useEffect(() => {
    const $body = document.querySelector("body");
    const overflow = $body.style.overflow;
    $body.style.overflow = "hidden";
    return () => {
      $body.style.overflow = overflow;
    };
  }, []);

  let selectedChildren = React.Children.map(children, (child) => {
    if(child.props.id === id){
      return child;
    }else{
      return null;
    }
  })

  return (
    <>
      <div className={classes.backDrop} onClick={closeModal} />
      <dialog open className={classes.modal}>
      {selectedChildren}
      </dialog>
    </>
  );
}

export default Modal;

책 검색 모달창을 위해서 사용하던 컴포넌트인데, 로그인창도 모달로 표출되게 하기 위해 이 컴포넌트를 재사용 하고싶었다.  몇가지 난관들이 있었는데,

 

1. Modal의 열고 닫음 상태 관리를 전역으로 올려야 했다

 useContext 를 처음으로 사용! (성장)

 : 상태를 사용해야하는 하위 컴포넌트를 공통으로 가지는 최상위 컴포넌트에서 createContext()를 해줘야 했는데,

header의 메뉴에서 사용해야 했기 때문에 RootRayout에서 만들어줬다.

import { createContext } from "react";
import Header from "../components/Header";
import { Outlet } from "react-router-dom";
import { useState } from 'react';

const modalContext = createContext();

function RootLayout() {
  
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => {
    setIsOpen(true);
  };

  const closeModal = () => {
    setIsOpen(false);
  };

  return (
    <>
      <modalContext.Provider value={{ isOpen, openModal, closeModal }}>
        <Header />
        <Outlet context={{ isOpen, openModal, closeModal }}/>
      </modalContext.Provider>
    </>
  );
}

export default RootLayout;
export {modalContext};

 

2.  <context Provider> 로 jsx 태그들을 잘 감싸주어 하위 컴포넌트에서 공유할수 있게 해주면

<Header> 컴포넌트에서 value들을 사용할 수 있다.

그런데 또 다른 난관에 봉착..

 

<Outlet>은 컴포넌트가 아니라, react-router-dom 에서 제공하는 훅으로써, 여러개의 하위 컴포넌트를 묶어두고 있다.

따라서 Outlet에서는 useContext가 안먹힌다.. ㅠㅠ

 

다행히, 리액트 라우터 v6.1 이상부터는 useOutletContext를 사용할 수 있다. Outlet의 속성에 context를 추가해주고 사용할 value를 넣어주자.

 

import { useContext, useState } from "react";
import classes from "./Login.module.css";
import { Link, useOutletContext, useSearchParams } from "react-router-dom";
import { auth } from "../firebase";
import SignUp from "./SignUp";
import Modal from "../components/Modal";

function Login() {
  const [id, setId] = useState();
  const [password, setPassword] = useState();
  const { isOpen, openModal, closeModal } = useOutletContext();

  return (
    <div className={classes.container}>
      <section className={classes.loginDiv}>
        <>
          <p className={classes.logo}>(제작중)Dive into your Ocean</p>
          <form>
            <div className={classes.input}>
              <input
                type="text"
                onChange={(e) => setId(e.target.value)}
              ></input>
              <input type="password"></input>
              <button>로그인</button>
            </div>
            <p onClick={openModal} className={classes.signUp}>계정이 없으신가요?</p>
          </form>
        </>
        {isOpen && 
          <Modal closeModal={closeModal} id={1} >
            <SignUp id={1} />
          </Modal>
        }
      </section>
    </div>
  );
}

export default Login;

 

그다음, 사용할 하위 컴포넌트 Login으로 돌아와 useOutletContext를  import해준 다음

변수에 useOutletContext()로 할당을 해주면 이제 사용 가능!

 

진짜 다 끝난줄 알았다..

 

또 다른 난관 ^^

 

3. 모달 컴포넌트를 공유 하긴 했는데, children 도 공유해버리네?

내 상상속에서는, modal의 양식만 가져와서 쓰면 각각의 모달로 사용 할 수 있을것 같았는데

코드를 이렇게 짜니까 한가지 상태의 Modal로 똑같이 렌더링이 되어버렸다.

 

header에서 쓰는 모달 ( 책검색창)과 login의 모달(회원가입 창)이 같이 렌더링이 되는..

 

이 현상을 해결하기 위해서 열심히 찾아보니

React.Children.map(children, (child) => { // ... }

이 함수를 이용해서 필터링을 할 수 있는듯 하여

각각의 모달 컴포넌트에 id를 주고, 자식요소에도 같은 id 값을 부여한뒤

id값이 같은 child만 렌더링 하려 했으나 생각대로 안된다. 여전히 한꺼번에 렌더링이 되는데..

해결할 수 있을 것인가. 

 

두둥탁

 

 

 

 

+

생각해보니

modal 컴포넌트의 isOpen, 열림닫힘 메소드를 전역상태로 관리하는 것 자체가 결국 같은 상태객체를 전역적으로 공유한다는 것이니까, 랜더링을 아무리 다르게 하려고 해도. 굳이? 라는 생각이 들었다. 전체 컴포넌트에서 모달은 현재 2군데 밖에 사용하지 않는데.. 코드중복이긴 해도 각 컴포넌트에서 상태를 따로 관리하는게 그렇게까지 비효율적인걸까? 

일단 상태를 각 컴포넌트에서 관리해주는 방법으로 바꿨다.