드롭다운 관련 useRef, EventListener

2cham_ny·2024년 3월 11일
2

TIL

목록 보기
1/8

01 - WHAT

[드롭다운 관련 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라는 객체에 접근이 가능해졌다.

이제 가장 중요한 앞서 말한 네 가지 조건을 지켜가며 구현해야 한다.

  1. 글로벌 아이콘을 클릭했을 때 드롭다운이 펼쳐져야 함
  2. 다시 글로벌 아이콘을 클릭했을 때 드롭다운이 닫혀야 함
  3. 드롭다운 컨텐츠 내의 영역을 클릭할 수 있어야 함
  4. 드롭다운 컨텐츠 외부의 영역을 클릭하면 드롭다운이 닫혀야 함
 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);
    };
  }, []);

코드 완성! 😈


02 - HOW

[드롭다운 관련 useRef]

  • 문제 상황 : 드롭다운 기능 부분에서 외부 클릭시 닫히지 않은 이슈가 생겼음
  • 개선 방법 : useRef와 addEventListener를 활용해 current 감지

❔다른 방법도 있을까?

이 방법 외에 또다른 방법이 있는지 찾아보았다.

npm: react-click-outside

그렇게 발견한 리액트 라이브러리 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>
  );
};

위 코드처럼 훅을 활용해 외부 클릭 이벤트를 처리할 수 있다고 한다.

새롭게 알게 된 라이브러리다! 요런 게 있구나~

03 - RETROSPECT

쉬운 내용이지만 그래도 한번쯤은 정리하면 좋을 것 같아 이렇게 작성해보았다. useRef를 처음 보는 사람도 이해하기 쉽게 작성하려고 노력해봤는데, 어떻게 보일지 잘 모르겠지만 나름 만족한다! 히히

이제 드롭다운 관련 기능 구현은 완전히 버그 없이 잘 만들 수 있을 것 같다!

추후 TIL이 아닌 다른 태그로 아래 공식 문서를 원어로 읽으면서 다시 복습해야겠다!

useRef – React


04 - 오늘의 한마디

이해하는 과정을 서술하는 것이 중요하다는 것을 깨달았다.
그리고 오늘 저녁에 타코 먹으러 간다. 신난다!

profile
😈 기록하며 성장하자!

1개의 댓글

comment-user-thumbnail
2024년 3월 13일

좋은 글 감사합니다!

답글 달기