드롭다운(Dropdown)에 필요한 기능은 다음과 같다.
먼저 목록을 출력하는 드롭다운 컴포넌트의 경우 일단 검색 정렬, 기간 필터링 기능에서부터 중복 사용되기 때문에 재사용이 가능한 형식으로 구현했다.
export default function SortOption({
sortOptionData,
setCurrentSort,
currentSort,
}: SortOptionPropsType): JSX.Element {
const sortDropdownRef = useRef<HTMLDivElement>(null);
// 1)
const [isSortActive, setIsSortActive] = useOutsideClick(sortDropdownRef, false);
// 2)
const onClickSortBtn = useCallback(() => {
setIsSortActive(prev => !prev);
}, [setIsSortActive]);
return (
<SearhSortOptionContainer>
<div className="option__btn" onClick={onClickSortBtn}>
<span>{currentSort.name}</span>
<MdKeyboardArrowDown />
</div>
// 3)
<div
className={`sort__dropdown dropdown ${isSortActive ? 'sort-active' : ''}`}
ref={sortDropdownRef}
>
<ul>
{sortOptionData.map(sortOption => (
<li
key={sortOption.value}
onClick={() => setCurrentSort(sortOption)}
className={`${
currentSort.value === sortOption.value ? 'item-active' : ' '
}`}
>
<span>{sortOption.name}</span>
</li>
))}
</ul>
</div>
</SearhSortOptionContainer>
);
}
1) 먼저 드롭다운 컴포넌트에 필요한 상태는 활성 또는 비활성 상태로 나눌 수 있는데 커스텀 훅 useOutsideClick()
으로 제어한다.
2) onClickSortBtn 함수는 드롭다운 컨테이너에 onClick 이벤트가 발생할 때 활성, 비활성 상태를 업데이트해준다.
3) 이 상태를 이용해 className에 속성을 추가하여 css에서 모달창이 보이거나 보이지 않도록 변경한다. (css-in-js에서는 props로 전달 가능)
css
export const SearhSortOptionContainer = styled.div<{
currentTheme: null | string;
}>`
position: relative;
.option__btn {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
svg {
margin-left: 0.571em;
}
}
.sort__dropdown {
right: 0;
text-align: center;
visibility: hidden;
}
.dropdown {
border: 1px solid ${({ theme }) => theme.grayScale[4]};
border-radius: 3px;
background-color: #fff;
position: absolute;
margin-top: 1.071em;
z-index: 9999;
}
.dropdown.sort-active {
visibility: visible;
}
ul {
padding: 1.429em 1.571em;
li {
white-space: nowrap;
cursor: pointer;
padding: 0.5em 0;
}
}
`;
1단계에서 끝난다면 반쪽의 드롭다운이라 할 수 있다. 사용자가 드롭다운 메뉴를 다 이용했다면 다른 기능을 방해하지 않도록 사라지게 만들어야 한다. 외부 클릭을 감지하는 이 기능은 꼭 드롭다운 만이 아니라 다른 모달 등에서 재사용될 수 있는 로직을 포함하기 때문에 custom hook으로 만들었다.
useOutsideClick.tsx
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
type UseOutsideClickType = ( // 선택된 요소(el), Active 초기값(initialState)을 넘겨받는다.
el: React.RefObject<HTMLDivElement>,
initialState: boolean
) => [boolean, Dispatch<SetStateAction<boolean>>];
export const useOutsideClick: UseOutsideClickType = (el, initialState) => {
const [isActive, setIsActive] = useState(initialState);
// 1)
useEffect(() => {
const pageClickEvent = e => {
if (el.current !== null && !el.current.contains(e.target)) {
setIsActive(!isActive);
}
};
// 2)
if (isActive) {
window.addEventListener('click', pageClickEvent);
}
return () => {
window.removeEventListener('click', pageClickEvent);
};
}, [isActive, el]);
// 3)
return [isActive, setIsActive];
};
1) useEffect를 사용하여 isActive값이 변경될 때, pageClickEvent함수를 실행시킨다. pageClickEvent 함수는 요소의 current값이 null이 아니면서, 드롭다운에 클릭된 요소가 포함되어 있지 않은 조건(외부 클릭 시)이라면 상태를 비활성으로 업데이트하고 드롭다운을 닫는다.
2) 추가로 드롭다운이 활성화되어 있을 때만 이벤트리스너를 실행하기 위한 코드를 작성 해준다.
3) 마지막으로 isActive/setIsActive를 포함하는 배열을 반환하면 상태관리를 개별 컴포넌트가 아니라 커스텀 훅에서 공통적으로 처리할 수 있게 된다.
References
Building a Dropdown Menu Component With React Hooks