useDropdown 커스텀훅 만들기

krkorklo·2022년 11월 18일
0

Dropdown

워크스페이스 UI를 만드는데, 워크스페이즈 정렬하는 부분을 맡게 되었다.

다음과 같이 워크스페이스 카드를 정렬하기 위한 드롭다운이 필요했다.

구현하기 전에 두 가지 생각이 들었는데

  1. 드롭다운 UI와 로직을 분리하는게 효율적이지 않을까
  2. 모달이나 드롭다운과 같이 해당 컴포넌트 외부를 클릭하는 경우 닫히게 하는 로직은 재사용하기에 좋지 않을까

결론은,,

custom hook을 만들자

로직 재사용 및 분리를 위해 현재 드롭다운(모달/드롭다운 등을 퉁쳐서ㅎ)의 상태와 이벤트를 custom hook으로 만들기로 결정했다.

필요한 상태는 현재 드롭다운이 열려있는지를 알려주는 상태, 그리고 외부 DOM의 클릭 여부를 확인하기 위한 드롭다운 DOM였고

필요한 이벤트는 드롭다운 버튼을 클릭하는 경우 visibility를 toggle하는 이벤트, 드롭다운 외부 영역을 클릭하는 경우 드롭다운을 없애는 이벤트였다.

useDropdown

const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

상태 선언부터 해보자면, 최상단에 드롭다운의 visibility 상태를 위한 state, 드롭다운 DOM을 가리키는 ref를 선언한다.

useEffect(() => {
	document.addEventListener('click', handleOutsideClick);

	return () => {
		document.removeEventListener('click', handleOutsideClick);
	};
}, []);

const handleOutsideClick = (e: Event) => {
	const current = dropdownRef.current;
	if (isOpen && current && !current.contains(e.target as Node)) setIsOpen(false);
};

다음으로는 드롭다운 외부가 클릭됐을 때 드롭다운이 바뀌게 만들고자 하는 로직을 구현할건데, useEffect를 사용해 documentclick event를 붙여준다.

document가 클릭될 때마다 현재 클릭된 target을 확인하고, 클릭된 부분이 드롭다운 컴포넌트 내에 포함되지 않은 경우에는 modal을 닫히도록 만들었다.

그런데 문제가 발생했다!

outside를 클릭할 때 handleOutsideClick이 제대로 먹히지가 않았다.

열심히 분석해보다 보니 useEffect에서 발생한 문제였다는걸 알았다.

useEffect(() => {
	document.addEventListener('click', handleOutsideClick);

	return () => {
		document.removeEventListener('click', handleOutsideClick);
	};
}, []);

const handleOutsideClick = (e: Event) => {
	const current = dropdownRef.current;
	if (isOpen && current && !current.contains(e.target as Node)) setIsOpen(false);
};

현재 useEffect에서 dependancy 없이 handleOutsideClick이 이벤트로 붙게 되는데, 그러다 보니 handleOutsideClick는 처음 렌더링 될 때에 등록이 되게 되고, 이때 isOpen은 false 상태이다. 그 상태를 계속 기억한 채로 handleOutsideClick이 실행되기 때문에 계속해서 드롭다운이 닫혀있는 상태로 인식된 것이다.
isOpen 상태가 바뀔 때마다 이벤트를 등록해주자!

useEffect(() => {
	document.addEventListener('click', handleOutsideClick);

	return () => {
		document.removeEventListener('click', handleOutsideClick);
	};
}, [isOpen]);

이벤트 등록할 때마다 실수하는데 매번 까먹고는 하는 것 같다,,, 이번엔 찐막실수

const toggleDropdown = () => {
  setIsOpen((prevIsOpen) => !prevIsOpen);
};

이제 마지막으로 드롭다운 버튼을 눌렀을 때 현재 open 상태에 따라 toggle될 수 있도록 이벤트를 선언해주고 export해줬다!

import { useEffect, useRef, useState } from 'react';

function useDropdown() {
	const [isOpen, setIsOpen] = useState(false);
	const dropdownRef = useRef<HTMLDivElement>(null);

	useEffect(() => {
		document.addEventListener('click', handleOutsideClick);

		return () => {
			document.removeEventListener('click', handleOutsideClick);
		};
	}, [isOpen]);

	const toggleDropdown = () => {
		setIsOpen((prevIsOpen) => !prevIsOpen);
	};

	const handleOutsideClick = (e: Event) => {
		const current = dropdownRef.current;
		if (isOpen && current && !current.contains(e.target as Node)) setIsOpen(false);
	};

	return { isOpen, dropdownRef, toggleDropdown };
}

export default useDropdown;

다음과 같이 useDropdown이 완성되었다.

이제 완성된 useDropdown hook을 사용해보면,

function OrderDropdown() {
	const [orderType, setOrderType] = useRecoilState(workspaceOrderState);
	const { isOpen, dropdownRef, toggleDropdown } = useDropdown();

	const handleOrderType = (id: number) => {
		setOrderType(id);
		toggleDropdown();
	};

	return (
		<Dropdown ref={dropdownRef}>
			<div className="dropdown-button" onClick={toggleDropdown}>
				<p>{orderItems[orderType].description}</p>
				<img className="dropdown-icon" alt="dropdown button" src={isOpen ? dropdownActive : dropdownInActive} />
			</div>
			{isOpen && (
				<div className="dropdown-container">
					{orderItems.map((type) => (
						<Description key={type.id} isSelected={orderType === type.id} onClick={() => handleOrderType(type.id)}>
							{type.description}
						</Description>
					))}
				</div>
			)}
		</Dropdown>
	);
}

필요한 컴포넌트에서 useDropdown을 사용해 로직을 숨기고 재사용도 할 수 있게 되었다.

0개의 댓글