https://tkdodo.eu/blog/the-useless-use-callback

 

The Useless useCallback

Why most memoization is downright useless...

tkdodo.eu

์›๋ฌธ

 

 

๋ฉ”๋ชจ์ด์ œ์ด์…˜(memoization)์— ๋Œ€ํ•ด์„œ๋Š” ์ด์ œ ์ถฉ๋ถ„ํžˆ ๋‹ค๋ค˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ์š”์ฆ˜ ์ž์ฃผ ๋ณด์ด๋Š” ํ•œ ๊ฐ€์ง€ ํŒจํ„ด์„ ๋ณด๋ฉด์„œ ์ƒ๊ฐ์ด ๋‹ฌ๋ผ์กŒ๋‹ค.
๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ useCallback๊ณผ, ๋ถ€๋ถ„์ ์œผ๋กœ๋Š” useMemo์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณด๋ ค ํ•œ๋‹ค.
ํŠนํžˆ ๋‚ด๊ฐ€ ๋ณด๊ธฐ์—” ์ •๋ง ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ๋“ค์— ๋Œ€ํ•ด ๋ง์ด๋‹ค.

 

์™œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์‚ฌ์šฉํ• ๊นŒ?

๋ณดํ†ต useCallback์œผ๋กœ ํ•จ์ˆ˜๋ฅผ, useMemo๋กœ ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ์ด์œ ๋Š” ๋‹จ ๋‘ ๊ฐ€์ง€๋ฟ์ด๋‹ค.

 

1. ์„ฑ๋Šฅ ์ตœ์ ํ™”(Performance optimization)

์–ด๋–ค ๋™์ž‘์ด ๋А๋ฆด ๋•Œ, ์šฐ๋ฆฌ๋Š” ๋ณดํ†ต ๊ทธ๊ฒƒ์„ “๋‚˜์˜๋‹ค”๊ณ  ๋А๋‚€๋‹ค. ์ด์ƒ์ ์œผ๋กœ๋Š” ๋” ๋น ๋ฅด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ข‹์ง€๋งŒ, ํ•ญ์ƒ ๊ทธ๋ ‡๊ฒŒ ํ•  ์ˆ˜๋Š” ์—†๋‹ค. ๋Œ€์‹  ๊ทธ ๋А๋ฆฐ ๋™์ž‘์ด ์ผ์–ด๋‚˜๋Š” ํšŸ์ˆ˜๋ฅผ ์ค„์ด๋Š” ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค.

React์—์„œ๋Š” ์ด๋Ÿฐ “๋А๋ฆฐ ๋™์ž‘”์ด ์ฃผ๋กœ ํ•˜์œ„ ํŠธ๋ฆฌ(sub-tree)์˜ ๋ฆฌ๋ Œ๋”๋ง์ด๋‹ค. ๋”ฐ๋ผ์„œ “๊ตณ์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค”๊ณ  ํŒ๋‹จ๋˜๋ฉด ์ด๋ฅผ ํ”ผํ•˜๊ณ  ์‹ถ์–ด์ง„๋‹ค.

 

