이전의 값을 가져오는 현상
이 발생할 때React 의 useEffect 는 클래스형 컴포넌트의 라이프 사이클, 생명주기 메서드와 완전 같은 개념이 아니다.
componentDidMount
와 유사하게 구현할 수 있는 것 뿐이지...
useEffect 를 제대로 이해하기 위해 라이프 사이클을 잊고, 다시 개념을 정리해보자.
useEffect 를 사용하면 보통 익명함수를 넣어 사용한다.
여기서 익명함수는 렌더링마다 새로 생성이 된다. 여러번 생성된 각기의 익명함수는 각자가 생성될 당시의 state 값을 바라본다.
즉 클로저가 생성되었다는 뜻이다.
useEffect 에 전달한 익명함수는 리액트가 변경사항을 DOM 에 전부 반영해서 렌더링을 끝낸 뒤에 실행해주는 함수이다. 이 함수는 생성될 그 당시의 state 값을 그대로 기억하고 있다. (클로저의 개념)
클로저
- 함수는 자신이 생성될 당시의 변수를 기억하고 있다. 그래서 본인이 가지고 있지 않은 변수를 참조해야 할 때 주변에 있는 다른 변수를 스코프 체인을 타고 올라가며 탐색을 한다.
즉, useEffect 의 익명 함수는 컴포넌트의 내부의 state, props 를 렌더링 단위로 기억한다
- useEffect 에 넘겨준 익명함수가 컴포넌트 내부에 있는 state, props 를 렌더링 단위로 기억하는 이유는 그 익명함수가 생성될 당시의 상황(주변 변수)을 기억하는 클로저의 특성 때문이라고 할 수 있는 것.
useEffect 를 포함한 각종 함수들을 선언하면 이 때 클로저가 형성이 된다.
useEffect 의 익명함수는 선언될 당시의 state 값을 기억하고 있다.브라우저의 렌더링이 끝나고
렌더링이 끝난 뒤, useEffect 가 선언해두었던 콜백함수를 실행. 이 함수는 클로저 때문에 선언될 당시에 기억하고 있는 변수 값을 참조한다.
즉, 컴포넌트 안에 있는 모든 함수는 렌더링 될 당시의 상수화된 변수값들을 사용한다.
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 를 참조하면 최신 값을 참조하게 된다.
useEffect 의 return 함수는 클래스형 컴포넌트의 생명주기 메서드 componentWillUnmount
고, 그 return 함수는 이펙트를 클린업하기 위해 사용한다.
useEffect 의 return 함수의 동작 순서
- props 나 state 가 업데이트
- 컴포넌트 리렌더링
- 이전 이펙트 클린업
- 새로운 이펙트 실행
컴포넌트를 리렌더링 하기 전에 클린업이 되는 게 아니라, 리렌더링이 된 후에 클린업이 되고
그 다음 새로운 이펙트가 실행이 되는 것이다.
React 는 브라우저가 페인팅을 하고 난 다음에 이펙트를 실행한다.
이펙트 클린함수이건, 이펙트 함수 자체든 말이다.
만약에 props.value 가 10 에서 20으로 업데이트 된 예를 들어보면, 이펙트는 아래와 같이 동작한다.
- props.value = 20 으로 업데이트
- 컴포넌트 리렌더링
- 이전 이펙트 클린업 (이전 이펙트 함수는 props.value = 10 을 바라보고 있다.)
- 새로운 이펙트 실행 (이 이펙트 함수는 변경된 props.value = 20 을 바라보고 있다.)
이전 이펙트 클린업 함수가 이전 값을 보고 있는 이유는 위에 말한 클로저의 특성 때문이다
📝 useEffect 의 진짜 목적은, React 컴포넌트 트리 바깥에 있는 것들을 props 와 state 에 따라 동기화 하는 것이다.
clean-up
함수는 useEffect 내의 함수가 여러번 실행될 때, 다음 useEffect 가 실행되기 전에 실행되는 함수이다.
컴포넌트가 언마운트 되거나 업데이트 되기 직전에 어떤 작업을 수행하고 싶다면, clean-up 함수를 반환해주어야 한다.
위에서 말한 것처럼 새로운 이펙트 실행을 하기 전에 이전 이펙트를 클린업을 해주는데
이 단계에서 클린업을 하게 되면, 선언될 당시의 과거의 변수 값을 참조하고 있던 이펙트가 클린업(정리)되고, 새로운 변수 값을 다시 참조하게 되는 것.
즉,
clean-up
함수는 보다 효율적으로 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 에 명시를 해주어야 버그가 발생하지 않는다
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
/// 1초마다 count + 1 을 증가시켜주도록
return () => clearInterval(id);
}, [count]);
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
/// 1초마다 count + 1 을 증가시켜주도록
return () => clearInterval(id);
}, []);
또는 useReducer
를 사용하여, 분리하는 방법 또한 있다