λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
πŸ‘¨‍πŸ’» web.dev/fe

React 였래된 ν΄λ‘œμ €(Stale Closure) 이슈 ν•΄κ²°ν•˜κΈ°

by HandHand 2022. 5. 11.

 

πŸ“Œ requestAnimationFrame + React Hook

νŠΈλ¦¬ν”Œ μ‚¬μ „κ³Όμ œμ—μ„œ μ• λ‹ˆλ©”μ΄μ…˜μ„ κ΅¬ν˜„ν•  λ•Œ rAF(requestAnimationFrame)을 μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

CSS λ§ŒμœΌλ‘œλŠ” κ΅¬ν˜„μ΄ νž˜λ“€λ‹€κ³  μƒκ°λ˜μ–΄ μ‚¬μš©ν•œ API인데 React와 ν•¨κ»˜ μ‚¬μš©ν•˜λ©°

λ§ˆμ£Όν–ˆλ˜ 이슈λ₯Ό μ •λ¦¬ν•˜λ €κ³  ν¬μŠ€νŒ…μ„ λ‚¨κΈ°κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

 

πŸ“Œ rAFλ₯Ό μ‚¬μš©ν•œ slotMachine κ΅¬ν˜„λΆ€

 

ν•΄λ‹Ή section 을 κ΅¬ν˜„ν•˜λŠ” 것이 κ³Όμ œμ˜€λ‹€.

 

μœ„ μ„Ήμ…˜μ—μ„œ μ‚¬μš©μž μ§€ν‘œλ₯Ό λ‚˜νƒ€λ‚΄λŠ” 숫자 λΆ€λΆ„μ˜ 증감 속도가

천천히 κ°μ†Œν•˜λŠ” μ• λ‹ˆλ©”μ΄μ…˜μ„ κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄ rAFλ₯Ό μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

κ΅¬ν˜„ μ½”λ“œλŠ” λŒ€λž΅ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

ν•΄λ‹Ή μ»΄ν¬λ„ŒνŠΈκ°€ μ΅œμ΄ˆμ— λ Œλ”λ§ 될 λ•Œ μ• λ‹ˆλ©”μ΄μ…˜μ„ μ‹œμž‘ν•˜κΈ° μœ„ν•΄ useEffectλ₯Ό μ‚¬μš©ν•˜μ˜€κ³ 

λͺ©ν‘œ μˆ«μžμ— 도달할 λ•ŒκΉŒμ§€ μž¬κ·€μ μœΌλ‘œ μ• λ‹ˆλ©”μ΄μ…˜ ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.

const SlotMachine = ({ startNumber, targetNumber, duration }) => {
    const [count, setCount] = useState(0)
    const animationId = useRef(0)    

    useEffect(() => {
        const increaseNumberAnimation = (timestamp: number) => {
            /** βœ… λͺ©ν‘œ μˆ«μžμ— λ„λ‹¬ν• λ•ŒκΉŒμ§€ μ• λ‹ˆλ©”μ΄μ…˜μ„ λ°˜λ³΅ν•©λ‹ˆλ‹€. */
            if (count < targetNumber) { 
                /** βœ… λ‹€μŒ ν”„λ ˆμž„μ— λ³΄μ—¬μ€˜μ•Όν•˜λŠ” 숫자λ₯Ό κ³„μ‚°ν•œ λ’€ */
                const next = getNextFrameNumber(timestamp)

                /** βœ… 이 숫자λ₯Ό μƒνƒœκ°’μ— μ—…λ°μ΄νŠΈν•΄μ„œ 화면을 λ‹€μ‹œ λ Œλ”λ§ν•©λ‹ˆλ‹€. */            
                setCount(next)
                animationId.current = requestAnimationFrame(increaseNumberAnimation)
            }
        }

        animationId.current = requestAnimationFrame(increaseNumberAnimation)

        return () => cancelAnimationFrame(animationId.current)
    })

    return <data>{ count }</data>
}

ν•˜μ§€λ§Œ μœ„μ™€ 같이 κ΅¬ν˜„ν•˜λ©΄ increaseNumberAnimation ν•¨μˆ˜κ°€ λ¬΄ν•œμ • 호좜되게 λ©λ‹ˆλ‹€.

μ™œ 이런 λ¬Έμ œκ°€ λ°œμƒν•˜λŠ” κ²ƒμΌκΉŒμš”?

 

πŸ“Œ 였래된 ν΄λ‘œμ €(Stale Closure) λž€?

였래된 ν΄λ‘œμ € (stale closure)λŠ” React Hook을 μ‚¬μš©ν•  λ•Œ 빈번히 λ°œμƒλ˜λŠ” μ΄μŠˆμž…λ‹ˆλ‹€.

이 ν˜„μƒμ΄ λ°œμƒν•˜λ©΄ μƒνƒœ 값에 λ³€ν™”κ°€ λ°œμƒν•΄λ„ 이λ₯Ό κ°μ§€ν•˜μ§€ λͺ»ν•˜κ³  μ˜ˆμ „ 값을 λ°”λΌλ³΄κ²Œ λ©λ‹ˆλ‹€.

κ·Έλ ‡λ‹€λ©΄ JavaScript μ—μ„œ κΌ­ λ“±μž₯ν•˜λŠ” κ°œλ…μΈ ν΄λ‘œμ €λž€ λ¬΄μ—‡μΌκΉŒμš”?

 

ν΄λ‘œμ €λž€ 생λͺ…μ£ΌκΈ°κ°€ λλ‚œ μ™ΈλΆ€ν•¨μˆ˜μ˜ λ³€μˆ˜λ₯Ό μ°Έμ‘°ν•˜λŠ” ν•¨μˆ˜λ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€.

 

