강의를 듣다가 모달을 구현하는 두 가지 방식을 배웠다

import React, { useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';

interface ModalProps {
  open: boolean;
  children: React.ReactNode;
  onClose: () => void;
}

function Modal({ open, children, onClose }: ModalProps) {
  const dialog = useRef<HTMLDialogElement | null>(null);

  useEffect(() => {
    if (open) {
      dialog.current?.showModal();
    } else {
      dialog.current?.close();
    }
  }, [open]);

  return createPortal(
    <dialog className="modal" ref={dialog} onClose={onClose}>
      {open ? children : null}
    </dialog>,
    document.getElementById('modal') as Element,
  );
}

export default Modal;

State를 외부에 두고 직관적이고 간단하게 관리하는 방식

import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { createPortal } from 'react-dom';

interface ModalProps {
  children: React.ReactNode;
  buttonCaption: string;
}

const Modal = forwardRef<{ open: () => void; close: () => void }, ModalProps>(
  (
    props: ModalProps,
    ref: React.Ref<{ open: () => void; close: () => void }>
  ) => {
    const dialog = useRef<HTMLDialogElement>(null);
    const modalRoot = document.getElementById('modal-root');
    const { children, buttonCaption } = props;

    useImperativeHandle(ref, () => ({
      open() {
        dialog.current?.showModal();
      },
      close() {
        dialog.current?.close();
      },
    }));
    return createPortal(
      <dialog ref={dialog}>
        {children}
        <form method="dialog">
          <button>{buttonCaption}</button>
        </form>
      </dialog>,
      modalRoot as Element
    );
  }
);

Modal.displayName = 'Modal';

export default Modal;

State를 내부에 두고 useImperativeHandle을 이용하여

상태 제어 함수를 외부에 노출을 해 제어하는 방식이다.

 

두 방법의 장단점은 다음과 같다

 

첫 번째 방법(외부에서 상태 관리)

장점

1. 단순하고 직관적인 상태 관리

단점

1. 컴포넌트 간 의존성 증가

- 부모 컴포넌트의 상태에 의존하게 된다, 이럴 경우 특정 부모 컴포넌트에 종속적이게 되어 재사용성이 낮아질 수 있다.  모달의 상태를 가지고 있는 컴포넌트가 한 화면에 한개만 렌더링 된다면 상관 없겠지만 2개 이상이 되는 순간부터 상태관리가 매우 어려워지게 된다.

2. 비즈니스 로직의 결합

3. 복잡한 로직 처리

 

두 번째 방법(내부에서 상태 관리, 외부에 메서드 제공)

장점

1. 모달 열기/닫기 메서드 제공

단점

1. 조금 더 복잡한 구현

2. 추가적인 코드 필요

- 모달을 사용하기 위해서는 부모 컴포넌트에서 ref를 설정해야 하고, 이를 통해 모달 제어해야 하므로 추가적인 코드 작성이 필요하다

 

내가 선택할 방법

두 번째 방법은 내가 모달창을 여러개 띄우는 웹은 만드는 경우가 아니라면 굳이 사용하지 않을 것 같고

첫 번째 방법을 Context API를 사용하여 전역에 모달의 상태를 만들어 

1. 상태 충돌 가능성 감서

2. 상태 관리의 복잡성 감소

3. 재사용성 향상

4. 비즤스 로직의 결합 감소

등을 꾀한다

하지만 여전히 

1. 상태 관리의 성능 문제

- Context API는 상태가 변경될 때 해당 Contex모두를 리렌더링 하기 때문에 성능에 영향을 끼칠 수 있다

2. 디버깅의 어려움

- 어느 컴포넌트에서 이벤트가 발생했는지 추적하기 어렵고, 여러 Provider를 사용할 경우 더 파악하기 힘들다

등의 문제가 생기는데

추후 Recoil을 도입하여 

해당 문제들을 개선할 것이다

 

 

 

 

 

상태를 이용한 접근 방식(선언형)


1. 상태 기반 제어:

부모 컴포넌트에서 상태를 관리하고, 이를 자식 컴포넌트에 props로 전달하여 모달을 열고 닫습니다.
상태 변경에 따라 컴포넌트가 다시 렌더링됩니다.


2. 간단한 데이터 흐름:
상태를 통해 데이터가 부모에서 자식으로 흐르기 때문에 데이터 흐름이 명확합니다.


3. 리렌더링:
상태 변경 시 컴포넌트가 리렌더링되므로, 성능에 영향을 미칠 수 있습니다.


forwardRef와 useImperativeHandle을 이용한 접근 방식
1. 명령형 제어:
부모 컴포넌트가 자식 컴포넌트의 특정 메서드를 호출하여 동작을 제어합니다.
상태 변경 없이 직접적으로 자식 컴포넌트의 메서드를 호출합니다.


2. 캡슐화:
자식 컴포넌트의 내부 구현을 부모 컴포넌트에 노출하지 않고, 필요한 메서드만 노출합니다.
자식 컴포넌트의 내부 상태나 DOM 요소에 직접 접근할 수 있습니다.


성능:
직접적인 돔 도작으로 상태 변경 없이 메서드를 호출하므로, 불필요한 리렌더링을 피할 수 있습니다.

 

- 상태 기반 접근 방식은 데이터 흐름이 명확하고 간단하지만, 상태 변경 시 리렌더링이 발생합니다.

 

- forwardRef와 useImperativeHandle을 이용한 접근 방식은 명령형으로 제어할 수 있으며, 불필요한 리렌더링을 피할 수 있습니다. 또한, 자식 컴포넌트의 내부 구현을 캡슐화할 수 있습니다.

 

상황에 따라 적절한 방법을 선택하면 됩니다. 예를 들어, 모달의 열기/닫기와 같은 단순한 동작은 상태 기반 접근 방식이 더 직관적일 수 있지만, 복잡한 동작이나 성능 최적화가 필요한 경우 forwardRef와 useImperativeHandle을 사용하는 것이 더 적합할 수 있습니다.

 

 

리액트에서는 선언적 방식을 더 추천하잖아 근데 이 모달에서는 선언적 방식으로 하게되면 불필요한 리랜더링이 일어나는 것 같은데 너는 어떤 방식을 더 추천해

 

리액트에서는 일반적으로 선언적 방식을 추천하지만, 모달과 같은 특정 컴포넌트에서는 성능 최적화가 필요할 때 명령형 방식이 유리할 수 있습니다. 두 접근 방식의 장단점을 고려하여 상황에 맞게 선택하는 것이 중요합니다.

선언적 방식의 장점과 최적화 방법

장점:

 

  • 가독성 및 유지보수성: 상태 기반으로 UI를 관리하기 때문에 코드가 직관적이고 이해하기 쉽습니다.

 

 

 

  • React의 철학에 부합: 상태가 변할 때 UI가 자동으로 업데이트되므로 예측 가능성이 높습니다.

 

 

최적화 방법:

 

  • React.memo 사용: 불필요한 리렌더링을 방지하기 위해 React.memo를 사용하여 컴포넌트를 메모이제이션할 수 있습니다.

 

 

 

  • useCallback 및 useMemo 활용: 함수와 값을 메모이제이션하여 자식 컴포넌트에 전달되는 프롭스가 변경되지 않도록 합니다.

 

 

 

  • 상태 분리: 모달의 열림/닫힘 상태와 다른 상태를 분리하여 리렌더링 범위를 최소화할 수 있습니다.
import React, { useState, useCallback } from 'react';
import Modal from './components/Modal';

const App = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const openModal = useCallback(() => setIsModalOpen(true), []);
  const closeModal = useCallback(() => setIsModalOpen(false), []);

  return (
    <div>
      <button onClick={openModal}>모달 열기</button>
      <Modal isOpen={isModalOpen} onClose={closeModal}>
        <p>모달 내용</p>
      </Modal>
    </div>
  );
};

export default App;

 

추천

상황에 따라 선택하세요:

 

  • 복잡한 상태 관리이 필요 없는 간단한 모달: 선언적 방식을 사용하고, React.memo와 같은 최적화 기법을 적용하여 성능을 유지하세요.

 

 

 

  • 성능이 매우 중요한 경우 또는 직접적인 제어가 필요한 경우: 명령형 방식을 사용하여 불필요한 리렌더링을 피하고 세밀한 제어를 할 수 있습니다.

 

 

결론적으로, 대부분의 경우 선언적 방식을 사용하는 것이 유지보수성과 가독성 측면에서 더 유리합니다. 하지만 실제로 성능 문제가 발생하거나 특정한 제어가 필요할 때는 명령형 방식을 고려하는 것이 좋습니다. 최적화를 위해서는 먼저 선언적 방식을 시도하고, 필요에 따라 명령형 방식으로 전환하는 접근이 권장됩니다.

+ Recent posts