์ฃผ์˜ํ•˜์„ธ์š”!

useLayoutEffect๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด useEffect๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

useLayoutEffect๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ์‹คํ–‰๋˜๋Š” useEffect์ž…๋‹ˆ๋‹ค.

useLayoutEffect(setup, dependencies?)

๋ ˆํผ๋Ÿฐ์Šค

useLayoutEffect(setup, dependencies?)

useLayoutEffect๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ๋ ˆ์ด์•„์›ƒ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

import { useState, useRef, useLayoutEffect } from 'react';

function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);

useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// ...

์•„๋ž˜์—์„œ ๋” ๋งŽ์€ ์˜ˆ์‹œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

๋งค๊ฐœ๋ณ€์ˆ˜

  • setup: Effect์˜ ๋กœ์ง์ด ํฌํ•จ๋œ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. setup ํ•จ์ˆ˜๋Š” ์„ ํƒ์ ์œผ๋กœ cleanup ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์— ์ถ”๊ฐ€๋˜๊ธฐ ์ „์— React๋Š” setup ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. dependencies๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด ๋‹ค์‹œ ๋ Œ๋”๋ง ๋  ๋•Œ๋งˆ๋‹ค, React๋Š” (cleanup ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ–ˆ๋‹ค๋ฉด) ๋จผ์ € ์ด์ „ ๊ฐ’์œผ๋กœ cleanup ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ ๋‹ค์Œ, ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ setup ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์—์„œ ์ œ๊ฑฐ๋˜๊ธฐ ์ „์— React๋Š” cleanup ํ•จ์ˆ˜๋ฅผ ํ•œ ๋ฒˆ ๋” ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  • ์„ ํƒ์‚ฌํ•ญ dependencies: setup์ฝ”๋“œ ๋‚ด์—์„œ ์ฐธ์กฐ๋œ ๋ชจ๋“  ๋ฐ˜์‘ํ˜• ๊ฐ’์˜ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. ๋ฐ˜์‘ํ˜• ๊ฐ’์—๋Š” props, state, ๊ทธ๋ฆฌ๊ณ  ์ปดํฌ๋„ŒํŠธ ๋ณธ๋ฌธ์— ์ง์ ‘ ์„ ์–ธ๋œ ๋ชจ๋“  ๋ณ€์ˆ˜์™€ ํ•จ์ˆ˜๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. linter๊ฐ€ React์šฉ์œผ๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ, ๋ชจ๋“  ๋ฐ˜์‘ํ˜• ๊ฐ’์ด ์˜์กด์„ฑ์œผ๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ง€์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์˜์กด์„ฑ ๋ชฉ๋ก์—๋Š” ์ผ์ •ํ•œ ์ˆ˜์˜ ํ•ญ๋ชฉ์ด ์žˆ์–ด์•ผ ํ•˜๋ฉฐ [dep1, dep2, dep3]์™€ ๊ฐ™์ด ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. React๋Š” Object.is ๋น„๊ต ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ์˜์กด์„ฑ์„ ์ด์ „ ๊ฐ’๊ณผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. ์˜์กด์„ฑ์„ ์ „ํ˜€ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค์‹œ ๋ Œ๋”๋งํ•  ๋•Œ๋งˆ๋‹ค Effect๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’

