useEffect
로 componentDidMount
동작을 흉내내려면?useEffect(fn, [])
으로 흉내낼 순 있지만 완전히 같지는 않다. 더 생산적으로 접근하기 위해 이펙트 기준으로 생각해야 한다.(thinking in effects)
추천하는 방법은 prop이나 state를 반드시 요구하지 않는 함수는 컴포넌트 바깥에 선언해서 호이스팅하고 이펙트 안에서만 사용되는 함수는 이펙트 함수 내부에 선언하는 것.
렌더 범위 안에 있는 함수를 이펙트가 사용하고 있다면 구현부를 useCallback
으로 감싸라.
state
나 props
값을 참조할까?아마도 의존성 배열에 지정하는 걸 깜빡했을 것.
prop
과 state
가 있다.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
를 사용하는 것. 하지만 이런 방식은 흐름을 거슬러 올라가는 일이기 때문에 신중하게 사용해야 한다.
클린업의 목적은 이펙트를 되돌리는 것.
리액트는 브라우저가 페인트를 하고 난 뒤에 이펙트를 실행한다. 그리하여 대부분의 이펙트가 스크린 업데이트를 가로막지 않아 앱을 빠르게 만들어 준다. 이전 이펙트는 새 prop과 함께 리랜더링 되고 난 뒤에 클린업된다.
useEffect는 리액트 트리 밖에 있는 것들을 prop과 state에 따라 동기화할 수 있다.
의존성 배열(Deps)에 값을 추가하는 것은 우리가 리액트에게 "랜더링 스코프에서 name
(추가된 값)외의 값은 쓰지 않는다고 약속할게." 라고 하는 것과 같다.
리액트는 함수 안을 살펴볼 순 없지만 deps를 비교할 수 있기 때문에 deps가 같으면 새 이펙트 실행을 스킵한다.
어떤 상태 변수가 다른 상태 변수의 현재 값에 연관되도록 설정하려고 한다면, 두 상태 변수 모두 useReducer
로 교체해야 한다.
리튜서는 컴포넌트 안에서 일어나는 액션의 표현과 그 반응으로 상태가 어떻게 업데이트되어야 할지를 분리한다.
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
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를 사용하면 업데이트 로직과 그로 인해 무엇이 일어나는 지 서술하는 것을 분리할 수 있다. 불필요한 의존성을 제거하여 필요할 때보다 더 자주 실행되는 것을 피할 수 있도록 도와준다.
흔한 실수 중 하나가 함수는 의존성에 포함하면 안된다는 것. 함수를 이펙트 안으로 옮겨라.
컴포넌트 안에 정의된 함수는 매 랜더링 마다 바뀐다.
useCallback
훅으로 감싸라데이터 요청 순서롤 보장할 수 없기 때문에 먼저 시작된 요청이 더 늦게 끝나서 잘못된 상태를 덮어씌우는 경우가 있는데 이를 경쟁상태라 한다.
보통 비동기 호출 결과도 돌아올 때까지 기다린다고 여기며 위에서 아래로 데이터가 흐르면서 async/await이 섞여있는 코드에 자주 나타난다.
boolean 값을 사용하여 타이밍을 조절할 수 있다.
useEffect를 클래스 컴포넌트의 라이프 사이클 개념으로 생각하면 사이드 이펙트는 랜더링 결과물과 다르게 동작한다. UI를 랜더링하는 것은 props, state을 통해 이루어지며 이들의 일관성에 따라 보장받는다. 하지만 사이드이펙트는 아니다.
useEffect 의 개념으로 생각하면 모든 것들은 기본적으로 동기화된다. 사이드 이펙트는 리액트 데이터 흐름의 일부이다.
Suspense가 나오면서 데이터를 불러오는 경우를 더 많이 커버하게 되면 (이미 나왔지만) 미래에 useEffect는 더욱 로우 레벨로 내려가 파워유저들이 진정으로 사이드 이펙트를 통해 props, state를 동기화 하고자 할 때 사용하는 도구가 될 것이다.
참고: https://www.rinae.dev/posts/a-complete-guide-to-useeffect-ko