TIL

TimePicker ์™ธ๋ถ€ ํด๋ฆญ ์‹œ ๋‹ค์‹œ ์—ด๋ฆฌ๋Š” ํ˜„์ƒ

inz1234 2025. 3. 22. 02:48

๐Ÿšจ  TroubleShotting

๋ฌธ์ œ ๋ฐœ์ƒ์˜ ๋ฐฐ๊ฒฝ - As Is

ํˆฌํ‘œ ์‹œ๊ฐ„์„ ์„ค์ •ํ•˜๋Š” ํƒ€์ด๋จธ์—์„œ Input์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์‹œ๊ฐ„ ์„ ํƒ์ฐฝ(TimePicker)์ด ์—ด๋ฆฌ๋„๋ก ํ•˜๊ณ ,
์—ด๋ ค ์žˆ๋Š” ์ƒํƒœ์—์„œ ์™ธ๋ถ€๋ฅผ ํด๋ฆญํ•˜๋ฉด TimePicker๊ฐ€ ๋‹ซํžˆ๋„๋ก ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.
์ด ๊ณผ์ •์—์„œ ๋‘ ๊ฐ€์ง€ ํฐ ์ด์Šˆ๋ฅผ ๊ฒช์—ˆ๋‹ค:


โ—๏ธ ๋ฌธ์ œ

[true, false] ์ƒํƒœ์—์„œ Input์„ ํด๋ฆญํ•˜๋ฉด handleClickOutside๊ฐ€ ์‹คํ–‰๋˜์–ด [false, false]๋กœ ๋‹ซํ˜”๋‹ค๊ฐ€,
๊ณง์ด์–ด ๋‹ค์‹œ [true, false]๋กœ ๋‹ค์‹œ ์—ด๋ฆฌ๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•จ.

# 1) ์ด ์ƒํƒœ์—์„œ [false, false]๋กœ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ ์ž ํ•จ
timeOpen => [true, false]

# 2) ๊ทธ๋Ÿฐ๋ฐ [false, false]๊ฐ€ ๋˜์ž๋งˆ์ž ๊ณง๋ฐ”๋กœ ๋‹ค์‹œ [true, false]๊ฐ€ ๋จ
timeOpen => [false, false]
timeOpen => [true, false]
// ๋‹น์‹œ ์ฝ”๋“œ
useEffect(() => {
    if (timeRef && timeRef.current) {
      const handleClickOutside = (e: MouseEvent) => {
        if (timeOpen.some((t) => t) && timeRef.current.some((ref) => ref && !ref.contains(e.target as Node))) {
          setTimeOpen(Array(2).fill(false));
        }
      };
      document.addEventListener("mousedown", handleClickOutside);
      return () => {
        document.removeEventListener("mousedown", handleClickOutside);
      };
    }
  }, []);
  
  ... 
  
  <div style={{ display: "flex", alignItems: "center", gap: "5px", position: "relative" }}
   onClick={() => setTimeOpen((prev) => prev.map((p, i) => (i === idx ? true : p)))}
   ref={(el: any) => {timeRef.current[idx] = el;}}
   >

2. ์›์ธ ๋ถ„์„

โœ… ์›์ธ 1. useEffect์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์ด ๋นˆ๋ฐฐ์—ด๋กœ ๋˜์–ด ์žˆ์–ด timeOpen ์ƒํƒœ๊ฐ€ ํด๋กœ์ €์— ๊ฐ‡ํž˜

  • useEffect์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์ด ๋นˆ๋ฐฐ์—ด์ด๋ฉด, ๋‚ด๋ถ€ ํ•จ์ˆ˜๋Š” ์™ธ๋ถ€์˜ ์ตœ์‹  state๋ฅผ ์ž๋™์œผ๋กœ ์ฐธ์กฐํ•˜์ง€ ์•Š๊ณ , ์ •์˜ ๋‹น์‹œ์˜ ๊ฐ’์„ ๊ธฐ์–ตํ•œ ํด๋กœ์ €์— ๊ฐ‡ํžˆ๊ฒŒ ๋œ๋‹ค.
  • ์ด๋กœ ์ธํ•ด ์ƒํƒœ๋Š” ๋ฐ”๋€Œ์—ˆ์ง€๋งŒ handleClickOutside์—์„œ๋Š” ์—ฌ์ „ํžˆ ์ด์ „์˜ timeOpen ๊ฐ’์„ ์ฐธ์กฐํ•˜๊ณ  ์žˆ์—ˆ์Œ

