[React] useEffect에 대해 조금 더 알아보기 (cleanUpEffect, setState 동기처리)

GY·2021년 12월 20일
0

리액트

목록 보기
28/54
post-thumbnail

아직까지 이해가 제대로 안된 부분들이 있어 내용이 제대로 정리가 되지 않았다.
궁금한 부분을 해결하고 나서, 다시 정리할 예정이다.

setState는 비동기적으로 작동하기 때문에 state가 한 템포 늦게 변경되어 적용되고, 이를 동기적으로 처리하기 위해 setState에 콜백함수를 전달한다는 것은 알고 있었다.

오늘은 이게 그래서 무슨 말인지..무슨 원리인지 조금 더 깊게 알아보려고 한다.

왜 더 깊게 알려고 하는데?

단지 궁금해서만은 아니다.
공부할 시간은 한정되어 있고 공부할 건 많으니까, 정말 필요한 것들부터 공부하고 정리하려고 한다.

프로젝트를 진행하다보니 계속해서 useEffect함수가 발생하고 cleanUp함수에 대해서는 제대로 사용해본적이 없다보니 이 기회에 제대로 정리해 적절히 사용하는 방법에 대해 알아보기로 했다.



useEffect

state가 즉시 업데이트 되지 않는 이유

useEffect에 보통 익명함수를 넣어주어 사용했었다.
여기서 익명함수는 렌더링마다 새로 생성되는데, 여러번 생성된 각기의 익명함수는 각자가 생성될 당시의 state값을 바라보고 있다. 즉, 클로저가 생성되었다.

왜?

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

클로저

함수는 자신이 생성될 당시의 주변 변수를 기억하기 때문에 본인이 가지고 있지 않은 변수를 참조해야할 때 주변의 있는 다른 변수를 스코프 체인을 타고 올라가 탐색한다. 이때 변수와 함수가 선언될 당시의 값은 변하지 않는 상수값으로 저장되어 있다.

따라서 useEffect의 익명함수는 컴포넌트 내부의 state와 prop를 렌더링 단위로 기억한다.

  1. 우리가 useEffect를 포함한 각종 함수들을 선언해주면 이 때 클로저가 형성된다. useEffect의 익명함수는 선언될 당시의 state값을 기억하고 있다.
  2. 브라우저의 렌더링이 끝난다.
  3. 렌더링이 끝난 뒤 useEffect가 선언해두었던 콜백함수를 실행한다. 이 함수는 클로저 때문에 선언될 당시에 기억한 변수값을 참조한다.

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



state를 최신 상태로 가져오는 방법

1. 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);
  });

//출처: https://simsimjae.tistory.com/401?category=384814 [104%]

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

좋은 방법이지만, state와 props를 이용하는 방법이 더 궁금하다.


cleanup함수

드디어 나왔다!

useEffect함수를 사용하면서 구글링도 하고 공부하다보면 마주하게 되는 것이 cleanup함수였는데, 사실상 이 포스팅을 작성하게 된 이유이기도 하다. cleanup은 왜 쓰는지를 알기 위해서 지금까지의 위의 내용들을 정리했다.



clean-up 함수란?

useEffect()에서 파라미터로 전달한 콜백함수의 return 함수
useEffect 내의 함수가 여러번 실행될때, 다음 useEffect가 실행되기 전에 실행되는 함수
컴포넌트가 unmount되거나 update되기 직전에 어떤 작업을 수행하고 싶다면 clean-up함수를 반환해주어야 한다.

clean-up함수를 사용하면 리렌더링 후 이전의 effect들을 정리한 뒤, 다시 effect를 실행한다.

useEffect의 리턴함수는 다음과 같은 순서로 동작한다.
1. props/state 업데이트
2. 컴포넌트 리렌더링
3. 이전 이펙트 클린업
4. 새로운 이펙트 실행

(브라우저가 페인팅을 하고 난 뒤 이펙트를 실행하는 것인데, 이렇게 해야 리액트가 브라우저의 렌더링을 방해하지 않기 때문이다.)

이 단계에서 이펙트를 클린업하면, 선언될 당시의 과거의 변수값을 참조하고 있던 이펙트가 클린업(정리)되고 새로운 변수값을 다시 참조한다.


clean-up 함수를 쓰는 이유

보다 효율적으로 useEffect가 작동할 수 있도록 제어하여 메모리 관리에 용이하다.

찾았다, 무한루프에 빠지는 이유!

