๐ŸŒฑ <groot/>
์œ„์ ฏ 10๊ฐœ์—์„œ ํŽ˜์ด์ง€๊ฐ€ ๋ฒ„๋ฒ…์˜€๋‹ค โ€” ๊ฐ•์ œ reflow๋ฅผ ์œ ํœด ์‹œ๊ฐ„์œผ๋กœ ๋ฐ€์–ด๋‚ด 95% ๊ฐœ์„ ํ•œ ์ด์•ผ๊ธฐ | Groot's Tech Blog
๐ŸŒWeb

์œ„์ ฏ 10๊ฐœ์—์„œ ํŽ˜์ด์ง€๊ฐ€ ๋ฒ„๋ฒ…์˜€๋‹ค โ€” ๊ฐ•์ œ reflow๋ฅผ ์œ ํœด ์‹œ๊ฐ„์œผ๋กœ ๋ฐ€์–ด๋‚ด 95% ๊ฐœ์„ ํ•œ ์ด์•ผ๊ธฐ

2026.07.02ยท3๋ถ„ ์ฝ๊ธฐ

์‹œ์„ธ ์œ„์ ฏ ํ™•์žฅ์—๋Š” ์ž‘์ง€๋งŒ ์š•์‹ฌ๋‚ธ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค. ์œ„์ ฏ ํ…์ŠคํŠธ ์ƒ‰์„ ๋ฐฐ๊ฒฝ์— ๋งž์ถฐ ์ž๋™์œผ๋กœ ์กฐ์ •ํ•˜๋Š” ๊ฒƒ. ํฐ ํŽ˜์ด์ง€ ์œ„์—์„  ๊ฒ€์€ ๊ธ€์”จ, ์–ด๋‘์šด ํŽ˜์ด์ง€ ์œ„์—์„  ํฐ ๊ธ€์”จ. ๊ทธ๋Ÿฐ๋ฐ ์ด ์˜ˆ์œ ๊ธฐ๋Šฅ์ด ์„ฑ๋Šฅ์„ ๋ฌด๋„ˆ๋œจ๋ ธ๋‹ค.

์ฆ์ƒ: ์œ„์ ฏ์„ ์—ฌ๋Ÿฌ ๊ฐœ ๋„์šฐ๋ฉด ํŽ˜์ด์ง€๊ฐ€ ๋А๋ ค์ง„๋‹ค

์œ„์ ฏ์„ 10๊ฐœ์ฏค ๋„์šฐ๋‹ˆ, ๋‚ด๊ฐ€ ๋ณด๊ณ  ์žˆ๋˜ ํŽ˜์ด์ง€ ์ž์ฒด์˜ ๋ฐ˜์‘์„ฑ์ด ๊ธ‰๊ฒฉํžˆ ๋‚˜๋น ์กŒ๋‹ค. ์Šคํฌ๋กค๋„, ํด๋ฆญ๋„ ๊ตผ๋–ด๋‹ค. ์ง€ํ‘œ๋กœ๋Š” INP(Interaction to Next Paint)๊ฐ€ ํฌ๊ฒŒ ํŠ€์—ˆ๋‹ค.

Chrome DevTools Performance ํƒญ์„ ์—ด์–ด๋ณด๋‹ˆ ๋ฒ”์ธ์ด ๋ช…ํ™•ํ–ˆ๋‹ค. getBackgroundColorAtPoint๋ผ๋Š” ํ•จ์ˆ˜๊ฐ€ **์ „์ฒด ์‹คํ–‰ ์‹œ๊ฐ„์˜ 65~71%**๋ฅผ ๋จน๊ณ  ์žˆ์—ˆ๋‹ค.

์›์ธ: elementsFromPoint๋Š” ๊ณต์งœ๊ฐ€ ์•„๋‹ˆ๋‹ค