useLayoutEffect๋Š” undefined๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์˜์‚ฌํ•ญ

  • useLayoutEffect๋Š” Hook์ด๋ฏ€๋กœ, ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„ ๋ ˆ๋ฒจ ๋˜๋Š” ์ปค์Šคํ…€ Hook์—์„œ๋งŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ณต๋ฌธ์ด๋‚˜ ์กฐ๊ฑด๋ฌธ ๋‚ด์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด ์ž‘์—…์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ•ด์„œ Effect๋ฅผ ์ƒˆ ์ปดํฌ๋„ŒํŠธ๋กœ ์˜ฎ๊ธฐ์„ธ์š”.

  • Strict Mode๊ฐ€ ์ผœ์ ธ ์žˆ์œผ๋ฉด, React๋Š” ์‹ค์ œ ์ฒซ ๋ฒˆ์งธ setup ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ด์ „์— ๊ฐœ๋ฐœ ๋ชจ๋“œ์—๋งŒ ํ•œ์ •ํ•˜์—ฌ ํ•œ ๋ฒˆ์˜ ์ถ”๊ฐ€์ ์ธ setup + cleanup ์‚ฌ์ดํด์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” cleanup ๋กœ์ง์ด setup ๋กœ์ง์„ ์™„๋ฒฝํžˆ โ€œ๋ฐ˜์˜โ€ํ•˜๊ณ , setup ๋กœ์ง์ด ์ˆ˜ํ–‰ํ•˜๋Š” ์ž‘์—…์„ ์ค‘๋‹จํ•˜๊ฑฐ๋‚˜ ๋˜๋Œ๋ฆฌ๋Š” ์ง€๋ฅผ ํ™•์ธํ•˜๋Š” ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด cleanup ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

  • ์˜์กด์„ฑ ์ค‘์— ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ์ •์˜๋œ ๊ฐ์ฒด๋‚˜ ํ•จ์ˆ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, Effect ๊ฐ€ ํ•„์š” ์ด์ƒ์œผ๋กœ ๋‹ค์‹œ ์‹คํ–‰๋  ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ๋ถˆํ•„์š”ํ•œ ๊ฐ์ฒด ์˜์กด์„ฑ์ด๋‚˜ ํ•จ์ˆ˜ ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜์„ธ์š”. State ์—…๋ฐ์ดํŠธ๋‚˜ ๋น„ ๋ฐ˜์‘ํ˜• ๋กœ์ง์„ effect ๋ฐ–์œผ๋กœ ๋นผ๋‚ผ ์ˆ˜ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

  • Effect๋Š” ํด๋ผ์ด์–ธํŠธ ํ™˜๊ฒฝ์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„ ๋ Œ๋”๋ง ์ค‘์—๋Š” ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • useLayoutEffect ๋‚ด๋ถ€์˜ ์ฝ”๋“œ์™€ ์ด๋กœ ์ธํ•œ ๋ชจ๋“  state ์—…๋ฐ์ดํŠธ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์„ ๋ง‰์Šต๋‹ˆ๋‹ค. ๊ณผ๋„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ์•ฑ์ด ๋Š๋ ค์ง‘๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•˜๋ฉด useEffect๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.


์‚ฌ์šฉ๋ฒ•

๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ๋ ˆ์ด์•„์›ƒ ๊ณ„์‚ฐํ•˜๊ธฐ

๋Œ€๋ถ€๋ถ„์˜ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ Œ๋”๋ง์„ ์œ„ํ•ด ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์˜ ํ™”๋ฉด์ƒ ์œ„์น˜์™€ ํฌ๊ธฐ๋ฅผ ์•Œ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๊ฐ€ JSX๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ปดํฌ๋„ŒํŠธ์˜ ๋ ˆ์ด์•„์›ƒ(์œ„์น˜์™€ ํฌ๊ธฐ)๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.

๊ฐ€๋”์€ ์ด๊ฒƒ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•œ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋งˆ์šฐ์Šค ์ปค์„œ๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ํˆดํŒ์ด ์š”์†Œ ์˜†์— ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์ƒ๊ฐํ•ด ๋ณด์„ธ์š”. ์ถฉ๋ถ„ํ•œ ๊ณต๊ฐ„์ด ์žˆ๋‹ค๋ฉด ํˆดํŒ์€ ์š”์†Œ ์œ„์— ๋‚˜ํƒ€๋‚˜๊ฒ ์ง€๋งŒ, ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•˜๋‹ค๋ฉด ์•„๋ž˜์— ๋‚˜ํƒ€๋‚˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ ํˆดํŒ์„ ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜์— ๋ Œ๋”๋งํ•˜๋ ค๋ฉด ํˆดํŒ์˜ ๋†’์ด๋ฅผ ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์œ„์ชฝ ๊ณต๊ฐ„์— ๋“ค์–ด๊ฐ€๋Š”์ง€ ํŒ๋‹จ ํ•ด์•ผ ํ•จ)