useeffect를 사용할 때 두번째 파라미터에 빈배열 []을 주어 사용하는 경우가 많았다.
렌더링 이후 한번만 실행되도록 하기 위한 방법이었는데, 이것은 버그가 있는 일종의 편법이라는 것은 처음 알았다.

예시를 보자.

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>{count}</h1>;
}

//출처: https://simsimjae.tistory.com/401?category=384814 [104%]

  1. useEffect 2번째 인자에 빈 배열이 들어갔으므로 첫 렌더링 직후 한번만 실행된다.
  2. 이때 setInterval이 실행되는데, setInterval은 count를 참조하여 setCount로 state를 업데이트 한다.
  3. setCount를 하면 컴포넌트가 count가 1인상태로 두번째 렌더링을 시작한다.
  4. 리액트가 이후 이펙트를 실행할지 말지 결정하는데, 2번째 파라미터에 빈 배열이 들어있으므로 이펙트를 실행하지 않는다.
  5. 이전 렌더링에서 등록했던 클린업함수도 실행되지 않는다.
  6. 즉, setInterval은 여전히 선언될 당시의 count 0을 바라보고 있고, 이 상태로 interval은 무한루프한다.

의존성 배열을 빈 배열로 지정할 경우 첫 렌더링 시에만 실행한다는 것은 알고 있었다.
왜 이렇게 동작하는 걸까?

리액트는 useEffect 함수 내부를 볼 수 없다. 따라서 이 이펙트 함수를 실행시켜야 할지 여부를 스스로 결정할 수 없다. 따라서 이펙트 내부에서 사용되는 모든 변수는 두번째 파라미터인 deps에 명시해 주어야 버그가 발생하지 않는다.



의존성 배열

리액트는 함수를 호출해보지 않고는 함수가 어떤 일을 하는지 알아낼 수 없다.
따라서 특정 이펙트가 불필요하게 다시 실행되는 것을 방지하고 싶다면 의존성 배열을 useEffect의 인자로 전달해주어야 한다.

이것은 우리가 리액트에게 이 함수의 안을 못보는 거 알아. 렌더링 스코프에서 의존성 배열에 전달한 값 말고 다른 값은 쓰지 않는다고 약속할게라고 하는 것과 같다.

그럼 리액트는 이 값들만 이전 이펙트 때와 같은지를 확인하고, 동기화가 필요없다면 함수를 호출하지 않는다.

fetch data

data를 가져올때 이런 방식을 많이 사용했었다.
그런데.. 이게 좋은 방법이 아니라고 한다.

function SearchResults() {
  async function fetchData() {
    // ...
  }
  useEffect(() => {
    fetchData();
  }, []); /
  // ...
}

그러면 어떻게 해야할까?

1. 이펙트 내부에 사용되는 모든 값을 의존성 배열에 포함하기

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

count값은 이펙트를 다시 실행하면서 매번 다음 interval에서 setCount(count + 1) 해당 렌더링 시점의 count값을 사용한다.

문제는...

count 값이 변경될때마다 인터벌이 해제된 후 다시 설정된다는 것이다.

2. 이펙트의 코드를 변경해 원하는 만큼만 변경된 값을 반영하기

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

이렇게 함수형으로 state를 업데이트 하면 된다.

3. useReducer 사용하기

어떤 상태변수가 다른 상태변수의 현재 값에 연관되도록 설정하고자 할 때, 두 상태변수 모두 useReducer로 교체하는 것이 더 적절할 수 있다.

리듀서는 컴포넌트에서 일어나는 액션의 표현과 그 반응을 상태가 어떻게 업데이트되어야 할지를 분리한다.

setState를 동기적으로 처리하기 위한 방법 - 콜백함수 전달

setState가 비동기적으로 처리되는 이유?

React는 batch update를 한다.
한 마디로 한번에 모아서 업데이트를 한다는 의미이다.
연속적으로 setState가 호출될 경우, 리액트는 batch update 를 16ms 단위로 진행한다.
16ms 동안 변경된 상태 값들을 모아서 한꺼번에 리렌더링을 진행하는 것이다.
이렇게 되면 최종적으로 변경된 상태값들을 모아 적용된 엘리먼트 트리를 비교한 뒤, 최종적으로 변경된 부분만 DOM에 적용하게 된다.

왜 이렇게 만든거야?

짐작할 수 있듯이, 불필요한 렌더링 횟수를 줄여 브라우저의 성능을 향상시키기 위함이다.


동기적으로 처리하기 위한 방법

setState에 이전 state와 함께 콜백함수를 인자로 전달하여 state를 업데이트 하는 방법이 있다.



Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글