Dan Abramov 가 운영하는 overreacted 블로그에 포스팅 된 글로, 올라온 지는 좀 됐다. useEffect 에 대해 오해하고 있는 부분에 대해 원리부터 상세하게 설명되어 있다. (글 원문)
22일, 23일 총 이틀에 걸쳐서 읽었는데 왠지 글은 하나만 써야 할 거 같아서(라는 핑계로) 어제는 TIL 을 안 올렸다.
글에서 해결하고자 하는 질문은 다음과 같다.
- 🤔
useEffect
로componentDidMount
동작을 흉내내려면 어떻게 하지?- 🤔
useEffect
안에서 데이터 페칭(Data fetching)은 어떻게 해야할까? 두번째 인자로 오는 배열([]
) 은 뭐지?- 🤔 이펙트를 일으키는 의존성 배열에 함수를 명시해도 되는걸까?
- 🤔 왜 가끔씩 데이터 페칭이 무한루프에 빠지는걸까?
- 🤔 왜 가끔씩 이펙트 안에서 이전 state나 prop 값을 참조할까?
글이 워낙 길어서 소화를 100% 시키진 못했지만, 내가 이해한 만큼 최대한 정리해보려 한다.
useEffect
를 단순히 class 컴포넌트 시절의 componentDidMount
, componentDidUpdate
를 통합한 hook 이라고 생각하는 사람들이 꽤 있는데, dan 은 클래스와 함수의 차이가 존재하기 때문에 단순하게 치환이 가능하다고 생각하면 안 된다 고 한다.
클래스에서는 this 에서 props 정보를 불러오게 되는데, 이 때 불러온 props는 언제나 최신의 값이기 때문에 componentDidUpdate 에서 비동기 함수가 this.props 에 접근하는 경우 컴포넌트가 업데이트 된 시점의 값이 아니라 실제 비동기 함수가 호출되는 시점의 this.props 값을 사용하게 된다. (내 글론 이해가 안 될 수 있는데 원문의 글를 찬찬히 따라가보길 바란다. codepen 링크)
useEffect 는 자바스크립트의 closure 와 밀접한 연관이 있다. useEffect 는 상태 변경 이후에 예약되어 알 수 없는 시점에 비동기로 이루어지는 무언가가 아니다. 단지, 렌더링 시점의 props와 state 값을 알고 있는 함수다. 즉, 렌더링 될 때마다 새로운 인스턴스가 생성되며 각 인스턴스 내에서 사용되는 props와 state 는 해당 함수가 생성된 시점의 값이다.
componentDidMount 와 똑같이 만들고 싶으면 useEffect(fn, [])
을 사용해서 의존성 배열에 아무 것도 담지 않으면 된다. useEffect가 실행되는 시점이 첫 렌더링 이후이기 때문이다. (paint(1) > useEffect(1) > paint(2) > clean-up(1) > useEffect(2) 순서로 이루어진다.)
그치만 컴포넌트 마운트 시점에만 실행시키겠다고 의존성 배열에 거짓말을 쳐선 절대 안 된다고 dan은 강조, 강조 또 강조한다. 분명 useEffect 안에 의존하고 있는 함수가 있거나 state 가 있는데도 불구하고 fetch 를 한 번만 하기 위해서 의존성 배열을 비운다던지 하게 되면 문제가 생길 수 있다. 이러한 경우 의존성 배열을 거짓으로 비우는 것이 아니라 다른 방법을 사용해야 한다.
결론적으로 class 컴포넌트와 function 컴포넌트의 차이가 분명하게 존재하기 때문에 똑같이 만들려고 하지 말고 function 컴포넌트만의 특징을 잘 이해하라는 것이 취지로 보인다.
데이터 fetching 함수는 통상 비동기로 이루어진다. (로컬에 있는 js, json 파일을 가져오는 게 아닌 이상 백엔드와 통신을 하기 때문..) 컴포넌트가 렌더링 될 때 한 번만 데이터를 가져와서 화면에 보여주고 싶은 경우 대부분 useEffect 안에 fetch 함수를 넣고 의존성 배열을 비워두어 딱 한 번만 실행되게 만든다.
data fetching 말고, 좀 더 쉬운 예시로 useEffect 에서 어떤 state A 가 변경될 때마다 state B의 상태를 변경하기 위해 setState B 를 호출한다고 하자. setState(B + 1) 가 useEffect 에 있으면 의존성 배열 안에 B를 넣어야 하기 때문에 대신 업데이터 함수를 넣음으로서 의존성을 제거할 수 있다. (setState((beforeState) => beforeState + 1))
일단 첫 번째로는 구현체 자체를 useEffect 안에 넣기를 추천한다. 만약 useEffect 안에 fetch 함수를 호출하고 의존성 배열을 빈배열로 두게 된다면 당장 어떤 state 에 의존적이지 않을지라도 나중에 길어지다보면 상태를 보고 있을 수도 있고 이 때는 에러가 발생할 수 있기 때문이다. 이후 발생할 에러를 방지하기 위해서 그냥 함수 정의 자체를 useEffect 안에서 하라고 한다.
(이 부분을 더 읽어보면 좋겠다.)
글이 너무 길어서.. 내일 더 써야겠다.
좋은 글 감사합니다. 자주 올게요 :)