사실 오늘 이벤트 루프에 대해 더 써보려고 했는데 너무 좋은글을 읽게 되어서 요약해서 적어보려고한다.
Dan Abramov의 글이다.
https://overreacted.io/a-complete-guide-to-useeffect/
useEffect에서 완벽히 comoponentDidMount를 대체할수 있을까? 란생각은 해본적이 있다.
또한 의존성 배열을 무엇으로 넣어야 할지, 이걸 넣어도 될지, 고민한적이 많다.
아래의 데이터 페칭이 무한루프 걸린적 또한 많다 ㅎㅎ
단순히 React 공식문서를 읽고 끝냈던것을 이 글을 통해 정리해보려고 한다.
🤔 useEffect 로 componentDidMount 동작을 흉내내려면 어떻게 하지?
🤔 useEffect 안에서 데이터 페칭(Data fetching)은 어떻게 해야할까? 두번째 인자로 오는 배열([]) 은 뭐지?
🤔 이펙트를 일으키는 의존성 배열에 함수를 명시해도 되는걸까?
🤔 왜 가끔씩 데이터 페칭이 무한루프에 빠지는걸까?
🤔 왜 가끔씩 이펙트 안에서 이전 state나 prop 값을 참조할까?
(이러한 고민들은 많이들 해보았을 것이다.)
이 글은 아는 것 보다 잊는것에 초점을 두라한다.!!!!
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
** <p>You clicked {count} times</p> **
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
//버튼 클릭!!!!!!!!!
// 처음 랜더링 시
function Counter() {
const count = 0; // useState() 로부터 리턴
// ...
<p>You clicked {count} times</p>
// ...
}
// 클릭하면 함수가 다시 호출된다
function Counter() {
const count = 1; // useState() 로부터 리턴
// ...
<p>You clicked {count} times</p>
// ...
}
// 또 한번 클릭하면, 다시 함수가 호출된다
function Counter() {
const count = 2; // useState() 로부터 리턴
// ...
<p>You clicked {count} times</p>
// ...
}
예시로 든 코드이다
그 후 state변화에 따른 count가 어떻게 바뀌고, 렌더링 되는지를 말한다.
처음 클릭시 state로부터 가져온 count 변수는 0,
setCount를 불러서 1을 set하면 다시 컴포넌트를 호출하고, 이때 count는 1이되는 식이다.
즉 state를 업데이트할때 마다, 컴포넌트를 다시 호출한다는 것이다.
매 렌더 결과물은 count변수를 "살펴보고" 이 값은 함수안에 "상수"로 존재하는 값이라는 것이다.
<p>You clicked {count} times</p>
즉 이건 어떠한 데이터 바인딩도 없이, 렌더링 결과물에 숫자값을 내장 ( 상수 ) 하는 것이다.
setCount 를 호출할 때, 리액트는 다른 count 값과 함께 컴포넌트를 다시 호출한다.(useState 로직을 보면 이해가 조금 더 쉽다!!)
그 후 리액트는 랜더링 결과물(count가 반영된)과 일치하도록 DOM을 업데이트 한다.
필자는 컴포넌트가 다시 호출되고, 각각의 랜더링마다 격리된 고유의 count 값을 “보는” 것입니다. 라고 요약한다.
(참고 : https://overreacted.io/ko/react-as-a-ui-runtime/)
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
이 예제는 버튼을 누르면 3초후 count값이 나오는 Alert을 띄우는 로직이다.
이 순서로 했을 때, 어떻게 실행이 될까??
3이 나온다 (참고 : https://overreacted.io/ko/how-are-function-components-different-from-classes/)
이는 아까 위에서 count가 "상수" 였다는 것에 주목해야 한다.
즉 우리가 보는 Alert은 count가 3인버전의 Alert인 것이다
이를 필자는
특정 랜더링 시 그 내부에서 props와 state는 영원히 같은 상태로 유지된다 !!. props와 state가 랜더링으로부터 분리되어 있다면, 이를 사용하는 어떠한 값(이벤트 핸들러를 포함하여)도 분리되어 있는 것입니다. 이들도 마찬가지로 특정 랜더링에 “속해 있습니다”. 따라서 이벤트 핸들러 내부의 비동기 함수라 할지라도 같은 count 값을 “보게” 될 것입니다.
라 요약한다.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
이번 예제는 리액트 공식문서의 예제이다.
어떻게 이펙트가 최신의 count를 읽을까??
이펙트에도 똑같이 count는 "상수"라는게 적용이 된다.
즉 이펙트도 똑같이 그 count ver 의 이펙트가 있다는 것이다.
// 최초 랜더링 시
function Counter() {
// ...
useEffect(
// 첫 번째 랜더링의 이펙트 함수
() => {
document.title = `You clicked ${0} times`;
}
);
// ...
}
// 클릭하면 함수가 다시 호출된다
function Counter() {
// ...
useEffect(
// 두 번째 랜더링의 이펙트 함수
() => {
document.title = `You clicked ${1} times`;
}
);
// ...
}
// 또 한번 클릭하면, 다시 함수가 호출된다
function Counter() {
// ...
useEffect(
// 세 번째 랜더링의 이펙트 함수
() => {
document.title = `You clicked ${2} times`;
}
);
// ..
}
각각의 이펙트 함수는 그 랜더링에 “속한” props와 state를 “본다”.
이해하기 쉽도록 필자의 예제를 넣어 보겠다
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
아까 문제와 비슷한 문제다.
5번 클릭하면 어떻게 될까?
1, 2, 3, 4, 5 가 찍힌다.
위에 글을 봤으면 아마 이렇게 생각 했을 것이다.
각각 클릭마다 count를 상수로 가지고 있으니 그렇게 될 것이라 생각 할 것이다.
나도 그랬다.
하지만 class component에서도 그럴까??
import React, {Component} from "react";
import ReactDOM from "react-dom";
class Example extends Component {
state = {
count: 0
};
componentDidMount() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({
count: this.state.count + 1
})}>
Click me
</button>
</div>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
똑같은 로직을 수행하는 코드이다.
같은값이 찍힐까??
아니다 !!! (와 진짜 충격적이였다.)
this.state.count 는 항상 최신의 값을 가져오기 떄문에, 5밖에 안찍히게 된다...
클로저는 접근 하려는 값이 절대 바뀌지 않을때 유용하다. 이문제 또한 클로저를 이용해 예제를 고쳐 바꿀 수 있다.https://codesandbox.io/s/w7vjo07055
“컴포넌트의 랜더링 안에 있는 모든 함수는 (이벤트 핸들러, 이펙트, 타임아웃이나 그 안에서 호출되는 API 등) 랜더(render)가 호출될 때 정의된 props와 state 값을 잡아둔다.”
위의 내용이다. “컴포넌트의 랜더링 안에 있는 모든 함수는 (이벤트 핸들러, 이펙트, 타임아웃이나 그 안에서 호출되는 API 등) 랜더(render)가 호출될 때 정의된 props와 state 값을 잡아둔다.”
최신의 값을 쓰고 싶으면 여러 방법이 있다. 그중 쉬운건 Ref를 이용하면 되는데, (https://overreacted.io/how-are-function-components-different-from-classes/ 마지막 단락)
이러한 과거의 렌더링 시점에서 최신의 값을 조회할때는 흐름을 거스르는 일이기 떄문에 주의 해야한다.
잘못 된건 아니지만 패러다임을 벗어나는게 ?? 일 수 있다. 아래의 코드는 의도적인 결과인데 하이라이트 쳐둔 코드는 타이밍에 민감하고 다루기 어렵기 때문이다. 이에 비해 클래스 컴포넌트라면 언제 값을 조회하는지 덜 명확하다.
아래 예제는 클래스 컴포넌트와 동일하게 작동한다.
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
리액트로 어떠한 값을 Ref로 변경하는 것이 꺼림칙 할 수도 있다. 리액트의 클래스 컴포넌트는 정확하게 이런 식으로 this.state 를 재할당하고 있습니다. 미리 잡아둔 props 및 state와는 달리 특정 콜백에서 latestCount.current 의 값을 읽어 들일 때 언제나 같은 값을 보장하지 않고 최신의 값을 받는다 !!
내 이해식으로 렌더링부분을 했다
다음 2탄, 3탄에서 나머지 부분을 써보겠다.