[드롭다운 관련 useRef]
드롭다운을 구현할 때 본 버튼 영역과 드롭다운 영역 외의 부분을 클릭했을 때 드롭다운이 사라지게 하는 방법을 알아보았다. 그렇게 크게 어려운 난이도의 이슈가 아니긴 하지만, 코드를 세부적으로 뜯어보기 위해 다시 한번 복기하려고 한다.
다음은 드롭다운 이미지이다. 이 기능을 구현할 때 신경써야 할 부분은 다음과 같다.
이러한 기능을 완벽히 구현하기 위해서는 리액트의 useRef를 사용해야 한다.
❓그 이유는 무엇인지 톺아보자.
우선 그 전에, useRef가 무엇인지 알고가야 한다.
useRef란?
- useRef를 사용하면 리액트 컴포넌트 내에서 DOM 요소에 직접적으로 접근이 가능하다.
- useRef를 사용하면 리액트의 렌더링 주기에 영향을 받지 않고도 컴포넌트 간에 상태를 유지할 수 있다.
- useRef를 사용하여 DOM 요소에 직접적으로 접근할 경우, 리액트가 불필요한 렌더링을 방지할 수 있다.
다시 드롭다운을 생각해보자.
드롭다운 메뉴는 일반적으로 유저가 마우스를 클릭하거나 호버하는 액션이 활성화될 때 해당 요소의 위치나 스타일을 변경해야 하는 기능이라고 할 수 있다. 즉 이 부분에서 useRef를 사용하면 DOM 요소에 바로 접근하여 변경이 가능하다는 점이다.
이제 useRef에 대해 알아보았으니 코드를 함께 보자.
// dropdown 오픈됐는지의 여부를 판단하는 state
const [showDropDown, setShowDropDown] = useState(false);
// dropdown 외부클릭 시 닫기
const dropdownRef = useRef<HTMLLIElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowDropDown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
//
return(
...
<li ref={dropdownRef}>
<div onClick={() => setShowDropDown(!showDropDown}>
icon
</div>
<div
clsx={showDropDown ? 'show' : ''}
onClick={() => setShowDropDown(!showDropDown)}
>
dropdown 컨텐츠 박스
</div>
</li>
...
)
먼저 전체적인 로직은 다음과 같다.
하나씩 살펴보자.
const [showDropDown, setShowDropDown] = useState(false);
먼저 드롭다운의 오픈여부를 판단하는 state가 당연히 필요할 것이다.
const dropdownRef = useRef<HTMLLIElement>(null);
이제 useRef를 사용할 때이다. 지금 여기서는 li 태그에 걸어야 하므로 HTMLLIElement를 타입으로 넣어 작성한다. 초기값은 null로 넣어주고 dropdownRef를 콘솔에 찍어보면 다음과 같이 출력된다.
{current : 초기값}
의 객체 형태로 반환된다.
설명을 덧붙이자면, current라는 키값을 지닌 프로퍼티가 생성되고, 추후 value를 변경하거나 업데이트하고자 할 때 이 current를 사용하면 된다.
이제 만들어놓은 반환값인 dropdownRef에 접근하기 위해 다음과 코드를 작성할 수 있다.
return(
...
<li ref={dropdownRef}>
...
</li>
...
)
이제 li 태그에 dropdownRef라는 객체에 접근이 가능해졌다.
이제 가장 중요한 앞서 말한 네 가지 조건을 지켜가며 구현해야 한다.
- 글로벌 아이콘을 클릭했을 때 드롭다운이 펼쳐져야 함
- 다시 글로벌 아이콘을 클릭했을 때 드롭다운이 닫혀야 함
- 드롭다운 컨텐츠 내의 영역을 클릭할 수 있어야 함
- 드롭다운 컨텐츠 외부의 영역을 클릭하면 드롭다운이 닫혀야 함
return(
...
<li ref={dropdownRef}>
<div onClick={() => setShowDropDown(!showDropDown}>
icon
</div>
<div
clsx={showDropDown ? 'show' : ''}
onClick={() => setShowDropDown(!showDropDown)}
>
dropdown 컨텐츠 박스
</div>
</li>
...
)
이렇게 되면 1, 2, 3번은 구현이 완료됐다.
이제 가장 중요한 4번을 위해 세부적으로 톺아보자!
dropdownRef의 current를 톺아보면 엄청나게 많은 메서드들이 존재한다.
여기서 우리는 contains라는 메서드를 이용해보자.
contains
⇒ 현재 클릭한 엘리먼트를 인자로 넘기게 되면 참조 중인 엘리먼트에 속해 있을 경우엔 true 반환, 그렇지 않으면 false 반환
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowDropDown(false);
}
};
우선 외부 영역을 클릭했을 때 호출될 이벤트 핸들러 함수를 먼저 정의해보자. 여기선 마우스 이벤트가 매개 변수로 들어오게 된다.
만약 dropdownRef가 존재하고(==드롭다운 메뉴가 렌더링되었고), 클릭된 요소가 dropdownRef 안에 속하지 않는 경우에 setShowDropDown(false)
를 호출하여 드롭다운 메뉴를 닫는 플로우로 이루어진다.
document.addEventListener('mousedown', handleClickOutside);
다음으로, 문서 전체에 mousedown 이벤트 리스너를 등록한다.
여기서 잠깐! addEventListener에 대해 짚고가자.
addEventListener
eventTarget.addEventListener(’eventType’, function)
eventTarget : 해당 이벤트를 적용할 DOM을 가져와준다.
eventType : 어떤 타입의 이벤트를 적용할 것인지 작성해준다.
function : 실행할 함수
따라서 우리는 document
, 즉 문서 전체에 mousedown 이벤트 리스너를 등록하게 된 것이고, 이 리스너는 사용자가 문서의 어떤 부분이든 마우스 버튼을 누를 때 호출된다.
이제 드롭다운 컨텐츠 외부 영역을 클릭하면, 잘 닫히게 된다.
❓왜 mousedown인 걸까?
헷갈릴 만한 포인트를 짚고 넘어가자.
click
event → 마우스 버튼을 눌렀다가 놓을 때 시작됨
mousedown
event → 버튼을 처음 누르는 순간 시작됨
그런데 여기서 잠깐❗
addEventListener()을 사용한다면, 여기에서 그치면 안된다는 것을 알게 되었다. 활용도가 없는 addEventListener()는 removeEventListener()을 통해 추후 삭제해줘야 메모리 릭(memory leak==메모리 누수)을 막을 수 있다고 한다.
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
즉, 다음과 같은 코드를 추가해야 한다.
❓왜 메모리 릭
이 생기는 걸까?
→ addEventListener()을 통해 이벤트 리스너를 등록하면, 이 listener는 컴포넌트가 마운트되어 있는 동안 계속 존재하게 된다. 이것이 side-effect
가 된다!
→ 만약 제거를 해주지 않으면, 컴포넌트가 언마운트되어도 이벤트 리스너가 계속 남아 있게 된다. 즉, 여기서 메모리 릭이 초래되고 이로 인해 더 이상 필요하지 않은 리소스를 소비하게 된다!!
❓왜 return 구문 안에서 제거할까?
→ useEffect 로직 내의 리턴 구문은 컴포넌트가 언마운트될 때 호출이 된다. 따라서, 이 시점에서 메모리 릭을 방지하고 불필요한 리소스 소비를 최소화할 수 있다.
이제 모든 설명이 끝났다.
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowDropDown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
코드 완성! 😈
[드롭다운 관련 useRef]
❔다른 방법도 있을까?
이 방법 외에 또다른 방법이 있는지 찾아보았다.
그렇게 발견한 리액트 라이브러리 react-click-outside
!
import React, { useState } from 'react';
import { useClickOutside } from 'react-click-outside';
const DropdownMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef();
const handleClickOutside = () => {
setIsOpen(false);
};
// useClickOutside 훅을 사용하여 드롭다운 메뉴 외부 클릭을 처리합니다.
useClickOutside(() => {
setIsOpen(false);
}, dropdownRef);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle Dropdown</button>
{isOpen && (
<div ref={dropdownRef}>
{/* 드롭다운 메뉴 내용 */}
</div>
)}
</div>
);
};
위 코드처럼 훅을 활용해 외부 클릭 이벤트를 처리할 수 있다고 한다.
새롭게 알게 된 라이브러리다! 요런 게 있구나~
쉬운 내용이지만 그래도 한번쯤은 정리하면 좋을 것 같아 이렇게 작성해보았다. useRef를 처음 보는 사람도 이해하기 쉽게 작성하려고 노력해봤는데, 어떻게 보일지 잘 모르겠지만 나름 만족한다! 히히
이제 드롭다운 관련 기능 구현은 완전히 버그 없이 잘 만들 수 있을 것 같다!
추후 TIL이 아닌 다른 태그로 아래 공식 문서를 원어로 읽으면서 다시 복습해야겠다!
이해하는 과정을 서술하는 것이 중요하다는 것을 깨달았다.
그리고 오늘 저녁에 타코 먹으러 간다. 신난다!
좋은 글 감사합니다!