이제 μœ„μ—μ„œ λ°œμƒν•œ 문제의 원인을 μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

React의 useStateλŠ” μƒνƒœ 값을 count λΌλŠ” λ³€μˆ˜μ— λ„˜κ²¨μ€λ‹ˆλ‹€.

그리고 increaseNumberAnimationλΌλŠ” ν•¨μˆ˜κ°€ μ΄ˆκΈ°ν™”λ  λ•Œ count 값을 μ°Έμ‘°ν•˜λŠ”λ°,

μ΄λ•Œ λ³€ν™”λœ count κ°’μœΌλ‘œ increaseNumberAnimation ν•¨μˆ˜λ₯Ό μž¬ν• λ‹Ήν•˜μ§€ μ•ŠμœΌλ©΄

초기의 count 값인 0 을 계속 바라보고 있게 λ©λ‹ˆλ‹€.

 

πŸ“Œ Stale Closure 문제 ν•΄κ²°ν•˜κΈ°

κ·Έλ ‡λ‹€λ©΄ 이 문제λ₯Ό μ–΄λ–»κ²Œ ν•΄κ²°ν•  수 μžˆμ„κΉŒμš”?

μ œκ°€ μƒκ°ν•œ 방법은 두 가지이며 상황에 따라 μ μ ˆν•œ 방법을 μ„ νƒν•˜λ©΄ λ©λ‹ˆλ‹€.

1️⃣ useEffect에 μ˜μ‘΄μ„± μ•Œλ €μ£ΌκΈ°

useEffect에 μ˜μ‘΄μ„± λ°°μ—΄λ‘œ countλ₯Ό μ „λ‹¬ν•˜λ©΄ count의 λ³€ν™”λ₯Ό κ°μ§€ν•΄μ„œ

μƒˆλ‘œμš΄ increaseNumberAnimation ν•¨μˆ˜λ₯Ό 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.

useEffect(() => {
	const increaseNumberAnimation = (timestamp: number) => {
    	// ...
    }
    // ...
}, [count]) // <-- count μΆ”κ°€!

ν•˜μ§€λ§Œ μ΄λ ‡κ²Œ ν•˜λ©΄ count κ°€ μ¦κ°€ν• λ•Œλ§ˆλ‹€ requestAnimationFrame ν•¨μˆ˜μ™€

cancelAnimationFrame ν•¨μˆ˜κ°€ λͺ©ν‘œ μˆ«μžμ— 도달할 λ•ŒκΉŒμ§€ 반볡적으둜 μ‹€ν–‰λ˜κΈ° λ•Œλ¬Έμ—

μ„±λŠ₯μƒμœΌλ‘œ λ¬Έμ œκ°€ 될 μˆ˜λ„ μžˆμ„ 것이라고 μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

κ·Έλž˜μ„œ 이번 κ²½μš°μ—λŠ” 이 λ°©λ²•λ³΄λ‹€λŠ” useRef λ₯Ό ν™œμš©ν•œ 두 번째 방법을 μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

 

2️⃣ useRef λ₯Ό μ΄μš©ν•΄μ„œ μƒνƒœ κ°’ κ΄€λ¦¬ν•˜κΈ°

React 곡식 κ°€μ΄λ“œ λ¬Έμ„œμ— 보면 useRef λ₯Ό μ΄μš©ν•΄μ„œ μ»΄ν¬λ„ŒνŠΈμ˜ μ§€μ—­λ³€μˆ˜μ™€ 같은 역할을

μˆ˜ν–‰ν•  수 μžˆλ‹€κ³  μ•ˆλ‚΄ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

useRef μ˜ λ°˜ν™˜ 값은 λ³€μˆ˜μ˜ 참쑰값이기 λ•Œλ¬Έμ— 참쑰값이 κ°€λ¦¬ν‚€λŠ” λ³€μˆ«κ°’μ„ μ—…λ°μ΄νŠΈν•˜λ©΄

참쑰값은 λ³€ν•˜μ§€ μ•Šλ”λΌλ„ μ΅œμ‹  값을 μ•ˆμ „ν•˜κ²Œ κ°€μ Έμ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

이λ₯Ό μœ„ν•΄ ν˜„μž¬ count 값을 μ €μž₯ν•˜λŠ” tick 을 μ„ μ–Έν•©λ‹ˆλ‹€.

const SlotMachine = ({ startNumber, targetNumber, duration }) => {
    const [count, setCount] = useState(0)
    const animationId = useRef(0)    
    const tick = useRef(0)

    useEffect(() => {
        const increaseNumberAnimation = (timestamp: number) => {
            /** βœ… tick.current 둜 쑰건 검사 */
            if (tick.current < targetNumber) { 

                const next = getNextFrameNumber(timestamp)

                /** βœ… 이 숫자λ₯Ό μƒνƒœκ°’μ— μ—…λ°μ΄νŠΈν•΄μ„œ 화면을 λ‹€μ‹œ λ Œλ”λ§ν•©λ‹ˆλ‹€. */            
                setCount(next)
                tick.current = next // <-- μΆ”κ°€!
                animationId.current = requestAnimationFrame(increaseNumberAnimation)
            }
        }

        // ...
    })

    return <data>{ count }</data>
}

 

πŸ“Œ 참고자료

Be Aware of Stale Closures when Using React Hooks

[λ²ˆμ—­] 심측 뢄석: React Hook은 μ‹€μ œλ‘œ μ–΄λ–»κ²Œ λ™μž‘ν• κΉŒ?

Things to understand in react "Stale Clousers".

λ°˜μ‘ν˜•

πŸ’¬ λŒ“κΈ€