아래의 내용은 정리가 뒤죽박줄 할 수 있다.
쓰면서 계속 새로운 사실을 알게 되었기 때문에 그때그때 수정했기 때문이다.내가 결국 마지막에 이해가 된 것은 overReact에서였다.
따라서 링크를 첨부하여 아래 내용이 이해가 되지 않으면 다시 볼 수 있도록 하였다.
Making setInterval Declarative with React Hooks
A Complete Guide to useEffect아 참고로 overReact를 모두 읽어보는 것이 매우 좋겠다.
zerocho의 강의를 듣다가 이상한 것을 발견했다.
(강의는 react 웹게임 강좌의 가위바위보 게임이다.)
아래는 이번 여정의 시작이 된 사진이다.
class
형태로 구현된 코드다.
사진의 코드는 가위/바위/보를 연달아 보여주지 못하고 딱 한 번만 시행이 되었다.
그 이유는 imgCoord
에 담은 변수가 Component에 담길 때 한번 시행된 후, 고정되기 떄문이다.(class 형식에서는 render만 재시행되는 것을 기억하도록.)
따라서 zeroCho는 이후 setInterval 안에서 const {imgCoord} = this.state
할당을 시행하였다.
위 부분을 이해하고 나는 hooks
로 넘어갔다.
그런데,,
참고
Class와 hook은 완전 다른 방식으로 작동한다. overreacted
따라서 위에서 살펴본class
형태에서 발생한 문제와 아래hooks
에서 발생한 문제는 결이 다르다.
처음 작성시에는 같다고 생각했었으나, overreacted에서 관련 자료를 읽어보고 잘 못생각했다는 것을 깨달았다.
(전체 코드는 맨 아래에)
const changePicture = () => {
if(imgCoord === rspCoords['바위']){
setImgCoord(rspCoords['가위'])
} else if(imgCoord === rspCoords['가위']){
setImgCoord(rspCoords['보'])
} else {
setImgCoord(rspCoords['바위'])
}
console.log('hello')
console.log(imgCoord)
}
useEffect(()=>{
interval.current = setInterval(changePicture, 1000)
return () => {
clearInterval(interval.current)
}
},[])
처음에는 이렇게 짜면 정상적으로 시행이 될 것으로 보였다.
허나 그렇지가 않았다.
console.log('hello')
console.log(imgCoord)
이 문제를 해결하기 위해 찾아보다가 이 링크까지 가게 된 것이다.
(이 게시글을 보면, 내가 짠 코드가 왜 정상적으로 작동하지 않는지 알 수 있으며, 더 나아가 setIntverval을 React 내에서 가장 잘 활용할 수 있는 방법을 알려준다.)
이 게시글을 읽으면서 아래와 같은 사실들을 알 수 있었다.
이와 같은 특성으로 버그가 발생할 수 있다.
아래와 같은 코드는 작동하기는 한다.
import React, { useState, useEffect, useRef } from "react"; import ReactDOM from "react-dom"; function Counter() { const [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }); return <h1>{count}</h1>; } const rootElement = document.getElementById("root"); ReactDOM.render(<Counter />, rootElement);
아래와 같은 코드는 화면에 표시되는 count가 0으로 멈춰있게 된다.
1초당 +1을 시행하도록 코드를 위에서 짜 놓았는데,
컴포넌트 자체의 rendering을 0.1초마다 시키기 때문에 그렇다.import React, { useState, useEffect, useRef } from "react"; import ReactDOM from "react-dom"; function Counter() { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }); return <h1>{count}</h1>; } const rootElement = document.getElementById("root"); // Second interval to demonstrate the issue. // Fast updates from it cause the Counter's // interval to constantly reset and never fire. setInterval(() => { ReactDOM.render(<Counter />, rootElement); }, 100);
내 실수가 해당하는 부분이다.
다시 한번 내 코드를 가지고 와보면,
const changePicture = () => {
if(imgCoord === rspCoords['바위']){
setImgCoord(rspCoords['가위'])
} else if(imgCoord === rspCoords['가위']){
setImgCoord(rspCoords['보'])
} else {
setImgCoord(rspCoords['바위'])
}
console.log('hello')
console.log(imgCoord)
}
useEffect(()=>{
interval.current = setInterval(changePicture, 1000)
return () => {
clearInterval(interval.current)
}
},[])
useEffect
의 두 번째 인자에 []
를 부여하면 componentDidMount
와 같은 역할을 한다는 점에 착안하여 짠 코드다.
그러나 이는
useEffect는 imgCoord를 첫 번째 render 과정에서 내부에 저장한 것이다.
이 부분에 대해서 짐작하건데 useEffect가 내부에
imgCoord
를 함수 내부에서 만든 변수에 자동으로 할당하는 것 같다.
따라서 setInterval에 의해 클로저가 형성 되었을 때,setInterval
의 callback이 참조하는 imgCoord의 값은 상위 스코프에서 선언된imgCoord
가 되고, 이는 바뀐 컴포넌트 내의imgCoord
가 아니라 useEffect 함수 내에서 선언된imgCoord
가 되는 것으로 보인다.수정!🔥🔥🔥🔥🔥
overreacted에서 관련 부분을 찾았다!!!!!
각각의 effect는 해당 effect가 실행될 때의 props와 state 값을 활용한다고 한다. (말그대로 capture 한다.)
즉, react에서 매 컴포넌트 전체(함수)가 재실행되며, 이전 effect는 이전 함수 내의 클로저이기 때문에 해당 effect가 참조하는 것은 이전 함수의 props와 state라는 것이다.// 처음 랜더링 시 function Counter() { const count = 0; // useState() 로부터 리턴 // ... function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } // ... } // 클릭하면 함수가 다시 호출된다 function Counter() { const count = 1; // useState() 로부터 리턴 // ... function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } // ... } // 또 한번 클릭하면, 다시 함수가 호출된다 function Counter() { const count = 2; // useState() 로부터 리턴 // ... function handleAlertClick() { setTimeout(() => { alert('You clicked on: ' + count); }, 3000); } // ... }
따라서 setInverval이 시행되는 순간의 lexicalScope는 최초의 component였다.
console.log(imgCoord)를
1) changePicture 내부
2) changePicture 외부에
3) useEffect 내부에
각각 하나씩 넣어서 값을 비교한다면,useEffect
내부의 console.log는 단 한번 시행되며, changePicture 내/외부의 imgCoord은 서로 값이 다르다는 것을 알 수 있다.
이 글을 작성하다가 간략하게 작성한 클로저(미완)
여기 작성되어 있는 코드를 보면 외부에서 선언된 변수를 update하지 못하는 것을 볼 수 있다.실험 등을 통해 나온 나의 결론은 다음과 같다.
function outer(){ let cnt = 0 let cnt2 = cnt + 1 function inner1(){ cnt ++ console.log(cnt) } function inner2(){ console.log(cnt2) } return [inner1, inner2] } const [inner1, inner2] = outer() inner1() // 1 inner2() // 2 inner3() // 3 inner2() // 1
- 전역 환경에서 outer() 실행
- outer 함수의 환경에서 차례대로 cnt, cnt2에 값 할당
- 전역 환경에서 inner1() 실행
- inner1의 환경에서 outer의 환경에 있는 cnt를 참조하여 실행 * 3
- inner2의 내부 환경에서 cnt2가 log에 찍히는데, 이때 cnt2에는 1이 할당된 상태로 고정이 됨.(cnt2에 cnt1이 변해도 값이 재할당 되지 않는다. 어찌보면 당연하다.)
이 코드에서 useState의 두 번째 인자
를 건드리지 않고 강제로 update 시키고 싶다면,
1) "updater" form, 즉 setCount(prevState => prevState + 1)
식을 사용하거나 (prop으로 들어온 인자에 대해서는 적용할 수 없음)
왜 이렇게 작동하는지 정확하게 알지는 못하겠다.
더 깊게 들어가면 지칠 것 같으므로 이 부분은 여기서 멈추려고 한다.하지만 한 가지 의심해볼만한 사항은 updater form으로 작성시
react
내부에 강제적으로 component를 re-render 시키는 로직이 있지 않을까 하는 생각이 든다.updater form은 항상 state가 직전 상태임을 보장을 한다고 하는데, 그것이 강제 re-render로 구현되었을 것 같기 때문이다.
2) redux에서 많이 본듯한 useReducer()와 dispatch 구문을 이용.
하면 된다.
setInterval이 React와 같이 사용될 때 느껴지는 이질감의 원인은 setInterval의 내부 값들을 조작할 수 없다는데에 있다.
따라서 setInterval안의 값들을 바꿔주고 싶으면,
1) setInterval를 clear 한 후,
2) 새로운 setInterval를 만들어서
바꿔주어야 한다.
이 방식을 해결해주는 것이 바로 useRef
다.
useRef는 컴포넌트의 생애주기 전체에서 값을 공유한다. 따라서 개별의 effect는 그 effect가 선언된 해당 render 안의 props와 state만을 사용하는데 반해, ref 객체는 이전 render에서의 effect와 다음 render에서의 effect가 같은 값을 공유한다.
이와 같은 속성이 따라서 hooks에서 클로저 문제를 해결할 수 있다. (정확하게 말하면 effect가 props나 state를 'capture'하면서 발생하는 문제)
아래 코드를 보면 callback 함수는 render가 발생할 때마다 바뀐 count 값을 참조한다.
그리고 이 callback 함수를 Ref 객체
인 savedCallback
의 current
안에 담겨서 이전 render에서 실행된 interval에게 전달해주고 있다.
따라서 interval
이 클로저 문제에서 벗어나 계속 새로운 count를 갖고 시행될 수 있다.
function Counter() {
const [count, setCount] = useState(0);
const savedCallback = useRef();
function callback() {
setCount(count + 1);
}
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
더 나아가 delay도 동적으로 바뀌게 코드를 구성할 수 있다.
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
import React, {useState, useRef, useEffect} from 'react'
const rspCoords = {
바위: '0',
가위: '-142px',
보: '-284px',
};
const scores = {
가위: 1,
바위: 0,
보: -1,
};
const RSP = function (props){
const [result, setResult] = useState('')
const [imgCoord, setImgCoord] = useState('0')
const [score, setScore] = useState(0)
const interval = useRef(null)
const computerChoice = (imgCoord) => {
return Object.entries(rspCoords).find(v => v[1] === imgCoord )[0]
}
const changePicture = () => {
if(imgCoord === rspCoords['바위']){
setImgCoord(rspCoords['가위'])
} else if(imgCoord === rspCoords['가위']){
setImgCoord(rspCoords['보'])
} else {
setImgCoord(rspCoords['바위'])
}
console.log('hello')
console.log(imgCoord)
}
useEffect(()=>{
interval.current = setInterval(changePicture, 1000)
return () => {
clearInterval(interval.current)
}
},[])
const onClickBtn = string => {
clearInterval(interval.current)
const myScore = scores[string]
const cpuScore = scores[computerChoice(imgCoord)]
const diff = myScore - cpuScore
if(diff === 0 ){
setScore(score => score)
setResult('비겼습니다.')
} else if([1,-2].includes(diff)){
//컴이 이김
setScore(score => score - 1)
setResult('졌습니다.')
} else {
setScore(score => score + 1)
setResult('이겼습니다.')
}
}
return (
<>
<div id="computer" style={{ background: `url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${imgCoord} 0` }} />
<div>
<button id="rock" className="btn" onClick={() => onClickBtn('바위')}>바위</button>
<button id="scissor" className="btn" onClick={() => onClickBtn('가위')}>가위</button>
<button id="paper" className="btn" onClick={() => onClickBtn('보')}>보</button>
</div>
<div>{result}</div>
<div>현재 {score}점</div>
</>
)
}
export default RSP