์ด๋Ÿฐ ์ด์œ ๋กœ ๊ฐ€๋” React.memo๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๊ธฐ๋„ ํ•œ๋‹ค. ๋ฌผ๋ก  ์ด๋Š” ๋Œ€๋ถ€๋ถ„ “๋…ธ๋ ฅ ๋Œ€๋น„ ํšจ๊ณผ๊ฐ€ ํฌ์ง€ ์•Š์€ ์‹ธ์›€”์ด์ง€๋งŒ, ๊ทธ๋ž˜๋„ ์กด์žฌํ•˜๋Š” ํ…Œํฌ๋‹‰์ด๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ์ปดํฌ๋„ŒํŠธ์— ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜, ์›์‹œ ํƒ€์ž…์ด ์•„๋‹Œ ๊ฐ’์„ props๋กœ ๋„˜๊ธธ ๊ฒฝ์šฐ์—” ์ฐธ์กฐ(reference)๊ฐ€ ์•ˆ์ •์ ์ด์–ด์•ผ ํ•œ๋‹ค.
๊ทธ ์ด์œ ๋Š” React๊ฐ€ Object.is๋ฅผ ์‚ฌ์šฉํ•ด props๋ฅผ ๋น„๊ตํ•˜๊ณ , ๋™์ผํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด ํ•ด๋‹น ํ•˜์œ„ ํŠธ๋ฆฌ์˜ ๋ฆฌ๋ Œ๋”๋ง์„ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋งŒ์•ฝ ์ด ์ฐธ์กฐ๊ฐ€ ์•ˆ์ •์ ์ด์ง€ ์•Š๋‹ค๋ฉด — ์˜ˆ๋ฅผ ๋“ค์–ด, ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑ๋˜๋Š” ๊ฐ’์ด๋ผ๋ฉด — ์šฐ๋ฆฌ์˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ๊นจ์ง€๊ฒŒ ๋œ๋‹ค.

 

function Meh() {
  return (
    <MemoizedComponent
      value={{ hello: 'world' }}
      onChange={(result) => console.log('result')}
    />
  )
}

function Okay() {
  const value = useMemo(() => ({ hello: 'world' }), [])
  const onChange = useCallback((result) => console.log(result), [])

  return <MemoizedComponent value={value} onChange={onChange} />
}

 

๋ฌผ๋ก  ๋•Œ๋กœ๋Š” useMemo ๋‚ด๋ถ€์˜ ๊ณ„์‚ฐ ์ž์ฒด๊ฐ€ ๋А๋ฆด ๋•Œ๊ฐ€ ์žˆ๋‹ค.
์ด๋Ÿด ๋• ๋ถˆํ•„์š”ํ•œ ์žฌ๊ณ„์‚ฐ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๋Ÿฐ ๊ฒฝ์šฐ์˜ useMemo ์‚ฌ์šฉ์€ ์ „ํ˜€ ๋ฌธ์ œ ์—†๋‹ค.
ํ•˜์ง€๋งŒ ์†”์งํžˆ ๋งํ•˜์ž๋ฉด, ์ด๋Ÿฐ ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์€ ์•„๋‹ˆ๋‹ค.

 

 

2. Effect๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์ž์ฃผ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด

๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ๊ฐ’์„ ๋ฉ”๋ชจ๋œ ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ๋„˜๊ธฐ์ง€ ์•Š๋”๋ผ๋„,
๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ๊ทธ ๊ฐ’์€ ๊ฒฐ๊ตญ ์–ด๋–ค Effect์˜ ์˜์กด์„ฑ(dependency) ์œผ๋กœ ์ „๋‹ฌ๋œ๋‹ค.
(๋•Œ๋กœ๋Š” ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์˜ ์ปค์Šคํ…€ ํ›…์„ ๊ฑฐ์ณ์„œ ์ „๋‹ฌ๋˜๊ธฐ๋„ ํ•œ๋‹ค.)

Effect์˜ ์˜์กด์„ฑ ๋น„๊ต ๋ฐฉ์‹์€ React.memo์™€ ๋™์ผํ•˜๋‹ค.
React๋Š” ๊ฐ ์˜์กด์„ฑ์„ Object.is๋กœ ํ•˜๋‚˜์”ฉ ๋น„๊ตํ•ด, ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ๋‹ค๋ฉด Effect๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•œ๋‹ค.

