본문 바로가기
React

[React] 바깥 클릭 시 닫히는 드롭다운 (useOnClickOutside 훅)

by teamnova 2025. 9. 3.
728x90

 

 

1. use On Click Outside 

- “바깥 클릭 시 닫기” 로직에 대한 커스텀 훅을 따로 만들어줍니다. 

- ref 영역 밖을 누르면 handler를 호출하는 로직입니다. 

- 이렇게 개별적으로 분리해놓으면 여러 컴포넌트에서 사용할 수 있습니다. 

import { useEffect } from "react";

/** ref 영역 밖을 클릭/터치하면 handler 실행 */
export default function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const onPointerDown = (e) => {
      const el = ref?.current;
      if (!el || el.contains(e.target)) return;
      handler(e);
    };
    document.addEventListener("pointerdown", onPointerDown, { passive: true });
    return () => document.removeEventListener("pointerdown", onPointerDown);
  }, [ref, handler]);
}

- 컴포넌트에서 ref(DOM 요소)와 handler(닫기 함수 등)를 넘기면, 지정한 요소 밖에서 클릭/터치가 일어났을 때 handler가 실행되도록 이벤트를 걸어줍니다. 

 

 

2. Nav Drop down 

import { useRef, useState, useEffect } from "react"
import { Link as ScrollLink } from "react-scroll"
import useOnClickOutside from "../hooks/useOnClickOutside"

export default function NavDropdown({ label, items = [] }) {
  const [open, setOpen] = useState(false)
  const ref = useRef(null)
  useOnClickOutside(ref, () => setOpen(false))

  // ESC로 닫기 (선택)
  useEffect(() => {
    const onKey = (e) => e.key === "Escape" && setOpen(false)
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [])

  return (
    <div ref={ref} className="relative inline-block">
      <button
        onClick={() => setOpen(v => !v)}
        aria-haspopup="menu"
        aria-expanded={open}
        className="cursor-pointer hover:text-indigo-500 transition-colors"
      >
        {label}
      </button>

      {open && (
        <div
          role="menu"
          className="absolute top-full left-0 mt-2 bg-white dark:bg-neutral-900
                     shadow-lg border border-gray-200 dark:border-neutral-800
                     rounded-lg py-2 min-w-40 z-50"
        >
          {items.map((item, i) => (
            <ScrollLink
              key={i}
              to={item.to}
              spy={true}
              smooth={true}
              offset={-80}
              duration={500}
              onClick={() => setOpen(false)}      // 항목 클릭 시 닫힘
              className="block px-4 py-2 text-gray-700 dark:text-gray-200
                         hover:bg-indigo-50 dark:hover:bg-neutral-800
                         hover:text-indigo-600 transition-colors text-sm cursor-pointer"
            >
              {item.label}
            </ScrollLink>
          ))}
        </div>
      )}
    </div>
  )
}

- 네비게이션용 드롭다운 메뉴 컴포넌트

- 버튼 클릭으로 open 상태 토글 → 열리면 메뉴가 렌더링 됩니다. 

-  useOnClickOutside   바깥 클릭 시 자동으로 닫힙니다. 

- useEffect로 ESC 키 누르면 닫힙니다. 

 

3. Header. jsx 

import { Link as ScrollLink } from "react-scroll";
import DarkModeToggle from "./DarkModeToggle";
import NavDropdown from "./NavDropDown"; // ✅ 새 드롭다운 컴포넌트

function Header() {
  const dropdownItems = {
    about: [
      { label: "개인정보", to: "about" },
      { label: "경력사항", to: "about" },
      { label: "학력사항", to: "about" },
      { label: "기술스택", to: "about" },
    ],
    projects: [
      { label: "웹 개발", to: "projects" },
      { label: "모바일 앱", to: "projects" },
      { label: "데이터 분석", to: "projects" },
      { label: "AI/ML", to: "projects" },
      { label: "블록체인", to: "projects" },
    ],
    contact: [
      { label: "이메일", to: "contact" },
      { label: "전화번호", to: "contact" },
      { label: "소셜미디어", to: "contact" },
      { label: "위치", to: "contact" },
    ],
  };

  return (
    <header className="fixed top-0 w-full bg-white dark:bg-neutral-900 shadow-md z-50">
      <div className="max-w-5xl mx-auto px-6 py-4 flex justify-between items-center">
        <h1 className="text-xl font-bold text-indigo-600 dark:text-indigo-400">
          OOO.dev
        </h1>

        <nav className="flex items-center space-x-6 text-gray-700 dark:text-gray-200 font-medium">
          {/* Home: 단일 링크 그대로 유지 */}
          <ScrollLink
            to="home"
            spy={true}
            smooth={true}
            offset={-80}
            duration={500}
            className="cursor-pointer hover:text-indigo-500 transition-colors"
          >
            Home
          </ScrollLink>

          {/* ✅ 클릭 열림 + 바깥 클릭/ESC 닫힘 */}
          <NavDropdown label="About" items={dropdownItems.about} />
          <NavDropdown label="Projects" items={dropdownItems.projects} />
          <NavDropdown label="Contact" items={dropdownItems.contact} />

          {/* 다크모드 토글은 맨 우측 고정 */}
          <DarkModeToggle />
        </nav>
      </div>
    </header>
  );
}

export default Header;

- 기존 헤더에 NavDropdown 를 추가해줍니다. 

 

 

시연 영상입니다.