โœ… ์›์ธ 2. ์ด๋ฒคํŠธ ์‹คํ–‰ ์ˆœ์„œ์— ๋Œ€ํ•œ ์ดํ•ด ๋ถ€์กฑ

  1. ์‚ฌ์šฉ์ž๊ฐ€ Input์„ ํด๋ฆญ
  2. onClick → setTimeOpen(idx) ์‹คํ–‰ → ๐Ÿ” ๋ Œ๋”๋ง ์˜ˆ์•ฝ
  3. ๋™์‹œ์— document.mousedown ์ด๋ฒคํŠธ ๋ฐœ์ƒ → handleClickOutside ์‹คํ–‰๋จ
  4. ๊ทธ๋ž˜์„œ setTimeOpen([false, false]) ์‹คํ–‰๋จ
  5. ํ•˜์ง€๋งŒ ์ด ์‹œ์  ์ดํ›„์— ์›๋ž˜์˜ onClick์— ์˜ํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ์ด ๋‹ค์‹œ ๋ฐ˜์˜๋จ → ๋‹ค์‹œ true๋กœ ๋˜๋Œ์•„๊ฐ
  • onClick์€ ๋ฆฌ์•กํŠธ๊ฐ€ ์ž์ฒด์ ์œผ๋กœ mousedown + mouseup ๋‘˜ ๋‹ค ๋ฐœ์ƒํ•œ ํ›„ ๋งŒ๋“ค์–ด๋‚ธ ํ•ฉ์„ฑ ์ด๋ฒคํŠธ(synthetic event) ์ด๋ฏ€๋กœ mousedown์ด ํ•ญ์ƒ ๋จผ์ € ์ผ์–ด๋‚จ

2. ํ•ด๊ฒฐ๋ฐฉ์•ˆ

๐Ÿ”ง  ๋ฐฉ๋ฒ• 1. stopPropagation()

  • Input ์ƒ๋‹จ div์— onMouseDown={(e) => e.stopPropagation()} ์ถ”๊ฐ€
  • document๊นŒ์ง€ ์ด๋ฒคํŠธ๊ฐ€ ์ „ํŒŒ๋˜์ง€ ์•Š์•„ handleClickOutside๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Œ
  • ๊ฒฐ๊ณผ: ๋‹ซํžˆ์ง€ ์•Š์Œ → ์ƒํƒœ ์œ ์ง€๋จ ([true, false])

๐Ÿ”ง  ๋ฐฉ๋ฒ• 2. setTimeout(() => ..., 0)

  • mousedown ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์ธ handleClickOutside ํ•จ์ˆ˜ ๋‚ด๋ถ€ ๋กœ์ง์„ ํ•œ ๋ฐ•์ž ๋Šฆ๊ฒŒ ์‹คํ–‰๋˜๋„๋ก defer ์ฒ˜๋ฆฌ
  • onClick์ด ๋จผ์ € ์ผ์–ด๋‚˜๋„๋ก ๋ณ€๊ฒฝํ•ด์ฃผ์—ˆ์–ด
  • ๊ฒฐ๊ณผ: ๋‹ซํžˆ๋Š” ๋™์ž‘ ์„ฑ๊ณต ([false, false])
  useEffect(() => {
    if (timeRef && timeRef.current) {
      const handleClickOutside = (e: MouseEvent) => {
        setTimeout(() => {
          if (timeOpen.some((t) => t) && timeRef.current.some((ref) => ref && !ref.contains(e.target as Node))) {
            setTimeOpen(Array(2).fill(false));
          }
        }, 0);
      };
      document.addEventListener("mousedown", handleClickOutside);
      return () => {
        document.removeEventListener("mousedown", handleClickOutside);
      };
    }
  }, [timeOpen]);


๐Ÿ’ก ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ๋œ ์ 

๐Ÿง  ํด๋กœ์ € + useEffect

  • useEffect(() => {...}, [])๋Š” ๋‚ด๋ถ€ ํ•จ์ˆ˜๊ฐ€ ์ตœ์ดˆ ๋ Œ๋”๋ง ๋‹น์‹œ์˜ ์ƒํƒœ๋งŒ ๊ธฐ์–ตํ•จ
  • ์ตœ์‹  ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์˜์กด์„ฑ ๋ฐฐ์—ด ๋˜๋Š” useRef๋กœ ์ƒํƒœ ์ถ”์  ํ•„์š”

๐Ÿ”„  DOM ์ด๋ฒคํŠธ ์ „ํŒŒ ์ˆœ์„œ