๋”ฐ๋ผ์„œ Effect์˜ ์˜์กด์„ฑ ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜์ง€ ์•Š์œผ๋ฉด,
๋ Œ๋”๋ง์ด ์ผ์–ด๋‚  ๋•Œ๋งˆ๋‹ค Effect๊ฐ€ ๋งค๋ฒˆ ์žฌ์‹คํ–‰๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๊ฐ€๋งŒํžˆ ์ƒ๊ฐํ•ด๋ณด๋ฉด, ์ด ๋‘ ๊ฐ€์ง€ ์ƒํ™ฉ์€ ์‚ฌ์‹ค ์™„์ „ํžˆ ๊ฐ™์€ ๋งฅ๋ฝ์ด๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
๋‘˜ ๋‹ค “๊ฐ™์€ ์ฐธ์กฐ(reference)๋ฅผ ์œ ์ง€ํ•ด์„œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚˜์ง€ ์•Š๋„๋ก ๋ง‰๋Š” ๊ฒƒ”์ด ๋ชฉ์ ์ด๋‹ค.

์ฆ‰, useCallback์ด๋‚˜ useMemo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€์žฅ ๊ณตํ†ต๋œ ์ด์œ ๋Š” ๊ฒฐ๊ตญ ์ด๊ฑฐ๋‹ค:

 

 

์ฐธ์กฐ์˜ ์•ˆ์ •์„ฑ์ด ํ•„์š”ํ•˜๋‹ค.
์ฆ‰, ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ฐ’์ด๋‚˜ ํ•จ์ˆ˜๊ฐ€ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š๊ณ  
๊ฐ™์€ ์ฐธ์กฐ๋ฅผ ์œ ์ง€ํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ,
๊ทธ๊ฒŒ ๋ฐ”๋กœ useCallback๊ณผ useMemo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•ต์‹ฌ ์ด์œ ๋‹ค.

 

์šฐ๋ฆฌ ๋ชจ๋‘ ์ธ์ƒ์—์„œ ์–ด๋А ์ •๋„์˜ ์•ˆ์ •์„ฑ(stability)์€ ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
ํ•˜์ง€๋งŒ ์ฒ˜์Œ์— ๋งํ–ˆ๋“ฏ์ด, ๊ทธ ์•ˆ์ •์„ฑ์„ ๊ตณ์ด ์ถ”๊ตฌํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, ์ฆ‰ ‘์“ธ๋ฐ์—†๋Š” ์•ˆ์ •์„ฑ ์ถ”๊ตฌ’๋Š” ์–ธ์ œ์ผ๊นŒ?

 


 

1.  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ์—†๋‹ค๊ณ  ํ•ด์„œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์ด ์‚ฌ๋ผ์ง€๋Š” ๊ฑด ์•„๋‹ˆ๋‹ค

์•ž์„œ ์‚ดํŽด๋ณธ ์˜ˆ์‹œ์—์„œ, ์•„์ฃผ ์‚ฌ์†Œํ•œ ๋ถ€๋ถ„๋งŒ ํ•œ๋ฒˆ ๋ฐ”๊ฟ”๋ณด์ž.

 

no-memo

function Okay() {
  const value = useMemo(() => ({ hello: 'world' }), [])
  const onChange = useCallback((result) => console.log(result), [])

  return <Component value={value} onChange={onChange} />
}

 

์ฐจ์ด์ ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š”๊ฐ€? ๋งž๋‹ค — ์ด์ œ value์™€ onChange๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š”๋‹ค.
๊ทธ๋ƒฅ ์ผ๋ฐ˜์ ์ธ React ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์ผ ๋ฟ์ด๋‹ค.

์ด๋Ÿฐ ์ƒํ™ฉ์€ ํŠนํžˆ ๊ฒฐ๊ตญ์—” ๊ฐ’์ด React์˜ ๊ธฐ๋ณธ(built-in) ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋  ๋•Œ ์ž์ฃผ ๋ฐœ์ƒํ•œ๋‹ค:

 

react-built-ins

function MyButton() {
  const onClick = useCallback(
    (event) => console.log(event.currentTarget.value),
    []
  )

  return <button onClick={onClick} />
}

 

 

์—ฌ๊ธฐ์„œ onClick์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•ด๋„ ์•„๋ฌด๋Ÿฐ ํšจ๊ณผ๊ฐ€ ์—†๋‹ค.
์™œ๋ƒํ•˜๋ฉด ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋Š” onClick์ด ์ฐธ์กฐ๊ฐ€ ์•ˆ์ •์ ์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

