특정 영역인 div 외부를 클릭했을 때, 이를 감지해 div 영역을 숨겨보자
프로젝트에서 모달창이나 div를 닫기 위해 '취소' 및 '확인' 버튼을 생성해 모달창이 닫히도록 구현해놓았지만, 특정 영역 외부를 클릭했을 때 해당 영역이 자동으로 닫히면 사용성이 더 좋아질 것으로 보여 이 기능을 구현하게 되었다.
처음 훅으로 구현했다가 추후에 더 많은 영역에 탑재될 것으로 보여 리팩토링해서 컴포넌트로 만들어 사용했다. 이 과정을 정리했다.
1. 훅 구현
특정 영역 외부를 감지하고 이를 닫는 기능을 가진 커스텀 훅을 생성하고,
사용할 컴포넌트에서 해당 훅을 불러와 사용해 볼 것이다.
1. 훅에서 구현할 내용
- 특정 영역에 부여할 Ref를 생성한다. (이 영역 외부가 클릭되면 모달창이 닫히도록 한다)
- 특정 영역 외부를 클릭했을 때 발생시킬 함수를 등록
2. 사용할 컴포넌트에서 구현할 내용
- 각 컴포넌트에서 해당 기능을 담은 훅을 가져와 특정 영역에 Ref를 부여한다.
- 함수 제어값과 콜백함수를 훅 props로 전달한다. (훅에서 콜백함수를 실행시킬 것이다)
구현
1. 훅 생성 - useClickedOutsideHook
export function useClickedOutsideHook ({ isClickable, callback }) {
// 1. 특정 영역에 부여할 Ref 생성
const targetRef = useRef();
useEffect(() => {
if(!isClickable) {
return;
}
// 2. 이벤트리스너 등록
document.addEventListener('mousedown', handleCheckClickedOutside);
// cleanup - 이벤트리스너 제거
return () => {
document.removeEventListener('mousedown', handleCheckClickedOutside);
}
}, [isClickable]);
const handleCheckClickedOutside = (e) => {
if(targetRef.current && !targetRef.current.contains(e.target)) {
callback();
}
}
return targetRef;
}
- 특정 영역을 가리킬 targetRef를 생성한다.
- useEffect에서 마우스 버튼을 눌렀을 때
handleCheckClickedOutside()
가 실행될 수 있도록 이벤트 리스너를 등록한다. (document에 바로 등록. 어디를 클릭해도 이벤트를 발생시켜야 하기 때문) - 만약 mousedown이벤트가 발생하면 `handleCheckClickedOutside()`가 실행되고,
targetRef.current.contains()
로 클릭된 영역이 targetRef에 포함되는지 확인한다. 포함되지 않는다면 외부 영역을 클릭한 것이므로 callback함수를 실행시킨다. 이때 callback함수가 div를 닫는 역할이다.
여기서 useEffect의 cleanup함수로 mousedown 이벤트 리스너를 제거해줘야 한다. 해당 이벤트 리스너를 제거하지 않으면 이전에 등록됐던 이벤트가 제거되지 않아 그 다음 마우스를 클릭했을 때 이벤트가 중복으로 발생된다.
useEffect의 return함수인 cleanup함수는 useEffect의 dependency가 []인 경우 컴포넌트가 사라질 때, dependency가 있는 경우 해당 값이 업데이트 되기 전에 실행된다.
2. 컴포넌트에서 훅 사용 - DateSearchFieldView
위에서 구현한 useClickedOutsideHook
훅을 날짜 선택창 컴포넌트에서 사용한다.
export default function DateSearchFieldView ({
// 특정 영역이 open되어있는지 나타내는 상태값
const [datePickerOpen, setDatePickerOpen] = useState(false);
const datePickerRef = useClickedOutsideHook({
isClickable: datePickerOpen, // 콜백함수 제어값
callback: onActionToggleDatePicker // 콜백함수
});
const onActionToggleDatePicker = () => {
if(datePickerOpen) {
setDatePickerOpen(false);
} else {
setDatePickerOpen(true);
}
}
...
return (
// 특정 영역 element에 datePickerRef값 부여
<ButtonBox ref={datePickerRef}>
...
</ButtonBox>
)
DateSearchFieldView
컴포넌트에서 해당 훅을 가져와 props값으로 함수 제어값과 콜백함수를 전달한다.useClickedOutsideHook
에서 리턴받은 targetRef를 해당 컴포넌트에서 사용할 ref값 datePickerRef로 부여한다.- 날짜 선택창이 open되어있을 때만(datePickerOpen값이 true) 이벤트를 등록할 수 있도록 제한을 둔다. 이는 `useClickedOutsideHook` 훅에서 `isClickable`로 사용된다.
2. 컴포넌트 구현
훅을 사용하면 리턴받는 Ref값을 각 컴포넌트마다 선언해줘야 하고, 등록한 영역이 어디인지 눈에 잘 들어오지 않아 컴포넌트화 시켜 사용하려고 한다.
1. `useClickedOutsideHook`를 컴포넌트로 변경 - ClickableWrapper
export function ClickableWrapper({
children,
isClickable = true,
callback = () => { }
}) {
const targetRef = useRef();
useEffect(() => {
if (!isClickable) {
return;
}
document.addEventListener('mousedown', handleCheckClickedOutside);
return () => {
document.removeEventListener('mousedown', handleCheckClickedOutside);
};
}, [targetRef, isClickable]);
const handleMouseDownEvent = (e) => {
if (targetRef.current && !targetRef.current.contains(e.target)) {
callback();
}
}
return (
<div ref={targetRef}>
{children}
</div>
);
}
- targetRef를 반환하지 않고, 특정 영역을 나타내는 div를 반환해 컴포넌트로 사용한다.
2. 컴포넌트 사용 - DateSearchFieldView
export default function DateSearchFieldView ({
// 특정 영역이 open되어있는지 나타내는 상태값
const [datePickerOpen, setDatePickerOpen] = useState(false);
const onActionToggleDatePicker = () => {
if(datePickerOpen) {
setDatePickerOpen(false);
} else {
setDatePickerOpen(true);
}
}
...
return (
// 특정 영역 element을 ClickableWrapper컴포넌트로 사용
<ClickableWrapper
isClickable={datePickerOpen}
callback={onActionToggleDatePicker}
>
...
</ClickableWrapper>
)
- ClickableWrapper컴포넌트를 사용해, props값으로 함수 제어값과 콜백함수를 전달한다.
여러 페이지에서 해당 기능을 사용한다면 해당 컴포넌트를 재사용할 수 있을 것 같다.
개인적으로 훅보다 컴포넌트로 만들어 사용한게 코드도 깔끔하고 어느 영역에 적용하는지 잘보여서 좋은 것 같다. 특히 한 컴포넌트에서 해당 기능이 여러개 필요할 때 더욱 깔끔하게 사용할 수 있을 것 같다.
참고자료 😀
https://velog.io/@miyoni/TIL37
'React' 카테고리의 다른 글
[React] useEffect와 cleanup함수 (0) | 2024.03.10 |
---|