๐Ÿ›  ๋ชจ๋‹ฌ ํด๋ฆญํ•˜๋ฉด ๋‹ซํ˜€๋ฒ„๋ฆฌ๋Š” ์ด์Šˆ ํ•ด๊ฒฐ๊ธฐ (React + Zustand + Portal ๊ธฐ๋ฐ˜)

oversleepยท2025๋…„ 6์›” 6์ผ

web-development

๋ชฉ๋ก ๋ณด๊ธฐ
21/23

๐Ÿ“Œ ๋ฌธ์ œ ์ƒํ™ฉ

ํšŒ์›ํƒˆํ‡ด ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ, ๋ชจ๋‹ฌ์ด ์—ด๋ฆฌ์ž๋งˆ์ž ๊ณง๋ฐ”๋กœ ๋‹ซํ˜€๋ฒ„๋ฆฌ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.
์‹ฌ์ง€์–ด Modal ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ํด๋ฆญํ•œ ๊ฑด๋ฐ๋„ onClose()๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉฐ ์‚ฌ๋ผ์กŒ๋‹ค.


๐Ÿ” ์›์ธ ๋ถ„์„

๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ ์ฝ”๋“œ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋กœ์ง์ด ์žˆ์—ˆ๋‹ค:

document.addEventListener('click', handleClick, true);

handleClick์€ ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ–ˆ๋‹ค:

const handleClick = (e: MouseEvent) => {
  if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
    onClose();
  }
};

์ฆ‰, ๋ชจ๋‹ฌ ๋‚ด๋ถ€๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ๋ฌด์กฐ๊ฑด ๋‹ซ์•„๋ฒ„๋ฆฌ๋Š” ๊ตฌ์กฐ์˜€๋Š”๋ฐโ€ฆ

๐Ÿ”ป ๋ฌธ์ œ๋Š” ์—ฌ๊ธฐ ์žˆ์—ˆ๋‹ค:

  • ๋ชจ๋‹ฌ ๊ตฌ์กฐ๊ฐ€ ๋ณต์žกํ•˜๊ฑฐ๋‚˜,
  • ๋‚ด๋ถ€์˜ ๋ฒ„ํŠผ, span, input ๋“ฑ์ด modalRef.current ๋ฐ”๊นฅ์œผ๋กœ ์ธ์‹๋˜๋Š” ๊ฒฝ์šฐ

โ†’ ๋‚ด๋ถ€ ํด๋ฆญ์ด์–ด๋„ "์™ธ๋ถ€ ํด๋ฆญ"์œผ๋กœ ์˜คํ•ดํ•˜๊ณ  ๋‹ซ์•„๋ฒ„๋ฆผ


๐Ÿงช ์‹œ๋„ํ–ˆ๋˜ ๊ฒƒ๋“ค

  • console.log(e.target) ์ฐ์–ด์„œ ๋ญ ๋ˆŒ๋ €๋Š”์ง€ ํ™•์ธ
  • modalRef.current.contains(e.target)์—์„œ false ๋‚˜์˜ค๋Š” ์š”์†Œ๋“ค ๋ถ„์„

๊ฒฐ๊ตญ, ๋ฒ„ํŠผ์ด๋‚˜ ํ…์ŠคํŠธ๋„ ref๊ฐ€ ์ฐธ์กฐํ•œ ์—˜๋ฆฌ๋จผํŠธ ์•ˆ์— ์—†์–ด ๋ณด์ด๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑธ ํ™•์ธํ•จ.


โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. ๋ชจ๋‹ฌ ์ „์ฒด ์˜์—ญ์„ ๊ฐ์‹ธ๋Š” div์— ์ปค์Šคํ…€ ์†์„ฑ ๋ถ€์—ฌ:
<div ref={modalRef} data-ignore-outside-click className="...">
  1. handleClick()์—์„œ ์ด ์†์„ฑ์ด ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌ:
const handleClick = (e: MouseEvent) => {
  const target = e.target as HTMLElement;

  if (target.closest('[data-ignore-outside-click]')) return;

  if (modalRef.current && !modalRef.current.contains(target)) {
    onClose();
  }
};

๐Ÿ” ์ด๋กœ์จ data-ignore-outside-click ์†์„ฑ์ด ๋ชจ๋‹ฌ ๋‚ด๋ถ€์ž„์„ ๋ช…์‹œํ•˜๋Š” ๋ฐฉํŒจ ์—ญํ• ์„ ํ•˜๊ฒŒ ๋จ.


๐Ÿ’ก ๊ตํ›ˆ

  • useRef().current.contains(e.target)๋งŒ์œผ๋กœ๋Š” ๋ชจ๋“  ๋‚ด๋ถ€ ํด๋ฆญ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด์žฅํ•˜์ง€ ๋ชปํ•œ๋‹ค.
  • closest('[data-ignore-outside-click]') ๊ฐ™์€ ์ปค์Šคํ…€ ์†์„ฑ์œผ๋กœ ํด๋ฆญ ํ•„ํ„ฐ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ฑธ์–ด์ฃผ๋Š” ๊ฒŒ ๋” ์•ˆ์ •์ .
  • ๋ชจ๋‹ฌ ๋‚ด๋ถ€๊ฐ€ ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๊ฑฐ๋‚˜, ํฌํƒˆ ๊ตฌ์กฐ์—์„œ ๋ Œ๋”๋ง ๊ณ„์ธต์ด ๊ผฌ์ด๋ฉด ์ด๋Ÿฐ ๋ฒ„๊ทธ๊ฐ€ ์ž์ฃผ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿงฉ ๋ณด๋„ˆ์Šค ํŒ

React์—์„œ document.addEventListener(..., true)์ฒ˜๋Ÿผ ์บก์ฒ˜๋ง ๋‹จ๊ณ„๋กœ ์ด๋ฒคํŠธ๋ฅผ ์žก๋Š” ๊ฑด ๋ชจ๋‹ฌ ์™ธ๋ถ€ ํด๋ฆญ ๊ฐ์ง€์—๋Š” ํšจ๊ณผ์ ์ด์ง€๋งŒ,
์šฐ์„ ์ˆœ์œ„ ์ถฉ๋Œ์ด๋‚˜ ๋ฒ„๋ธ”๋ง ๋ฐฉํ•ด ๋ฌธ์ œ๋„ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์‹ ์ค‘ํžˆ ์จ์•ผ ํ•œ๋‹ค.

profile
๊ถ๊ธˆํ•œ ๊ฒƒ, ํ–ˆ๋˜ ๊ฒƒ, ์‹œํ–‰์ฐฉ์˜ค ๊ทธ๋ฆฌ๊ณ  ๊ธฐ์–ตํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ๋“ค์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€