“์•„๋ฌด ํšจ๊ณผ๊ฐ€ ์—†๋‹ค(Achieves nothing)”๋ผ๊ณ  ํ‘œํ˜„ํ–ˆ์ง€๋งŒ, ์—„๋ฐ€ํžˆ ๋งํ•˜๋ฉด ์•ฝ๊ฐ„ ์ž˜๋ชป๋œ ํ‘œํ˜„์ด๋‹ค. ์‚ฌ์‹ค ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ๋ถ„๋ช… ๋ฌด์–ธ๊ฐ€ ์ผ์ด ์ผ์–ด๋‚˜๊ณ  ์žˆ๋‹ค.

React๋Š” onClick ํ•จ์ˆ˜๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์บ์‹œ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•˜๊ณ , ์˜์กด์„ฑ์„ ์ถ”์ ํ•˜๋ฉฐ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๋น„๊ต๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. useCallback์— ์ „๋‹ฌ๋œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋„ ๋งค ๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์ง€๋งŒ, ์บ์‹œ๋œ ๋ฒ„์ „์ด ๋ฐ˜ํ™˜๋˜๋ฉด ์ฆ‰์‹œ ๋ฒ„๋ ค์ง„๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ, ๊ธฐ์ˆ ์ ์œผ๋กœ๋Š” ๋‚ด๋ถ€์— ์•ฝ๊ฐ„์˜ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ƒ๊ธด๋‹ค. ํ•˜์ง€๋งŒ ๋‚˜๋Š” ์ด ์˜ค๋ฒ„ํ—ค๋“œ์— ๋„ˆ๋ฌด ์ง‘์ค‘ํ•˜๊ณ  ์‹ถ์ง„ ์•Š๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ง„์งœ ๋ฌธ์ œ๋Š” ๊ทธ๊ฒŒ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

์ฆ‰, ์‚ฌ์šฉ์ž ์ •์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋ฉด, ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability)์€ ์‚ฌ์‹ค ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ๋œป์ด๋‹ค.

 

ํ•˜์ง€๋งŒ ์ž ๊น — ๋งŒ์•ฝ ๊ทธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ props๋ฅผ ๋‚ด๋ถ€์—์„œ useEffect์˜ ์˜์กด์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ๊ฐ’์„ ๋งŒ๋“ค์–ด ๊ทธ ๊ฐ’์„ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?


์ด ๊ฒฝ์šฐ, ์ง€๊ธˆ useMemo๋‚˜ useCallback์„ ์ œ๊ฑฐํ•˜๋ฉด , ๊ธฐ์กด ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์ œ๊ฑฐํ•˜๋ฉด ๋ฌด์–ธ๊ฐ€๊ฐ€ ๊นจ์งˆ ์ˆ˜๋„ ์žˆ๋‹ค.

๋ฐ”๋กœ ์ด ์ ์ด ๋‘ ๋ฒˆ์งธ ํฌ์ธํŠธ๋กœ ์ด์–ด์ง„๋‹ค.

 

2. props๋ฅผ ์˜์กด์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•  ๋•Œ

์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋ฐ›์€ ์›์‹œ ํƒ€์ž…์ด ์•„๋‹Œ(non-primitive) ๋น„์›์‹œ๊ฐ’ props๋ฅผ ๋‚ด๋ถ€ useEffect๋‚˜ useMemo ๋“ฑ์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š” ๊ฑด์€ ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„ ์ ์ ˆํ•˜์ง€ ์•Š๋‹ค.


์™œ๋ƒํ•˜๋ฉด ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๊ทธ props์˜ ์ฐธ์กฐ ์•ˆ์ •์„ฑ์— ๋Œ€ํ•ด ์–ด๋–ค ํ†ต์ œ๊ถŒ๋„ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

