본문 바로가기

FrontEnd

리액트 훅의 클로저 트랩(closure trap) 이해하기

반응형

원문 주소 : https://betterprogramming.pub/understanding-the-closure-trap-of-react-hooks-6c560c408cde

 

Understanding the Closure Trap of React Hooks

Digging into the classic problem

betterprogramming.pub

문제

import { useEffect, useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1500);
  }, []);

  useEffect(() => {
    setInterval(() => {
      console.log(count);
    }, 1500);
  }, []);

  return <div>Hello world</div>;
}

위 간단한 앱에서 count를 증가시키면 어떻게 될까요?

아래와 같이 0만 찍힙니다.

이것이 클로저 트랩입니다.

분석

React 런타임의 컴포넌트는 무엇입니까?

  • 컴포넌트는 실제로 파이버 노드입니다.
  • 그리고 각 파이버 노드에는 memorizedState라는 속성이 있습니다. 이 친구는 링크드 리스트입니다.
  • 컴포넌트의 각 Hook은 memorizedState 링크드 리스트의 노드에 해당하며 해당 노드에서 자신의 값에 액세스합니다.

각 훅은 memorizedState 링크드 리스트의 노드이며, 각자의 값을 갖고 있습니다.

위 3개의 훅은 memorizedState 링크드 리스트의 노드이며, 각자의 값을 갖고 있습니다.

각 훅은 next를 통해 서로를 연결합니다.

단방향 링크드 리스트군요!

각 훅은 각자의 memorizedState에 접근하여 자신의 방식대로 로직을 수행합니다.

 

훅의 구현

훅은 두 가지 위상을 같습니다.

즉 훅은 mount 함수이거나, 업데이트 함수입니다.

(주 : 실행 될 때의 "상태"정보를 갖으며, 해당 상태 플래그는 총 두개입니다.)

 

마운트 함수는 훅이 처음 생성될 때 실행되고,
이후 훅이 업데이트될 때마다 업데이트 함수가 실행됩니다.

아래는 useEffect의 구현입니다.

훅이 어떻게 의존성을 관리하나요?

우리는 훅의 deps 파라미터에 주목해야 합니다.

deps가 정의되지 않으면 undefined처럼 취급합니다.

그리고 새로운 deps가 전달되면, 이전 deps와 이것을 비교합니다.

동일하면 기존의 함수를 사용합니다.

그렇지 않으면 새로운 함수를 만듭니다.

두 deps가 같은지 판단하는 로직은 매우 간단합니다.

이전 deps가 null이면 false를 리턴합니다.

즉, 둘은 같지 않다는 것입니다.

그렇지 않으면 배열 내부를 비교합니다.

우리는 이제 3개의 결론을 얻을 수 있습니다.

 

  • 1. deps 배열이 undefined거나 null이면, 항상 함수를 새로 만듭니다.
  • 2. 빈 배열이면, 단 한번 마운트 시에민 실행합니다.
  • 3. 그렇지 않으면 배열 내부의 요소들을 각각 비교합니다.

useMemo 및 useCallback도 같은 방식으로 deps를 처리합니다.


이제 우리는 두 가지를 알고 있습니다.

  • useEffect와 같은 훅은 memorizedState를 통해 데이터를 접근합니다.
  • 훅은 deps가 같은지 비교하여 콜백 실행 여부를 판단합니다.

클로저 트랩

다시 클로저 트랩 문제로 돌아와 봅시다. 우리는 코드를 아래와 같이 작성했습니다.

useEffect(() => {
    const timer = setInterval(() => {
        setCount(count + 1);
    }, 500);
}, []);
useEffect(() => {
    const timer = setInterval(() => {
        console.log(count);
    }, 500);
}, []);
deps는 빈 배열이므로 이 효과는 한 번만 실행됩니다.
해당 소스 코드 구현은 다음과 같습니다.

실행해야 하는 이펙트는 HasEffect로 표시되며 나중에 실행됩니다.

여기서 deps는 빈 배열이므로 HasEffect 플래그가 없습니다. 효과가 더 이상 실행되지 않습니다.
 
따라서 타이머 setInterval은 한 번만 설정됩니다.
따라서 콜백 함수가 참조하는 상태는 항상 초기 상태이며 최신 상태를 얻을 수 없습니다.

최신 상태를 얻고 싶으면, fn이 매 렌더링 마다 한번씩 실행되도록 해야 합니다.

즉, 우리는 count를 의존성 배열에 제공해야 합니다.

useEffect(() => {
        setInterval(() => {
            setCount(count + 1);
        }, 1500);
    }, [count]);
useEffect(() => {
        setInterval(() => {
            console.log(count);
        }, 1500);
    }, [count]);

 
각 이펙트에서 타임 인터벌을 생성하므로, 클리어 함수를 제공해야 원하는 대로 동작합니다.

결론

memorizedState라는 연결 리스트가 파이버 노드에 저장됩니다.
연결 리스트의 노드는 하나씩 훅에 대응하며, 각 후크는 해당 노드의 데이터에 액세스합니다.
useEffect, useMomo 및 useCallback과 같은 후크에는 모두 deps 매개변수가 있습니다.
rerender가 수행될 때마다 새로운 deps와 이전 deps가 비교되며 deps가 변경되면 콜백 함수가 다시 실행됩니다.
 
따라서 매개변수가 정의되지 않고 null인 Hook은 모든 렌더러에서 실행되고,
매개변수가 []인 Hook은 한 번만 실행되며, 매개변수가 [state]인 Hook은 상태가 변경될 때만 다시 실행됩니다.
 
클로저 트랩이 발생하는 이유는 useEffect와 같은 hook에서 특정 상태를 사용하지만 deps 배열에 추가하지 않기 때문에
상태가 변경되어도 콜백 함수가 재실행되지 않고 이전 상태를 계속 참조하기 때문입니다.
 
클로저 트랩은 수정하기 쉽습니다. deps 배열을 올바르게 설정하기만 하면 됩니다.
상태가 변경될 때마다 새 상태를 참조하여 콜백 함수가 다시 실행됩니다.
그러나 이전 타이머, 이벤트 리스너 등을 정리하는 데에도 주의를 기울여야 합니다.

더보기

https://tkdodo.eu/blog/hooks-dependencies-and-stale-closures

 

Hooks, Dependencies and Stale Closures

Let's demystify what stale closures are in combination with react hooks with the help of the analogy of taking a photo ...

tkdodo.eu

 

10 JavaScript Closure Challenges Explained With Diagrams

Cracking the interview questions

betterprogramming.pub

 

A React Hooks Challenge for Senior React Developers

Can you solve this problem?

betterprogramming.pub

 

 

 

반응형