๋ฐฐ๊ฒฝ์ƒ‰์„ ์•Œ์•„๋‚ด๋ ค๊ณ  document.elementsFromPoint(x, y)๋ฅผ ์ผ๋‹ค. "์ด ์ขŒํ‘œ์— ์–ด๋–ค ์š”์†Œ๋“ค์ด ์Œ“์—ฌ ์žˆ๋‚˜"๋ฅผ ์•Œ๋ ค์ฃผ๋Š” API๋‹ค. ์ด๋Ÿฐ ๋ ˆ์ด์•„์›ƒ์„ ์ฝ๋Š” API๋Š” ๋ ˆ์ด์•„์›ƒ์ด dirty ์ƒํƒœ์ผ ๋•Œ ํ˜ธ์ถœํ•˜๋ฉด ๊ฐ•์ œ ์žฌ๊ณ„์‚ฐ(reflow) ์„ ์œ ๋ฐœํ•œ๋‹ค. ํ˜ธ์ถœ ์ž์ฒด๊ฐ€ ํ•ญ์ƒ ๋น„์‹ผ ๊ฑด ์•„๋‹ˆ๊ณ , "๊ทธ ์‚ฌ์ด์— ๋ญ”๊ฐ€ ์Šคํƒ€์ผ์„ ๊ฑด๋“œ๋ ธ๋‹ค๋ฉด" ๋น„์‹ธ์ง„๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋‚ด ์ฝ”๋“œ๊ฐ€ ์ •ํ™•ํžˆ ๊ทธ ์ง“์„ ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ์ƒ˜ํ”Œ๋งํ•  ๋•Œ ์œ„์ ฏ ์ž์‹ ์ด ์ฐํžˆ์ง€ ์•Š๋„๋ก, ์ƒ˜ํ”Œ ์‚ฌ์ด์‚ฌ์ด์— ์˜ค๋ฒ„๋ ˆ์ด ๋ฃจํŠธ์˜ pointer-events๋ฅผ ๊ป๋‹ค ์ผฐ๋‹ค ํ–ˆ๋˜ ๊ฒƒ.

const previousPointerEvents = root.style.pointerEvents;
root.style.pointerEvents = "none";        // โ† ์Šคํƒ€์ผ ๋ณ€๊ฒฝ = ๋ ˆ์ด์•„์›ƒ invalidate
const sampled = samplePoints.map(({ point }) =>
  getBackgroundColorAtPoint(point.x, point.y) // โ† ๋‹ค์‹œ ๋ ˆ์ด์•„์›ƒ์„ ์ฝ์Œ = ๊ฐ•์ œ reflow
);
root.style.pointerEvents = previousPointerEvents; // โ† ๋˜ invalidate

"์Šคํƒ€์ผ ์“ฐ๊ธฐ โ†’ ๋ ˆ์ด์•„์›ƒ ์ฝ๊ธฐ"๋ฅผ ๋ฐ˜๋ณตํ•˜๋‹ˆ, elementsFromPoint๊ฐ€ ์บ์‹œ๋œ ๋ ˆ์ด์•„์›ƒ์„ ์žฌ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋งค๋ฒˆ ์žฌ๊ณ„์‚ฐํ•˜๊ฒŒ ๋๋‹ค.

ํ˜ธ์ถœ ๊ตฌ์กฐ๊ฐ€ ์ด๋žฌ๋‹ค.

renderWidgets()  [RAF โ€” ํŽ˜์ธํŠธ ์ง์ „์— ์‹คํ–‰]
  โ”” getCachedTheme() ร— ์œ„์ ฏ N๊ฐœ
      โ”” sampleTheme()
          โ”” getBackgroundColorAtPoint() ร— 9 ์ƒ˜ํ”Œํฌ์ธํŠธ
              โ”” document.elementsFromPoint() ร— 7 ํ”„๋กœ๋ธŒ

์œ„์ ฏ ํ•˜๋‚˜๋‹น 9 ร— 7 = 63๋ฒˆ. ์œ„์ ฏ 10๊ฐœ๋ฉด ํ•œ ๋ฒˆ ๋ Œ๋”์— 630๋ฒˆ์˜ ๊ฐ•์ œ reflow. ๊ทธ๊ฒƒ๋„ ํ•˜ํ•„ requestAnimationFrame ์ฝœ๋ฐฑ ์•ˆ์—์„œ ๋™๊ธฐ์ ์œผ๋กœ ๋Œ์•˜๋‹ค. RAF๋Š” "ํŽ˜์ธํŠธ ๋ฐ”๋กœ ์ง์ „"์ด๋ผ, ์—ฌ๊ธฐ์„œ 630๋ฒˆ reflow๋ฅผ ํ•˜๋ฉด ํŽ˜์ธํŠธ๊ฐ€ ์ˆ˜ ์ดˆ๊ฐ„ ๋ธ”๋กœํ‚น๋œ๋‹ค. ์ธก์ •ํ•˜๋‹ˆ ์ด ๊ฒฝ๋กœ๊ฐ€ ์•ฝ 6,993ms๋ฅผ ์ฐจ์ง€ํ–ˆ๋‹ค.

ํ•ด๊ฒฐ 1: ๋ฌด๊ฑฐ์šด ์ผ์„ RAF ๋ฐ–์œผ๋กœ โ€” requestIdleCallback