ํ”ํ•œ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

props-as-dependencies

function OhNo({ onChange }) {
  const handleChange = useCallback((e: React.ChangeEvent) => {
    trackAnalytics('changeEvent', e)
    onChange?.(e)
  }, [onChange])

  return <SomeMemoizedComponent onChange={handleChange} />
}

 

 

์ด useCallback์€ ์•„๋งˆ ์“ธ๋ชจ๊ฐ€ ์—†๊ฑฐ๋‚˜,
๊ธฐ๊ปํ•ด์•ผ ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์ด ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋А๋ƒ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ๋ฟ์ด๋‹ค.

๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ํ˜ธ์ถœํ•˜๋Š” ์ชฝ์—์„œ ๊ทธ๋ƒฅ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค.

 

<OhNo onChange={() => props.doSomething()} />โ€‹

 

 

์ด๊ฑด ๋ฌดํ•ดํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋‹ค. ์ „ํ˜€ ๋ฌธ์ œ๋  ๊ฒŒ ์—†๋‹ค.
์‚ฌ์‹ค ์˜คํžˆ๋ ค ์ข‹์€ ํŒจํ„ด์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์™€ ๊ด€๋ จ๋œ ๋™์ž‘์„ ํ•œ๊ณณ์— ๋ชจ์•„๋‘์–ด, ํŒŒ์ผ ์ƒ๋‹จ์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ค์„œ ๊ธธ๊ฒŒ ์ด๋ฆ„ ๋ถ™์ธ handleChange ๊ฐ™์€ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“œ๋Š” ์ผ์„ ํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๊นจ์ง„๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ ์ˆ˜ ์žˆ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€, ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๋ฅผ ๊นŠ์ด ํŒŒ๊ณ  ๋“ค์–ด๊ฐ€ props๊ฐ€ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋ฟ์ด๋‹ค.
์†”์งํžˆ ๋งํ•ด, ๊ทธ๊ฑด ๋”์ฐํ•˜๋‹ค.

 

๋‹ค๋ฅธ ํ•ด๊ฒฐ์ฑ…์œผ๋กœ๋Š” “๋ชจ๋“  ๊ฑธ ํ•ญ์ƒ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•œ๋‹ค”๋ผ๋Š” ์ •์ฑ…์„ ์„ธ์šฐ๊ฑฐ๋‚˜,
์ฐธ์กฐ ์•ˆ์ •์„ฑ์ด ํ•„์š”ํ•œ props์—๋Š” mustBeMemoized ๊ฐ™์€ ์—„๊ฒฉํ•œ ๋„ค์ด๋ฐ ๊ทœ์น™์„ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.
ํ•˜์ง€๋งŒ ์ด ๋‘˜ ๋‹ค ์ข‹์€ ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ๋‹ค.

 

์‹ค์ œ ์‚ฌ๋ก€(Real Life Example)

์ง€๊ธˆ ๋‚˜๋Š” Sentry ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ ์ž‘์—…ํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด๊ฑด ์˜คํ”ˆ์†Œ์Šค๋ผ์„œ ๐ŸŽ‰ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์‰ฝ๊ฒŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.

๋‚ด๊ฐ€ ๋ฐœ๊ฒฌํ•œ ํ•œ ๊ฐ€์ง€ ์ƒํ™ฉ์€ useHotkeys ์ปค์Šคํ…€ ํ›…์ด๋‹ค.
ํ•ต์‹ฌ ๋ถ€๋ถ„์€ ๋Œ€๋žต ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค:

 

useHotkeys

export function useHotkeys(hotkeys: Hotkey[]): {
  const onKeyDown = useCallback(() => ..., [hotkeys])

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [onKeyDown])
}

 

 

์ด ์ปค์Šคํ…€ ํ›…์€ hotKeys ๋ฐฐ์—ด์„ ์ธ์ž๋กœ ๋ฐ›๊ณ ,
๊ทธ๊ฑธ ๊ธฐ๋ฐ˜์œผ๋กœ onKeyDown ํ•จ์ˆ˜๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•œ ๋’ค, ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ effect์— ๋„˜๊ธด๋‹ค.

 

