코딩하는라민

[JavaScript/React] 영역 외 클릭 감지로 모달/프로필 박스 닫기 기능 구현 본문

Core/JavaScript

[JavaScript/React] 영역 외 클릭 감지로 모달/프로필 박스 닫기 기능 구현

코딩하는라민 2023. 12. 7. 01:40
728x90
반응형

[JavaScript/React]  영역 외 클릭 감지로 모달/프로필 박스 닫기 기능 구현

웹 페이지를 구현하다보면 모달창이나 프로필 박스 혹은 사이드바를 구현해야 하는 경우가 많다.
모달창의 예를 들어보면,
모달창의 바깥 영역을 클릭했을 때 창을 닫을 수 있으면 사용자 경험을 향상시킬 수 있다.
 
이번 프로젝트의 경우에는 프로필 박스가 페이지가 전환됨에도 닫아지지 않고, 외부 영역을 클릭해도 상태 변화가 없어 닫아지지 않았다.
그래서 찾아본 기능이 영역 외 클릭 감지 기능이다.
 

👀 기본 레이아웃

더보기
HTML
<div id="wrapper">
  <div id="userName">ramincode</div>
  <div id="profile-box">hello world!</div>
</div>

 

CSS
#wrapper {
  width: 200px;
  height: auto;
  margin: 0 auto;
  position: relative;
}

#username {
  font-size: 25px;
  font-weight: 600;
  text-align: center;
  padding: 8px 20px;
  border-radius: 8px;
}

#profile-box {
  display: none;
  position: absolute;
  left: 50px;
  border: 1px solid #d9d9d9;
  border-radius: 8px;
  width: fit-content;
  padding: 8px 20px;
  font-size: 17px;
  margin-top: 10px;
  background-color: azure;
}

 

📌  토글 박스

Javascript

영역 외 클릭 감지를 위해서 document 에 클릭 이벤트를 준다.
모달이나 드롭다운 같은 요소가 오픈되어 있을 때 외부 영역을 클릭 시 닫는 패턴으로 사용한다.
 

DOM 요소 가져오기

profile-box, username 을 getElementById 로 가져온다.

const profileBox = document.getElementById('profile-box');
const usernameElement = document.getElementById('username');

 

상태 변수 선언

프로필 박스의 open or close 상태를 나타내는 변수를 선언해준다.
초기값은 false 이다.

let isProfileOpen = false;

 

토글 함수 실행 로직

프로필 박스가 열리면 isProfileOpen 은 true 가 될 것이고, 닫히면 false 이다.
프로필 박스가 열려있으면 닫기 위해 closeProfile 함수를 실행하고,
프로필 박스가 닫혀있으면 열기 위해 openProfile 함수를 실행한다.

function toggleProfile(event) {
  event.stopPropagation();
  isProfileOpen ? closeProfile() : openProfile();
}

 
isProfileOpen 이 false 일 경우 프로필 박스를 열기 위해 openProfile 함수를 실행한다.
isProfileOpen 를 true 로 변경하고 profileBox 를 보이게 display: block 속성을 준다.

function openProfile() {
  isProfileOpen = true;
  profileBox.style.display = "block";
}

 
isProfileOpen 이 true 일 경우 프로필 박스를 닫기 위해 closeProfile 함수를 실행한다

function closeProfile() {
  isProfileOpen = false;
  profileBox.style.display = "none";
}

 
usernameElement 에 클릭 이벤트를 할당해준다.

 usernameElement.addEventListener('click', toggleProfile);

 

👀 여기까지 전체 코드

더보기
const profileBox = document.getElementById('profile-box');
const usernameElement = document.getElementById('username');

let isProfileOpen = false;

function toggleProfile(event) {
  event.stopPropagation();
  isProfileOpen ? closeProfile() : openProfile();
}

function openProfile() {
  isProfileOpen = true;
  profileBox.style.display = 'block';
}

function closeProfile() {
  isProfileOpen = false;
  profileBox.style.display = 'none';
}

usernameElement.addEventListener('click', toggleProfile);

 

📌  영역 외 클릭 감지

바깥 영역을 클릭했을 때, 프로필이 오픈되어 있고, 프로필 박스 바깥에 타겟이 있는 경우 closeProfile 함수를 실행시킨다.
이 로직의 함수를 document 의 클릭 이벤트 리스너에 할당해준다.

function handleClickOutside(event) {
  if (isProfileOpen && !profileBox.contains(event.target)) {
    closeProfile();
  }
}
document.addEventListener('click', handleClickOutside);

 

contains() 메서드

contains() 메서드는 DOM 요소 내에 다른 요소가 포함되어 있는지 여부를 확인하는 메서드이다.
이 메서드는 부모 요소에 대해 호출되며, 전달된 매개변수가 부모 요소의 자식인지 여부를 검사한다.

const parentElement = document.getElementById('parent');
const childElement = document.getElementById('child');

const isChildInParent = parentElement.contains(childElement);

console.log(isChildInParent); // true

 

📌  전체 코드

const profileBox = document.getElementById('profile-box');
const usernameElement = document.getElementById('username');

let isProfileOpen = false;

function toggleProfile(event) {
  event.stopPropagation();
  isProfileOpen ? closeProfile() : openProfile();
}

function openProfile() {
  isProfileOpen = true;
  profileBox.style.display = 'block';
}

function closeProfile() {
  isProfileOpen = false;
  profileBox.style.display = 'none';
}

usernameElement.addEventListener('click', toggleProfile);

function handleClickOutside(event) {
  if (isProfileOpen && !profileBox.contains(event.target)) {
    closeProfile();
  }
}
document.addEventListener('click', handleClickOutside);

 

📌  React 로 영역 외 클릭 시 감지 기능 구현하기

프로필 박스 useState 정의하기

const [isProfileOpen, setIsProfileOpen] = useState(false);

 

useRef

useRef 는 값을 저장하는 역할을 한다.
프로필 박스 DOM 노드에 대한 참조를 저장한다.
useRef 를 이용하면 클릭된 대상이 프로필 박스인지 아닌지를 판단할 수 있게 되는 것이다.

Javascript
const profileRef = useRef(null)

 

Typescript
const profileRef = useRef<HTMLDivElement>(null)

 

이벤트 리스너 설정

마우스를 클릭했을 때 호출 가능한 함수를 정의해준다.
클릭된 대상이 프로필 박스 외부라면 프로필 박스를 닫는다.

Javascript
function handleClickOutside(e) {
  const currentProfileRef = profileRef.current
  if (currentProfileRef && !currentProfileRef.contains(e.target)) {
    setIsProfileOpen(false)
  }
}

 

Typescript
function handleClickOutside(e: MouseEvent): void {
  const currentProfileRef = profileRef.current
  if (currentProfileRef && !currentProfileRef.contains(e.target as Node)) {
    setIsProfileOpen(false)
  }
}

 

useEffect 사용

useEffect 를 이용해 profileRef 의 current 값이 변경될 때마다 해당 로직을 실행하도록 구성해준다.
profileRef.current를 의존성 배열에 넣어 프로필 박스 DOM 노드가 변경될 때만 이벤트 리스너를 재설정하게 한다.

useEffect(() => {
  document.addEventListener('click', handleClickOutside)
  return () => {
    document.removeEventListener("click", handleClickOutside)
  }
}, [profileRef.current])

 
removeEventListener를 사용하여 이전에 추가한 이벤트 리스너를 제거해준다.
클린업 함수로 mousedown 이벤트 리스너를 제거하여 불필요한 메모리 누수와 이벤트 리스너의 중첩을 방지하는 것.
 
 


참고 : chat gpt + me

728x90
반응형