์ด๋ฅผ ์œ„ํ•ด ๋‘ ๋ฒˆ์˜ ๋ Œ๋”๋ง์„ ๊ฑฐ์ณ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  1. ํˆดํŒ์„ (์ž˜๋ชป๋œ ์œ„์น˜๋ผ๋„) ์•„๋ฌด ์œ„์น˜์— ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค
  2. ํˆดํŒ์˜ ๋†’์ด๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ํˆดํŒ์„ ๋ฐฐ์น˜ํ•  ์œ„์น˜๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
  3. ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜์— ํˆดํŒ์„ ๋‹ค์‹œ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

์ด ์ž‘์—…์€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ๋ชจ๋‘ ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํˆดํŒ์ด ์›€์ง์ด๋Š” ๊ฑธ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์ด๊ณ  ์‹ถ์ง€ ์•Š์œผ๋‹ˆ๊นŒ์š”. useLayoutEffect๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ๋ ˆ์ด์•„์›ƒ์„ ๊ณ„์‚ฐํ•˜์„ธ์š”.

function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // ์•„์ง ์‹ค์ œ ๋†’์ด๋ฅผ ๋ชจ๋ฆ…๋‹ˆ๋‹ค.

useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // ์‹ค์ œ ๋†’์ด๋ฅผ ์•Œ์•˜์œผ๋‹ˆ ๋‹ค์‹œ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
}, []);

// ...์•„๋ž˜์— ์˜ฌ ๋ Œ๋”๋ง ๋กœ์ง์—์„œ tooltipHeight๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”...
}