ํ•จ์ˆ˜๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ์ด์œ ๋Š” ๋ช…๋ฐฑํ•˜๋‹ค: Effect๊ฐ€ ๋„ˆ๋ฌด ์ž์ฃผ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋‹ค.


ํ•˜์ง€๋งŒ ์ž…๋ ฅ๊ฐ’์ธ hotkeys๊ฐ€ ๋ฐฐ์—ด์ด๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ์ง์ ‘ hotkeys๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

๊ทธ๋ž˜์„œ ๋‚˜๋Š” useHotkeys๊ฐ€ ์‹ค์ œ ์ฝ”๋“œ์—์„œ ์–ด๋–ป๊ฒŒ ์“ฐ์ด๊ณ  ์žˆ๋Š”์ง€ ์ „์ˆ˜ ์กฐ์‚ฌ๋ฅผ ํ•ด๋ดค๋‹ค.
๋†€๋ž๊ฒŒ๋„ ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„(ํ•œ ๊ณณ์„ ์ œ์™ธํ•˜๊ณ )์€ ์ž…๋ ฅ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

 

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

 

 

paginateHotKeys

const paginateHotkeys = useMemo(() => {
  return [
    { match: 'right', callback: () => paginateItems(1) },
    { match: 'left', callback: () => paginateItems(-1) },
  ]
}, [paginateItems])

useHotkeys(paginateHotkeys)

 

 

useHotKeys๋Š” ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ paginateHotkeys๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์ด paginateHotkeys๋Š” paginateItems์— ์˜์กดํ•œ๋‹ค.

๊ทธ๋Ÿผ paginateItems๋Š” ์–ด๋””์„œ ์˜ฌ๊นŒ?


๋ฐ”๋กœ ๋˜ ๋‹ค๋ฅธ useCallback์ด๊ณ , ์ด ์ฝœ๋ฐฑ์€ screenshots์™€ currentAttachmentIndex์— ์˜์กดํ•œ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด screenshots๋Š” ์–ด๋””์„œ ์˜ค๋Š” ๊ฑธ๊นŒ?

 

breaking-memoization

const screenshots = attachments.filter(({ name }) =>
  name.includes('screenshot')
)

 

screenshots๋Š” ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋˜์ง€ ์•Š์€ attachments.filter ํ•จ์ˆ˜์—์„œ ์ƒ์„ฑ๋œ๋‹ค.
์ด ํ•จ์ˆ˜๋Š” ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ๋ฐฐ์—ด(Array)์„ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, downstream์— ์žˆ๋Š” ๋ชจ๋“  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๊นจ์ง„๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ ๋ชจ๋“  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๋ฌด์šฉ์ง€๋ฌผ์ด ๋œ๋‹ค.


์ฆ‰, paginateItems, paginateHotkeys, onKeyDown
์ด ์„ธ ๊ฐ€์ง€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๋‹ค์‹œ ์‹คํ–‰๋˜๋ฉฐ,
๋งˆ์น˜ ์•„์˜ˆ ์ž‘์„ฑํ•˜์ง€ ์•Š์€ ๊ฒƒ๊ณผ ๋™์ผํ•œ ์ƒํ™ฉ์ด ๋œ๋‹ค! (์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์šฐ๋ฆฌ๊ฐ€ ํ–ˆ๋˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ์ „๋ถ€ ๋ฌด์šฉ์ง€๋ฌผ์ด ๋œ๋‹ค)

 

 

