useImperativeHandle 이란?

useImperativeHandle is a React Hook that lets you customize the handle exposed as a ref.
useImperativeHandle은 ref로 노출되는 핸들을 사용자가 직접 정의할 수 있게 해주는 React 훅이다.

useImperativeHandle(ref, createHandle, dependencies?)

https://react-ko.dev/reference/react/useImperativeHandle#exposing-a-custom-ref-handle-to-the-parent-component

React에서 외부에서 주입된 prop을 사용하여 모달(또는 레이어)의 열림 상태를 제어

부모 컴포넌트에서 모달의 열림 상태를 관리하고, 이 상태를 모달 컴포넌트에 prop으로 전달

제어 컴포넌트(Controlled Component) 패턴

import React, { useState } from 'react';
import ListModal from './ListModal';

const ParentComponent: React.FC = () => {
    const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

    return (
        <div>
            <button onClick={() => setIsModalOpen(!isModalOpen)}>
                Toggle List
            </button>
            <ListModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
        </div>
    );
};

export default ParentComponent;
import React, { useEffect, useRef } from 'react';

interface ListModalProps {
    isOpen: boolean;
    onClose: () => void;
}

const ListModal: React.FC<ListModalProps> = ({ isOpen, onClose }) => {
    const modalRef = useRef<HTMLDivElement>(null);

    // 외부 클릭 감지
    useEffect(() => {
        const handleClickOutside = (event: MouseEvent) => {
            if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
                onClose();
            }
        };

        document.addEventListener('mousedown', handleClickOutside);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, [onClose]);

    if (!isOpen) {
        return null;
    }

    return (
        <div ref={modalRef} style={{ position: 'absolute', border: '1px solid black', padding: '10px', background: 'white' }}>
            <ul>
                <li>List Item 1</li>
                <li>List Item 2</li>
                <li>List Item 3</li>
            </ul>
        </div>
    );
};

export default ListModal;
import React from 'react';
import ListModal from './ListModal';

const ParentComponent: React.FC = () => {
    let modalRef: React.RefObject<ListModal> = React.createRef();

    const openModal = () => {
        modalRef.current?.openModal();
    };

    return (
        <div>
            <button onClick={openModal}>
                Open Modal
            </button>
            <ListModal ref={modalRef} />
        </div>
    );
};

export default ParentComponent;
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';

export interface ListModalHandles {
    openModal: () => void;
    closeModal: () => void;
}

const ListModal = forwardRef<ListModalHandles>((props, ref) => {
    const [isOpen, setIsOpen] = useState<boolean>(false);
    const modalRef = useRef<HTMLDivElement>(null);

    useImperativeHandle(ref, () => ({
        openModal: () => setIsOpen(true),
        closeModal: () => setIsOpen(false)
    }));

    // 외부 클릭 감지
    useEffect(() => {
        const handleClickOutside = (event: MouseEvent) => {
            if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
                setIsOpen(false);
            }

    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => {
        document.removeEventListener('mousedown', handleClickOutside);
    };
}, []);

if (!isOpen) {
    return null;
}

return (
    <div ref={modalRef} style={{ position: 'absolute', border: '1px solid black', padding: '10px', background: 'white' }}>
        <ul>
            <li>List Item 1</li>
            <li>List Item 2</li>
            <li>List Item 3</li>
        </ul>
    </div>
);
});

export default ListModal;

`ListModal` 컴포넌트는 내부적으로 자신의 상태(`isOpen`)를 관리합니다.

  1. `useImperativeHandle`과 `forwardRef`를 사용하여 `ListModal` 컴포넌트는 `openModal`과 `closeModal` 메서드를 외부로 노출합니다. 이를 통해 부모 컴포넌트가 이 메서드들을 사용할 수 있습니다.

  2. `ParentComponent`에서는 `modalRef`를 사용하여 `ListModal`의 `openModal` 메서드를 호출할 수 있습니다. 이를 통해 부모 컴포넌트에서 모달을 열 수 있습니다.

이 방법을 사용하면 `ListModal` 컴포넌트의 내부 상태를 유지하면서도, 부모 컴포넌트에서 해당 모달을 열고 닫는 것을 제어할 수 있습니다.

다시 정리하기

부모 컴포넌트에서 ListModal 컴포넌트의 닫힘 여부를 알기 위해서는 ListModal 컴포넌트로부터 어떤 형태의 피드백을 받아야 합니다. 이를 위해 ListModal에 콜백 prop을 추가하여 모달이 닫힐 때 이를 부모 컴포넌트에 알릴 수 있습니다.

예를 들어, ListModal에 onClose라는 콜백 prop을 추가할 수 있습니다. 이 콜백은 모달이 외부 클릭에 의해 닫힐 때 호출되며, 부모 컴포넌트에서 이 상태 변화를 감지할 수 있도록 합니다.

다음은 ListModal 컴포넌트에 onClose 콜백을 추가하는 방법입니다:

import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';

export interface ListModalHandles {
    openModal: () => void;
    closeModal: () => void;
}

interface ListModalProps {
    onClose?: () => void;
    children: React.ReactNode;
}

const ListModal = forwardRef<ListModalHandles, ListModalProps>(({ onClose, children }, ref) => {
    const [isOpen, setIsOpen] = useState<boolean>(false);
    const modalRef = useRef<HTMLDivElement>(null);

    useImperativeHandle(ref, () => ({
        openModal: () => setIsOpen(true),
        closeModal: () => {
            setIsOpen(false);
            if (onClose) onClose();
        }
    }));

    // 외부 클릭 감지 및 모달 닫기
    useEffect(() => {
        const handleClickOutside = (event: MouseEvent) => {
            if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
                setIsOpen(false);
                if (onClose) onClose();
            }
        };

        document.addEventListener('mousedown', handleClickOutside);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, [onClose]);

    if (!isOpen) {
        return null;
    }

    return (
        <div ref={modalRef} style={{ position: 'absolute', border: '1px solid black', padding: '10px', background: 'white' }}>
            {children}
        </div>
    );
});

export default ListModal;

이제 부모 컴포넌트에서 ListModal의 onClose prop을 사용하여 모달이 닫힐 때의 동작을 정의할 수 있습니다:

import React, { useRef } from 'react';
import ListModal from './ListModal';

const ParentComponent: React.FC = () => {
    let modalRef = useRef<ListModalHandles>(null);

    const handleModalClose = () => {
        console.log('Modal has been closed');
        // 모달이 닫혔을 때의 추가 동작을

이런 방식으로 컴포넌트를 제어하는 패턴은 "Imperative Handle Pattern" 또는 "Imperative Programming"이라고 합니다. useImperativeHandle과 forwardRef를 사용하여 부모 컴포넌트가 자식 컴포넌트의 메서드나 변수에 직접 접근할 수 있도록 하여, 부모 컴포넌트에서 자식 컴포넌트의 내부 상태를 직접 제어할 수 있게 합니다.

리액트 useImperativeHandle, forwardRef로 부모 컴포넌트가 자식 컴포넌트의 메서드나 변수에 직접 접근