์ฒซ ๋ฒˆ์งธ ์›์น™: ํŽ˜์ธํŠธ ๊ฒฝ๋กœ์—์„œ ๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ์„ ๊ฑท์–ด๋‚ธ๋‹ค. ํ…Œ๋งˆ ์ƒ˜ํ”Œ๋ง์„ RAF ์•ˆ์—์„œ ์ฆ‰์‹œ ํ•˜์ง€ ์•Š๊ณ , ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ•œ๊ฐ€ํ•  ๋•Œ(requestIdleCallback) ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ๋กœ ๋บ๋‹ค.

function getCachedTheme(widget) {
  const cached = themeCache.get(widget.id);
  if (cached?.theme) return cached.theme; // ์บ์‹œ ์žˆ์œผ๋ฉด ์ฆ‰์‹œ ๋ฐ˜ํ™˜

  // ์บ์‹œ ๋ฏธ์Šค: ์ง€๊ธˆ ๊ณ„์‚ฐํ•˜์ง€ ๋ง๊ณ  ํ์— ๋„ฃ๊ณ , ์ผ๋‹จ ๊ธฐ๋ณธ ํ…Œ๋งˆ ๋ฐ˜ํ™˜
  themeSampleQueue.add(widget.id);
  scheduleIdleThemeSampling();
  return DEFAULT_THEME; // โ† RAF๋ฅผ ๋ธ”๋กœํ‚นํ•˜์ง€ ์•Š๋Š”๋‹ค
}

function scheduleIdleThemeSampling() {
  if (themeSampleIdleHandle) return;
  themeSampleIdleHandle = requestIdleCallback((deadline) => {
    themeSampleIdleHandle = null;
    // ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ค€ ์—ฌ์œ  ์‹œ๊ฐ„(deadline) ์•ˆ์—์„œ๋งŒ ์กฐ๊ธˆ์”ฉ ์ฒ˜๋ฆฌ
    while (themeSampleQueue.size > 0 && deadline.timeRemaining() > 1) {
      const id = themeSampleQueue.values().next().value;
      themeSampleQueue.delete(id);
      const widget = widgets.find((w) => w.id === id);
      if (widget) themeCache.set(id, { key: id, theme: sampleTheme(widget) });
    }
    requestRender(); // ๋‹ค ๋๋‚˜๋ฉด ๊ทธ๋•Œ ํ•œ ๋ฒˆ ๋ Œ๋”
  });
}

๋ Œ๋”๋ง์€ "์ผ๋‹จ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๊ทธ๋ฆฌ๊ณ ", ๋ฌด๊ฑฐ์šด ์ƒ˜ํ”Œ๋ง์€ ์œ ํœด ์‹œ๊ฐ„์— ๋‚˜๋ˆ ์„œ ํ•œ๋‹ค. ํŽ˜์ธํŠธ๊ฐ€ ๋”๋Š” ๋ง‰ํžˆ์ง€ ์•Š๋Š”๋‹ค.

ํ•ด๊ฒฐ 2: ์• ์ดˆ์— ๊ณ„์‚ฐ๋Ÿ‰์„ ์ค„์ธ๋‹ค

requestIdleCallback์œผ๋กœ ์˜ฎ๊ฒจ๋„, ๊ทผ๋ณธ์ ์œผ๋กœ ํ˜ธ์ถœ ์ˆ˜๊ฐ€ ๋งŽ์œผ๋ฉด ์ข‹์„ ๊ฒŒ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์ƒ˜ํ”Œ ํฌ์ธํŠธ๋ฅผ 9๊ฐœ โ†’ 3๊ฐœ, ํ”„๋กœ๋ธŒ๋ฅผ 7๊ฐœ โ†’ 1๊ฐœ๋กœ ์ค„์˜€๋‹ค. ์œ„์ ฏ๋‹น 63๋ฒˆ โ†’ 3๋ฒˆ. ์‹ค์ธกํ•ด๋ณด๋‹ˆ 3๊ฐœ ํฌ์ธํŠธ๋งŒ์œผ๋กœ๋„ ๋ฐฐ๊ฒฝ์ƒ‰ ํŒ์ • ์ •ํ™•๋„๊ฐ€ ์ถฉ๋ถ„ํ–ˆ๋‹ค.

ํ•ด๊ฒฐ 3: ๊นœ๋นก์ž„ ์—†์• ๊ธฐ โ€” stale ์บ์‹œ ์œ ์ง€