์ด ์˜ˆ์‹œ๊ฐ€ ๋‚ด๊ฐ€ ์™œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ ์šฉ์— ๋ฐ˜๋Œ€ํ•˜๋Š” ์ž…์žฅ์ธ์ง€ ์ž˜ ๋ณด์—ฌ์คฌ์œผ๋ฉด ํ•œ๋‹ค.
๋‚ด ๊ฒฝํ—˜์ƒ, ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ๋„ˆ๋ฌด ์ž์ฃผ ๊นจ์ง€๊ณ , ๊ทธ๋Ÿด ๊ฐ€์น˜๊ฐ€ ์—†๋‹ค.


๊ฒŒ๋‹ค๊ฐ€ ์ฝ”๋“œ ์ „์ฒด์— ์—„์ฒญ๋‚œ ์˜ค๋ฒ„ํ—ค๋“œ์™€ ๋ณต์žก์„ฑ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ํ•ด๊ฒฐ์ฑ…์ด screenshots๊นŒ์ง€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.


๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฑ…์ž„์ด ๊ฒฐ๊ตญ ์ปดํฌ๋„ŒํŠธ์˜ prop์ธ attachments๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค.
์„ธ ๊ฐ€์ง€ ํ˜ธ์ถœ ์ง€์ (call-side) ๋ชจ๋‘์—์„œ ์‹ค์ œ๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ํ•„์š”ํ•œ useHotkeys์™€๋Š” ์ตœ์†Œ ๋‘ ๋‹จ๊ณ„ ์ด์ƒ ๋–จ์–ด์ ธ ์žˆ๊ฒŒ ๋œ๋‹ค.


์ด๊ฑด ์ฝ”๋“œ๋ฅผ ํƒ์ƒ‰ํ•˜๊ธฐ ์•…๋ชฝ ๊ฐ™์€ ์ƒํ™ฉ์„ ๋งŒ๋“ค๋ฉฐ, ๊ฒฐ๊ตญ ์•„๋ฌด๋„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํ•˜๋‚˜๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๋œ๋‹ค.
์™œ๋ƒํ•˜๋ฉด ๊ทธ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ์‹ค์ œ๋กœ ๋ฌด์—‡์„ ํ•˜๊ณ  ์žˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์‹ค์ œ๋กœ๋Š”, ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์ปดํŒŒ์ผ๋Ÿฌ์—๊ฒŒ ๋ชจ๋“  ๊ฑธ ๋งก๊ธฐ๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹๋‹ค.


๋ชจ๋“  ํ™˜๊ฒฝ์—์„œ ์ž˜ ์ž‘๋™ํ•œ๋‹ค๋ฉด ์ •๋ง ํ›Œ๋ฅญํ•˜๋‹ค.
ํ•˜์ง€๋งŒ ๊ทธ ์ „๊นŒ์ง€๋Š”, ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability) ํ•„์š”์„ฑ์˜ ํ•œ๊ณ„๋ฅผ ์šฐํšŒํ•  ํŒจํ„ด์„ ์ฐพ์•„์•ผ ํ•œ๋‹ค.

 


 

 

์ตœ์‹  Ref ํŒจํ„ด(The Latest Ref Pattern)

์ด ํŒจํ„ด์— ๋Œ€ํ•ด์„œ๋Š” ์ด์ „์— ์–ธ๊ธ‰ํ•œ ์ ์ด ์žˆ๋‹ค.
ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š”, Effect ์•ˆ์—์„œ ์ฆ‰์‹œ ์ ‘๊ทผํ•˜๊ณ  ์‹ถ์€ ๊ฐ’์„ ref ์•ˆ์— ์ €์žฅํ•ด๋‘๊ณ ,
๋˜ ๋‹ค๋ฅธ Effect๋ฅผ ํ†ตํ•ด ์˜๋„์ ์œผ๋กœ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

 

the-latest-ref

export function useHotkeys(hotkeys: Hotkey[]): {
  const hotkeysRef = useRef(hotkeys)

  useEffect(() => {
    hotkeysRef.current = hotkeys
  })

  const onKeyDown = useCallback(() => ..., [])

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [])
}

 

 

๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด hotkeysRef๋ฅผ Effect ์•ˆ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•  ํ•„์š”๋„ ์—†๊ณ , ๋‹จ์ˆœํžˆ ๋ฆฐํ„ฐ๋ฅผ ๋ฌด์‹œํ•˜๋‹ค๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” stale closure ๋ฌธ์ œ๋„ ๊ฑฑ์ •ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

์‹ค์ œ๋กœ React Query๋„ ์ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ๋‹ค.


์˜ˆ๋ฅผ ๋“ค์–ด PersistQueryClientProvider๋‚˜ useMutationState์—์„œ ์ตœ์‹  ์˜ต์…˜(latest options)์„ ์ถ”์ ํ•  ๋•Œ ๊ทธ๋ ‡๋‹ค.
์ฆ‰, ๊ฒ€์ฆ๋œ(tried-and-true) ํŒจํ„ด์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
๋งŒ์•ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ต์…˜์„ ์ง์ ‘ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋„๋ก ๊ฐ•์ œํ•œ๋‹ค๋ฉด ์–ผ๋งˆ๋‚˜ ๊ท€์ฐฎ์„์ง€ ์ƒ์ƒํ•ด๋ณด๋ผ.

 

 

UseEffectEvent

์ข‹์€ ์†Œ์‹์ด ํ•˜๋‚˜ ๋” ์žˆ๋‹ค.
React ํŒ€์€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ๋ฆฌ์•กํ‹ฐ๋ธŒ Effect ์•ˆ์—์„œ ์ตœ์‹  ๊ฐ’์„ ๋ช…๋ นํ˜•(imperative)์œผ๋กœ ์ ‘๊ทผํ•  ํ•„์š”๊ฐ€ ์žˆ์ง€๋งŒ, Effect๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์€ ์ƒํ™ฉ์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์ธ์‹ํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ด ํŒจํ„ด์„ ์œ„ํ•œ 1๊ธ‰ ํ”„๋ฆฌ๋ฏธํ‹ฐ๋ธŒ(first-class primitive), useEffectEvent๋ฅผ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ์˜ˆ์ •์ด๋‹ค.

 

์ด ํ›…์ด ๋„์ž…๋˜๋ฉด, ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๋ณด๋‹ค ๊น”๋”ํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ๋ฆฌํŒฉํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค:

 

the-latest-ref

export function useHotkeys(hotkeys: Hotkey[]): {
  const onKeyDown = useEffectEvent(() => ...)

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [])
}

 

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด onKeyDown์€ ๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ hotkeys์˜ ์ตœ์‹  ๊ฐ’์„ ํ•ญ์ƒ “๋ณผ ์ˆ˜ ์žˆ๊ฒŒ” ๋˜๋ฉฐ, ๋ Œ๋” ์‚ฌ์ด์—์„œ๋„ ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability) ์„ ์œ ์ง€ํ•œ๋‹ค.

์ฆ‰, useCallback์ด๋‚˜ useMemo๋ฅผ ๋‹จ ํ•˜๋‚˜๋„ ์“ฐ์ง€ ์•Š๊ณ ๋„, ๋ชจ๋“  ์žฅ์ ์„ ๋™์‹œ์— ์–ป์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

 

์ •๋ฆฌ

๋‹จ์ˆœํžˆ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋‚˜ ์•ˆ์ •์„ฑ์„ ์ด์œ ๋กœ ๋ฌด์ž‘์ • useMemo/useCallback์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋Œ€๋ถ€๋ถ„ ๋ถˆํ•„์š”ํ•˜๊ฑฐ๋‚˜ ์˜คํžˆ๋ ค ๋ฌธ์ œ๋ฅผ ๋งŒ๋“ ๋‹ค.
๋Œ€์‹  Ref๋ฅผ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด React ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด ์ตœ์‹  ๊ฐ’์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฐธ์กฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์“ฐ๋Š” ๊ฒŒ ์ข‹๋‹ค.

 

 

 

+ Recent posts