1 ์บก์ฒ˜๋ง ๋ถ€๋ชจ → ์ž์‹, document์˜ addEventListener(..., 3rd์ธ์ž true)๊ฐ€ ์žˆ์œผ๋ฉด ์‹คํ–‰๋จ
2 ํƒ€๊ฒŸ ํด๋ฆญํ•œ ์š”์†Œ ์ž์ฒด์—์„œ ์‹คํ–‰๋จ (์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์žˆ๋‹ค๋ฉด)
3 ๋ฒ„๋ธ”๋ง ์ž์‹ → ๋ถ€๋ชจ, document.addEventListener(..., false) ์‹คํ–‰๋จ → ์—ฌ๊ธฐ์„œ handleClickOutside
4 ๋ฆฌ์•กํŠธ ํ•ฉ์„ฑ ์ด๋ฒคํŠธ ๋ฆฌ์•กํŠธ์˜ ํ•ฉ์„ฑ ์ด๋ฒคํŠธ , onClick={() => ...} ์‹คํ–‰   ์—ฌ๊ธฐ์„œ setTimeOpen true๋กœ
  • ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” ๋ฒ„๋ธ”๋ง ๋‹จ๊ณ„์—์„œ ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋œ๋‹ค. true๋ฅผ 3rd์ธ์ž๋กœ ๋„˜๊ธฐ์ง€ ์•Š๋Š”ํ•œ!

๐Ÿงฉ React ํ•ฉ์„ฑ ์ด๋ฒคํŠธ (onClick)

  • DOM์˜ mousedown → mouseup ์ด๋ฒคํŠธ ํ๋ฆ„์ด ๋๋‚œ ๋’ค ๋ฐœ์ƒ
  • React ๋‚ด๋ถ€์—์„œ ์ถ”์ƒํ™”๋œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ด๋ฏ€๋กœ ๋ฒ„๋ธ”๋ง๋ณด๋‹ค ๋Šฆ๊ฒŒ ์‹คํ–‰๋จ

๐Ÿ”์„ ํƒ์˜ ๊ธฐ์ค€

"๋‚˜๋Š” ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด์„œ timeOpen์ด false๊ฐ€ ๋˜๊ธธ ์›ํ•˜๋Š” ์ƒํ™ฉ์ด์—ˆ์ง€."

๋”ฐ๋ผ์„œ stopPropagation()์ฒ˜๋Ÿผ ์ด๋ฒคํŠธ ์ž์ฒด๋ฅผ ์ฐจ๋‹จํ•˜๋Š” ๋ฐฉ์‹์ด ์•„๋‹ˆ๋ผ,
์‹คํ–‰ ์ˆœ์„œ๋ฅผ ๋ฐ”๊พธ๋Š” setTimeout()์ด ๋” ์ ํ•ฉํ•œ ํ•ด๊ฒฐ์ฑ…์ด์—ˆ์Œ


๐Ÿšฉ๋งˆ๋ฌด๋ฆฌ

์ด๋ฒˆ ์ด์Šˆ๋Š” ๋‹จ์ˆœํžˆ "์™ธ๋ถ€ ํด๋ฆญ ๊ฐ์ง€" ๊ธฐ๋Šฅ์ฒ˜๋Ÿผ ๋ณด์˜€์ง€๋งŒ,
๊ทธ ์†์—๋Š”  ํด๋กœ์ €์™€ useEffect์˜ ๊ด€๊ณ„, React์˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹, DOM์˜ ์ด๋ฒคํŠธ ์ „ํŒŒ ํ๋ฆ„,
๊ทธ๋ฆฌ๊ณ  stopPropagation๊ณผ setTimeout์˜ ์˜๋„๋œ ํƒ€์ด๋ฐ ์ œ์–ด ์ฐจ์ด๊นŒ์ง€ ์ค‘์š”ํ•œ ๊ฐœ๋…์ด ์ˆจ์–ด ์žˆ์—ˆ๋‹ค.

์ด์ œ๋Š” ์–ด๋–ค ์ƒํ™ฉ์— stopPropagation()์„ ์จ์•ผ ํ•˜๊ณ , ์–ด๋–ค ์ƒํ™ฉ์— setTimeout()์ด ๋” ์ ํ•ฉํ•œ์ง€๋„ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค.
๋˜ํ•œ, ์™ธ๋ถ€ ํด๋ฆญ ๊ฐ์ง€ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ๋•Œ ์–ด๋–ค ์‹œ์ ์— ์ƒํƒœ๊ฐ€ ๋ฐ˜์˜๋˜๋Š”์ง€๋„ ๋” ๋ช…ํ™•ํžˆ ์ดํ•ดํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.