useEffect (feat. clean-up)

🍉effy·2022년 5월 11일
2

useEffect 를 사용할 때 이전의 값을 가져오는 현상 이 발생할 때

React 의 useEffect 는 클래스형 컴포넌트의 라이프 사이클, 생명주기 메서드와 완전 같은 개념이 아니다. componentDidMount 와 유사하게 구현할 수 있는 것 뿐이지...
useEffect 를 제대로 이해하기 위해 라이프 사이클을 잊고, 다시 개념을 정리해보자.


useEffect 를 사용할 때 state 가 즉시 업데이트가 되지 않는 이유는 뭘까?

useEffect 를 사용하면 보통 익명함수를 넣어 사용한다.

여기서 익명함수는 렌더링마다 새로 생성이 된다. 여러번 생성된 각기의 익명함수는 각자가 생성될 당시의 state 값을 바라본다.

즉 클로저가 생성되었다는 뜻이다.

useEffect 에 전달한 익명함수는 리액트가 변경사항을 DOM 에 전부 반영해서 렌더링을 끝낸 뒤에 실행해주는 함수이다. 이 함수는 생성될 그 당시의 state 값을 그대로 기억하고 있다. (클로저의 개념)

클로저

  • 함수는 자신이 생성될 당시의 변수를 기억하고 있다. 그래서 본인이 가지고 있지 않은 변수를 참조해야 할 때 주변에 있는 다른 변수를 스코프 체인을 타고 올라가며 탐색을 한다.

즉, useEffect 의 익명 함수는 컴포넌트의 내부의 state, props 를 렌더링 단위로 기억한다

  • useEffect 에 넘겨준 익명함수가 컴포넌트 내부에 있는 state, props 를 렌더링 단위로 기억하는 이유는 그 익명함수가 생성될 당시의 상황(주변 변수)을 기억하는 클로저의 특성 때문이라고 할 수 있는 것.
  1. useEffect 를 포함한 각종 함수들을 선언하면 이 때 클로저가 형성이 된다.
    useEffect 의 익명함수는 선언될 당시의 state 값을 기억하고 있다.

  2. 브라우저의 렌더링이 끝나고

  3. 렌더링이 끝난 뒤, useEffect 가 선언해두었던 콜백함수를 실행. 이 함수는 클로저 때문에 선언될 당시에 기억하고 있는 변수 값을 참조한다.

즉, 컴포넌트 안에 있는 모든 함수는 렌더링 될 당시의 상수화된 변수값들을 사용한다.


최신 상태의 state 를 가져와야지


useRef

useRef 는 함수형 컴포넌트 내부에서 useRef 를 마치 지역변수처럼 사용할 수 있다.
따라서 과거의 렌더링 시점에 갇혀있는 함수가 과거의 값을 참조하지 않고, 최신 값을 참조하도록 할 수 있다.


