A Complete Guide to useEffect

silverj-kim·2022년 6월 22일
0

useEffectcomponentDidMount 동작을 흉내내려면?

useEffect(fn, []) 으로 흉내낼 순 있지만 완전히 같지는 않다. 더 생산적으로 접근하기 위해 이펙트 기준으로 생각해야 한다.(thinking in effects)

이펙트를 일으키는 의존성 배열에 함수를 명시해도 될까?

추천하는 방법은 prop이나 state를 반드시 요구하지 않는 함수는 컴포넌트 바깥에 선언해서 호이스팅하고 이펙트 안에서만 사용되는 함수는 이펙트 함수 내부에 선언하는 것.
렌더 범위 안에 있는 함수를 이펙트가 사용하고 있다면 구현부를 useCallback 으로 감싸라.

왜 가끔씩 이펙트 안에서 이전 stateprops 값을 참조할까?

아마도 의존성 배열에 지정하는 걸 깜빡했을 것.

모든 렌더링은 고유의 propstate가 있다.

setState를 호출하여 state를 업데이트 할 때 마다 리액트는 컴포넌트를 호출한다.
특정 랜더링 시 그 안에 있는 state상수고, 상수는 시간이 지난다고 바뀌는 것이 아니다. 컴포넌트가 다시 호출되고 각각 랜더링 마다 격리된 고유의 state 값을 보는 것이다. 👀

모든 랜더링은 고유의 이벤트 핸들러를 가진다.

우리의 함수는 여러번 호출되지만(랜더링 마다 한 번씩), 각각의 랜더링에서 함수 안의 state 값은 상수이자 독립적인 값(특정 랜더링 시의 state) 으로 존재한다.

특정 랜더링 시 그 내부에서 props와 state는 영원히 같은 상태로 유지된다. 이를 사용하는 어떠한 값(이벤트 핸들러 포함)도 분리되어 있다.

모든 랜더링은 고유의 이팩트를 가진다

변화하지 않는 effect 안에서 state가 임의로 바뀌는 것이 아니라 effect 함수 자체가 매 랜더링 마다 별도로 존재한다.

리액트는 우리가 제공한 이펙트 함수를 기억해놨다가 DOM 의 변화를 처리하고 브라우저가 스크린에 그리고 난 뒤 실행한다.
사실 매 랜더링 마다 이펙트 함수는 다른 함수라고 이해하면 쉽다.

모든 랜더링은 고유의 ... 모든 것을 가지고 있다.

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });
componentDidUpdate() {
  setTimeout(() => {
     console.log(`You clicked ${this.state.count}   times`);
  }, 3000);
}

useEffect 훅과 클래스 컴포넌트의 componentDidUpdate() 가 다르게 동작하는 걸 위 예제로 확인할 수 있다.

흐름을 거슬러 올라가기

이펙트 안에 정의해둔 콜백에서 사전에 잡아둔 값을 쓰는 것이 아니라 최신의 값을 사용하고 싶을 때는 어떻게 해야할까?
제일 쉬운 방법은 ref 를 사용하는 것. 하지만 이런 방식은 흐름을 거슬러 올라가는 일이기 때문에 신중하게 사용해야 한다.

클린업(cleanup)은 뭐지?

클린업의 목적은 이펙트를 되돌리는 것.
리액트는 브라우저가 페인트를 하고 난 뒤에 이펙트를 실행한다. 그리하여 대부분의 이펙트가 스크린 업데이트를 가로막지 않아 앱을 빠르게 만들어 준다. 이전 이펙트는 새 prop과 함께 리랜더링 되고 난 뒤에 클린업된다.

라이프사이클이 아니라 동기화

useEffect는 리액트 트리 밖에 있는 것들을 prop과 state에 따라 동기화할 수 있다.

리액트에게 이펙트를 비교하는 법 가르치기

의존성 배열(Deps)에 값을 추가하는 것은 우리가 리액트에게 "랜더링 스코프에서 name(추가된 값)외의 값은 쓰지 않는다고 약속할게." 라고 하는 것과 같다.
리액트는 함수 안을 살펴볼 순 없지만 deps를 비교할 수 있기 때문에 deps가 같으면 새 이펙트 실행을 스킵한다.

액션을 업데이트로 부터 분리하기

어떤 상태 변수가 다른 상태 변수의 현재 값에 연관되도록 설정하려고 한다면, 두 상태 변수 모두 useReducer 로 교체해야 한다.
리튜서는 컴포넌트 안에서 일어나는 액션의 표현과 그 반응으로 상태가 어떻게 업데이트되어야 할지를 분리한다.

before

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

after

 const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: 'tick' }); // setCount(c => c + step) 대신에
  }, 1000);
  return () => clearInterval(id);
}, [dispatch]);

리액트는 컴포넌트가 유지되는 한 dispatch 함수가 항상 같다는 것을 보장한다. step이 바뀔 때 마다 인터벌을 다시 셋팅할 필요 없다.

왜 useReducer가 hooks의 치트 모드인가

그렇다.
useReducer를 사용하면 업데이트 로직과 그로 인해 무엇이 일어나는 지 서술하는 것을 분리할 수 있다. 불필요한 의존성을 제거하여 필요할 때보다 더 자주 실행되는 것을 피할 수 있도록 도와준다.

함수를 이펙트 안으로 옮기기

흔한 실수 중 하나가 함수는 의존성에 포함하면 안된다는 것. 함수를 이펙트 안으로 옮겨라.

저는 이 함수를 이펙트 안에 넣을 수 없어요

컴포넌트 안에 정의된 함수는 매 랜더링 마다 바뀐다.

  • 함수가 컴포넌트 스코프 안의 어떤 것도 사용하지 않는다면 컴포넌트 외부로 빼라.
  • useCallback 훅으로 감싸라

경쟁상태 race condition에 대하여

데이터 요청 순서롤 보장할 수 없기 때문에 먼저 시작된 요청이 더 늦게 끝나서 잘못된 상태를 덮어씌우는 경우가 있는데 이를 경쟁상태라 한다.
보통 비동기 호출 결과도 돌아올 때까지 기다린다고 여기며 위에서 아래로 데이터가 흐르면서 async/await이 섞여있는 코드에 자주 나타난다.
boolean 값을 사용하여 타이밍을 조절할 수 있다.

진입장벽 더 높이기

useEffect를 클래스 컴포넌트의 라이프 사이클 개념으로 생각하면 사이드 이펙트는 랜더링 결과물과 다르게 동작한다. UI를 랜더링하는 것은 props, state을 통해 이루어지며 이들의 일관성에 따라 보장받는다. 하지만 사이드이펙트는 아니다.
useEffect 의 개념으로 생각하면 모든 것들은 기본적으로 동기화된다. 사이드 이펙트는 리액트 데이터 흐름의 일부이다.

Suspense가 나오면서 데이터를 불러오는 경우를 더 많이 커버하게 되면 (이미 나왔지만) 미래에 useEffect는 더욱 로우 레벨로 내려가 파워유저들이 진정으로 사이드 이펙트를 통해 props, state를 동기화 하고자 할 때 사용하는 도구가 될 것이다.

참고: https://www.rinae.dev/posts/a-complete-guide-to-useeffect-ko

profile
Front-end developer

0개의 댓글