์ž‘๋™ ๋ฐฉ์‹์„ ๋‹จ๊ณ„๋ณ„๋กœ ์•Œ์•„๋ด…์‹œ๋‹ค.

  1. Tooltip ์€ ์ดˆ๊ธฐํ™”๋œ ๊ฐ’์ธ tooltipHeight = 0์œผ๋กœ ๋ Œ๋”๋ง ๋ฉ๋‹ˆ๋‹ค (๋”ฐ๋ผ์„œ ํˆดํŒ์˜ ์œ„์น˜๋Š” ์ž˜๋ชป๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค).
  2. React๊ฐ€ ์ด ํˆดํŒ์„ DOM์— ๋ฐฐ์น˜ํ•˜๊ณ  useLayoutEffect ์•ˆ์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  3. useLayoutEffect๊ฐ€ ํˆดํŒ์˜ ๋†’์ด๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ๋ฐ”๋กœ ๋‹ค์‹œ ๋ Œ๋”๋ง์‹œํ‚ต๋‹ˆ๋‹ค.
  4. Tooltip ์ด ์‹ค์ œ tooltipHeight๋กœ ๋ Œ๋”๋ง ๋ฉ๋‹ˆ๋‹ค. (๋”ฐ๋ผ์„œ ํˆดํŒ์ด ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜์— ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค.)
  5. React๊ฐ€ DOM์—์„œ ์ด๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ๋งˆ์นจ๋‚ด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํˆดํŒ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜์˜ ๋ฒ„ํŠผ๋“ค ์œ„๋กœ ๋งˆ์šฐ์Šค ์ปค์„œ๋ฅผ ์˜ฌ๋ ค์„œ ํˆดํŒ์ด ๊ณต๊ฐ„์— ๋“ค์–ด๊ฐ€๋Š”์ง€์— ๋”ฐ๋ผ ์œ„์น˜๋ฅผ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜์„ธ์š”.

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
    console.log('Measured tooltip height: ' + height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // ์œ„์ชฝ ๊ณต๊ฐ„์— ๋“ค์–ด๊ฐ€์ง€ ๋ชปํ•˜๋ฏ€๋กœ ์•„๋ž˜์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

Tooltip ์ปดํฌ๋„ŒํŠธ๋Š” ๋‘ ๋ฒˆ์˜ ๋ Œ๋”๋ง์„ ๊ฑฐ์น˜์ง€๋งŒ (์ฒ˜์Œ์€ 0์œผ๋กœ ์ดˆ๊ธฐํ™”๋œ tooltipHeight๋กœ ๋ Œ๋”๋ง ๋˜๊ณ , ๊ทธ๋‹ค์Œ ์‹ค์ œ๋กœ ๊ณ„์‚ฐ๋œ ๋†’์ด๋กœ ๋ Œ๋”๋ง ๋จ), ์‹ค์ œ๋กœ ๋ณด์ด๋Š” ๊ฑด ์ตœ์ข… ๊ฒฐ๊ณผ๋ฟ์ž…๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ์—์„œ useEffect ๋Œ€์‹  useLayoutEffect๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ ์ฐจ์ด์ ์„ ์ž์„ธํ•˜๊ฒŒ ์‚ดํŽด๋ด…์‹œ๋‹ค.

useLayoutEffect vs useEffect

์˜ˆ์ œ 1 of 2:
useLayoutEffect ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์„ ๋ง‰์Šต๋‹ˆ๋‹ค

React๋Š” useLayoutEffect ๋‚ด๋ถ€์˜ ์ฝ”๋“œ์™€ ์ด๋กœ ์ธํ•œ ๋ชจ๋“  state ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ์ฒ˜๋ฆฌ๋˜๋Š” ๊ฒƒ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ๋•๋ถ„์— ํˆดํŒ์„ ๋ Œ๋”๋งํ•˜๊ณ , ์œ„์น˜์™€ ํฌ๊ธฐ๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ๋‹ค์‹œ ๋ Œ๋”๋งํ•˜๋ฉด์„œ ์ฒซ ๋ฒˆ์งธ ๋ Œ๋”๋ง์€ ์œ ์ €๊ฐ€ ๋ชจ๋ฅด๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, useLayoutEffect๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์„ ๋ง‰์Šต๋‹ˆ๋‹ค.

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // ์œ„์ชฝ ๊ณต๊ฐ„์— ๋“ค์–ด๊ฐ€์ง€ ๋ชปํ•˜๋ฏ€๋กœ ์•„๋ž˜์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

๋‘ ๋ฒˆ์— ๊ฑธ์ณ์„œ ๋ Œ๋”๋งํ•˜๊ณ  ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋ง‰๋Š” ๊ฒƒ์€ ์„ฑ๋Šฅ์„ ์ €ํ•˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•˜๋ฉด ํ”ผํ•˜์„ธ์š”.


๋ฌธ์ œ ํ•ด๊ฒฐ

์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: โ€useLayoutEffect does nothing on the serverโ€

useLayoutEffect์˜ ๋ชฉ์ ์€ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  1. ์ดˆ๊ธฐ ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  2. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ™”๋ฉด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ๋ ˆ์ด์•„์›ƒ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
  3. ์ฝ์€ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ตœ์ข… ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

์„œ๋ฒ„ ๋ Œ๋”๋ง์„ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋ผ๋ฉด, React ์•ฑ์€ ์„œ๋ฒ„์—์„œ ์ดˆ๊ธฐ ๋ Œ๋”๋ง์„ ํ•ด์„œ HTML์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด JavaScript ์ฝ”๋“œ๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ์ „์— ์ดˆ๊ธฐ HTML์„ ๋ณด์—ฌ์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ฌธ์ œ๋Š” ์„œ๋ฒ„์—๋Š” ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์•ž์„  ์˜ˆ์ œ์—์„  Tooltip ์ปดํฌ๋„ŒํŠธ์—์„œ useLayoutEffect๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํˆดํŒ์„ ์ฝ˜ํ…์ธ ์˜ ๋†’์ด์— ๋”ฐ๋ผ (์ฝ˜ํ…์ธ ์˜ ์œ„์ชฝ๊ณผ ์•„๋ž˜์ชฝ ์ค‘) ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ์„œ๋ฒ„ HTML์˜ ์ผ๋ถ€๋กœ Tooltip์„ ๋ Œ๋”๋งํ•˜๋ ค ํ•˜๋ฉด, ์ด๋•Œ๋Š” ํˆดํŒ์˜ ์œ„์น˜๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฒฐ์ •ํ•  ์ˆ˜ ์—†์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ๋Š” ์•„์ง ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๊ฐ€ ์—†์œผ๋‹ˆ๊นŒ์š”! ๋”ฐ๋ผ์„œ ํˆดํŒ์„ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•˜๊ธฐ๋ฅผ ์›ํ•˜๋”๋ผ๋„, ํด๋ผ์ด์–ธํŠธ๋กœ ์˜ฎ๊ฒจ์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋กœ๋“œ๋˜๊ณ  ์‹คํ–‰๋œ ํ›„์— ๋ Œ๋”๋งํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด์— ์˜์กดํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ์–ด์ฐจํ”ผ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด, ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์ค‘์— Tooltip์ด ๋ณด์ด๋Š” ๊ฒƒ์€ ๋ง์ด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. Tooltip์€ ํด๋ผ์ด์–ธํŠธ ์ƒํ˜ธ์ž‘์šฉ์— ์˜ํ•ด์„œ ๋ณด์ด๋Š” ๊ฒƒ์ด๋‹ˆ๊นŒ์š”.

๊ทธ๋Ÿผ์—๋„ ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ๋งˆ์ฃผ์นœ๋‹ค๋ฉด ๋ช‡ ๊ฐ€์ง€ ๋‹ค๋ฅธ ์˜ต์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • useLayoutEffect๋ฅผ useEffect๋กœ ๋Œ€์ฒด ํ•˜์„ธ์š”. ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์„ ๋ง‰์ง€ ๋ง๊ณ  (์ดˆ๊ธฐ HTML์ด Effect ์‹คํ–‰ ์ „์— ๋ณด์ด๊ธฐ ๋•Œ๋ฌธ์—) ์ดˆ๊ธฐ ๋ Œ๋”๋ง์ด ๋ณด์ด๋”๋ผ๋„ ๊ดœ์ฐฎ๋‹ค๊ณ  React์—๊ฒŒ ๋งํ•ด์ฃผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • ๋˜๋Š” ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ „์šฉ์œผ๋กœ ๋งŒ๋“œ์„ธ์š”. React๊ฐ€ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด <Suspense> ๊ฒฝ๊ณ„ ์•ˆ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์„œ๋ฒ„๋ Œ๋”๋ง ๋™์•ˆ (์Šคํ”ผ๋„ˆ๋‚˜ ๊ธ€๋ฆฌ๋จธ๊ฐ™์€) loading fallbck์œผ๋กœ ๋Œ€์ฒด ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

  • ๋˜๋Š” useLayoutEffect๊ฐ€ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ hydration ์ดํ›„์—๋งŒ ๋ Œ๋”๋งํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถˆ๋ฆฌ์–ธ ํƒ€์ž…์ธ isMounted state๋ฅผ ์ดˆ๊นƒ๊ฐ’์ธ false๋กœ ์œ ์ง€ํ•˜๋‹ค๊ฐ€, useEffect ํ˜ธ์ถœ๋˜๋ฉด ๊ฑฐ๊ธฐ์„œ true๋กœ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜์„ธ์š”. ๊ทธ๋Ÿฌ๋ฉด ๋ Œ๋”๋ง ๋กœ์ง์€ return isMounted ? <RealContent /> : <FallbackContent /> ์ฒ˜๋Ÿผ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•˜๋Š” ์ค‘์ด๊ฑฐ๋‚˜ hydration ๋™์•ˆ ์œ ์ €๋Š” FallbackContent๋ฅผ ๋ณผ ๊ฒƒ์ด๊ณ  FallbackContent๋Š” useLayoutEffect๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ํ›„์— React๊ฐ€ FallbackContent๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ „์šฉ์ด๋ฉด์„œ useLayoutEffect๋ฅผ ํ˜ธ์ถœํ•˜๋Š” RealContent๋กœ ๋ณ€๊ฒฝํ•  ๊ฒ๋‹ˆ๋‹ค.

  • ์ปดํฌ๋„ŒํŠธ๋ฅผ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์™€ ๋™๊ธฐํ™”ํ•˜๊ณ , ๋ ˆ์ด์•„์›ƒ ๊ณ„์‚ฐ ์™ธ์— ๋‹ค๋ฅธ ์ด์œ ๋กœ useLayoutEffect์— ์˜์กดํ•˜๋Š” ๊ฒฝ์šฐ๋ผ๋ฉด, ๋Œ€์‹  useSyncExternalStore๋ฅผ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”. ์ด Hook์€ ์„œ๋ฒ„ ๋ Œ๋”๋ง์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.