function Example () {
  const [count, setCount] = useState(0)
  const latestCount = useRef(count)
  
  useEffect () => {
    latestCount.current = count;
    
    setTimeout(() => {
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000)
  }
}

렌더링 될 때마다 당시의 state 값을 useRef 에 저장해두고, 콜백함수에서 이 ref 를 참조하면 최신 값을 참조하게 된다.


clean-up 함수

useEffect 의 return 함수는 클래스형 컴포넌트의 생명주기 메서드 componentWillUnmount 고, 그 return 함수는 이펙트를 클린업하기 위해 사용한다.

useEffect 의 return 함수의 동작 순서

  1. props 나 state 가 업데이트
  2. 컴포넌트 리렌더링
  3. 이전 이펙트 클린업
  4. 새로운 이펙트 실행

컴포넌트를 리렌더링 하기 전에 클린업이 되는 게 아니라, 리렌더링이 된 후에 클린업이 되고
그 다음 새로운 이펙트가 실행이 되는 것이다.

React 는 브라우저가 페인팅을 하고 난 다음에 이펙트를 실행한다.
이펙트 클린함수이건, 이펙트 함수 자체든 말이다.


만약에 props.value 가 10 에서 20으로 업데이트 된 예를 들어보면, 이펙트는 아래와 같이 동작한다.

  1. props.value = 20 으로 업데이트
  2. 컴포넌트 리렌더링
  3. 이전 이펙트 클린업 (이전 이펙트 함수는 props.value = 10 을 바라보고 있다.)
  4. 새로운 이펙트 실행 (이 이펙트 함수는 변경된 props.value = 20 을 바라보고 있다.)

이전 이펙트 클린업 함수가 이전 값을 보고 있는 이유는 위에 말한 클로저의 특성 때문이다


📝 useEffect 의 진짜 목적은, React 컴포넌트 트리 바깥에 있는 것들을 props 와 state 에 따라 동기화 하는 것이다.


그래서, clean-up 함수는 왜 사용하고, 언제 사용해야 하나?

clean-up 함수는 useEffect 내의 함수가 여러번 실행될 때, 다음 useEffect 가 실행되기 전에 실행되는 함수이다.

컴포넌트가 언마운트 되거나 업데이트 되기 직전에 어떤 작업을 수행하고 싶다면, clean-up 함수를 반환해주어야 한다.

위에서 말한 것처럼 새로운 이펙트 실행을 하기 전에 이전 이펙트를 클린업을 해주는데
이 단계에서 클린업을 하게 되면, 선언될 당시의 과거의 변수 값을 참조하고 있던 이펙트가 클린업(정리)되고, 새로운 변수 값을 다시 참조하게 되는 것.


즉, clean-up 함수는 보다 효율적으로 useEffect가 작동할 수 있도록 제어하여 메모리 관리를 하기 위함이다


useEffect를 사용했을 때, 무한 루프에 빠져버렸다..?

이전 프로젝트를 진행할 때, 무한 루프에 빠진 적이 있었다. 두번째 인자로 들어가는 deps 를 정해주지 않거나 빈배열을 넣어주어 사용했을 때. 렌더링 이후에 한번만 실행하게끔 빈 배열을 넣어 준 경우에도 무한 렌더링이 일어난 적이 있는데..


의존성 배열

위에서 말한 deps 를 의존성 배열이라고 한다. React 에 이런 이런 상황일 때, 이 이펙트를 실행해줘 라고 말해줘야 하는데, 이 의존성 배열로 할 수 있다.

의존성 배열 안의 요소가 변경되었을 때만 이펙트는 실행된다. deps 에 여러 개의 값이 전달이 되었을 때, 그 중 하나만이라도 변경이 되면 이펙트는 실행된다.


function Counter() {
	const [count, setCount] = useState(0);
  
  	useEffect(() => {
  		const id = setInterval(() => {
  		setCount(count + 1);
  }, 1000);
  /// 1초마다 count + 1 을 증가시켜주도록
  
  return () => clearInterval(id);
  }, []);
  return <div>{count}</div>
}

  • 위의 예시를 보면 useEffect 두번째 인자로 빈 배열이 들어갔으므로, 첫 렌더링 직후 한번만 실행이 된다

  • setInterval 이 실행되는데, setInterval은 count 를 참조해서 setCount 로 state 를 업데이트 시켜준다

  • setCount 로 업데이트를 하면, 컴포넌트가 count 가 1인 상태로 두번째 렌더링을 한다

  • 리액트는 이후에 이펙트를 실행할 지 안 할지를 결정하는데, 두번째 인자로 빈 배열을 넣어주었으므로, 이펙트를 실행하지 않는다

  • 이전 렌더링에서 등록했던 클린업 함수도 실행되지 않는다

  • 즉, setInterval 은 여전히 선언될 당시의 count=0 을 보게 되고, 이 상태로 interval 은 무한루프 한다


🧐 의존성 배열을 빈 배열로 지정하면 이펙트는 첫 렌더링 직후에 단 한번만 실행된다는 것을 알 수 있다.
=> React 는 useEffect 내부를 볼 수 없다. 이 이펙트를 실행시킬지의 여부를 스스로 결정할 수 없기 때문에 이펙트 내부에서 사용되는 모든 변수는 두번째 인자인 deps 에 명시를 해주어야 버그가 발생하지 않는다


effect 내부에 사용되는 값을 의존성 배열에 포함 시켜야 한다

 function Counter() {
	const [count, setCount] = useState(0);
  
  	useEffect(() => {
  		const id = setInterval(() => {
  		setCount(count + 1);
  }, 1000);
  /// 1초마다 count + 1 을 증가시켜주도록
  
  return () => clearInterval(id);
  }, [count]); 
  
  • 그런데 이렇게 deps 에 count 를 넣어주면, count 값은 이펙트를 다시 실행하면서 매번 다음 Interval 에서 해당 렌더링 시점의 count 값을 사용하게 되고 count 값이 변경 될 때마다 interval 이 해제된 후에 다시 설정된다.

 function Counter() {
	const [count, setCount] = useState(0);
  
  	useEffect(() => {
  		const id = setInterval(() => {
  		setCount(c => c + 1);
  }, 1000);
  /// 1초마다 count + 1 을 증가시켜주도록
  
  return () => clearInterval(id);
  }, []); 
  • 함수형으로 state 를 업데이트 해줌으로써, 원하는 만큼만 변경된 값을 반영시킬 수 있다

또는 useReducer 를 사용하여, 분리하는 방법 또한 있다

profile
Je vais l'essayer

0개의 댓글