๋‹ค์‹œ ์ƒ˜ํ”Œ๋งํ•˜๋Š” ๋™์•ˆ ์บ์‹œ๋ฅผ ๋น„์›Œ๋ฒ„๋ฆฌ๋ฉด, ๊ทธ ์ฐฐ๋‚˜์— ์œ„์ ฏ์ด ๊ธฐ๋ณธ์ƒ‰์œผ๋กœ ๋ฒˆ์ฉ์ธ๋‹ค. ๊ทธ๋ž˜์„œ "์žฌ์ƒ˜ํ”Œ๋ง ์ค‘"์ด์–ด๋„ ์ด์ „ ํ…Œ๋งˆ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์ƒˆ ๊ฐ’์ด ๋‚˜์˜ค๋ฉด ์กฐ์šฉํžˆ ๊ต์ฒดํ–ˆ๋‹ค. cache invalidation์˜ ๊ณ ์ „์  ํ•จ์ •์„, "์ง€์šฐ์ง€ ๋ง๊ณ  ๊ฐฑ์‹ ๋  ๋•Œ๊นŒ์ง€ ๋“ค๊ณ  ์žˆ๊ธฐ"๋กœ ํ”ผํ–ˆ๋‹ค.

๊ฒฐ๊ณผ

๋‹จ๊ณ„ ์กฐ์น˜ ์†Œ์š”
์ „ RAF ์•ˆ ๋™๊ธฐ ์ƒ˜ํ”Œ๋ง (630 calls) ~6,993ms
1 requestIdleCallback๋กœ ๋ถ„๋ฆฌ ~1,593ms
2 ์ƒ˜ํ”Œ ํฌ์ธํŠธ 9โ†’3, ํ”„๋กœ๋ธŒ 7โ†’1 (630โ†’30) ~133ms

์•ฝ 95% ๊ฐœ์„ . ์œ„์ ฏ์„ ์ž”๋œฉ ๋„์›Œ๋„ ํŽ˜์ด์ง€๊ฐ€ ๋”๋Š” ๋ฒ„๋ฒ…์ด์ง€ ์•Š๋Š”๋‹ค.

๋ฐฐ์šด ๊ฒƒ

  • ์ถ”์ธกํ•˜์ง€ ๋ง๊ณ  ์ธก์ •ํ•œ๋‹ค. DevTools๊ฐ€ getBackgroundColorAtPoint๋ฅผ ์ฝ• ์ง‘์–ด์ฃผ์ง€ ์•Š์•˜๋‹ค๋ฉด ์—‰๋šฑํ•œ ๋ฐ๋ฅผ ๊ณ ์ณค์„ ๊ฑฐ๋‹ค.

  • elementsFromPoint / getBoundingClientRect ๊ฐ™์€ ๋ ˆ์ด์•„์›ƒ์„ ์ฝ๋Š” API๋Š” ๊ทธ ์ž์ฒด๋กœ ํ•ญ์ƒ ๋น„์‹ผ ๊ฒŒ ์•„๋‹ˆ๋ผ, ์Šคํƒ€์ผ ์“ฐ๊ธฐ์™€ ๋ฒˆ๊ฐˆ์•„ ๋ถ€๋ฅด๋ฉด ๋งค๋ฒˆ ๊ฐ•์ œ reflow๊ฐ€ ๋œ๋‹ค (layout thrashing). ํŠนํžˆ RAFยท์Šคํฌ๋กค ํ•ธ๋“ค๋Ÿฌ ์•ˆ์—์„œ ๋ฐ˜๋ณตํ•˜๋ฉด ์น˜๋ช…์ .

  • ๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ํŽ˜์ธํŠธ ๊ฒฝ๋กœ ๋ฐ–(idle) ์œผ๋กœ, ๊ทธ๋ฆฌ๊ณ  ์• ์ดˆ์— ์–‘์„ ์ค„์ด๋Š” ๊ฒƒ ๋‘˜ ๋‹ค ํ•„์š”ํ–ˆ๋‹ค. ์‚ฌ์šฉ์ž ๋ˆˆ์—” ์•ˆ ๋ณด์ด๋Š” ์ด ์ตœ์ ํ™”๊ฐ€ ์‹ค์ œ ์ฒด๊ฐ์„ ๊ฐ€์žฅ ํฌ๊ฒŒ ๋ฐ”๊ฟจ๋‹ค.

  • ๊ด€๋ จ ํ”„๋กœ์ ํŠธ: Stock Price Widget

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
ยฉ 2026 ยท Groot's Tech Blog. Built with Gatsby ๐ŸŒฑ