가위바위보 게임은 비동기를 연습해볼 수 있는 프로그램 중 하나다.
이때 비동기를 리액트의 라이프사이클과 함께 사용하는 방법에 대해 알아보려고 한다.
우선 순수 javascript로만 만들어진 코드는 이러하다
자바스크립트 코드
게임을 시작하자마자 setInterval을 사용해서 묵찌바 모션을 반복해주고,
사용자가 클릭했을 때 멈췄다가 다시 게임을 재개하는 것이 포인트다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>가위바위보</title>
<link rel="stylesheet" href="style.css" />
<script
src="https://kit.fontawesome.com/bdf3223b68.js"
crossorigin="anonymous"
></script>
</head>
<body>
<script async src="main.js"></script>
<section class="game">
<div class="computer">
<i class="far fa-hand-rock"></i>
<i class="far fa-hand-scissors"></i>
<i class="far fa-hand-paper paper"></i>
</div>
<div class="result">
<div class="box"></div>
</div>
<div class="user">
<i class="far fa-hand-rock"></i>
<i class="far fa-hand-scissors"></i>
<i class="far fa-hand-paper"></i>
</div>
</section>
</body>
</html>
let selection = ['scissors', 'rock', 'paper'];
let winSelection = {
scissors: 'paper',
paper: 'rock',
rock: 'scissors',
};
//computer turn
let i = 0;
let selected;
let computer = setInterval(() => {
selected = selection[i];
i = (i + 1) % 3;
}, 100);
function displayResult(message) {
let resultbox = document.querySelector('.box');
resultbox.style.opacity = '100';
resultbox.innerHTML = `
<span>${message}</span>
`;
setTimeout(() => {
resultbox.style.opacity = '0';
}, 1000);
}
//user event
let buttons = document.querySelector('.user');
buttons.addEventListener('click', (e) => {
clearInterval(computer);
console.log(e.target);
let userselected = e.target.className.split(' ')[1].split('-')[2];
console.log('computer', selected);
//색깔 초기화
let c = document.querySelectorAll('.computer i');
let u = document.querySelectorAll('.user i');
c.forEach((element) => {
element.style.color = 'black';
});
u.forEach((element) => {
element.style.color = 'black';
});
//해당 가위바위보 색깔 바꾸기
document.querySelector(`.computer .fa-hand-${selected}`).style.color =
'green';
//console.log('user', userselected);
document.querySelector(`.user .fa-hand-${userselected}`).style.color =
'green';
//결과 알려주기
if (userselected === selected) {
displayResult('비겼습니다');
} else if (winSelection[userselected] === selected) {
displayResult('이겼습니다');
} else {
displayResult('졌습니다');
}
//다시 게임 재개
computer = setInterval(() => {
selected = selection[i];
i = (i + 1) % 3;
}, 100);
});
리액트 생명주기
일반적으로 클래스에서 리액트의 생명주기라고 하면 다음과 같다.
Constructor => render => ref => ComponentDidMount => componentDidUpdate(SetState/props에 따른 업데이트) => ComponentShouldUpdate(업데이트 되지 않아야할 것들을 제어) => render => componentWillUnmount |
componentDidMount를 실행하는 것을 구독 설정,
componentWillUnmount하는 것을 구독 취소, 혹은 정리라고 한다.
특히 지금 가위바위보 게임에서는 componentDidMount와 componentWilUnmount가 필요하다.
게임을 실행하고 게임 컴포넌트를 종료할 때 필요한 작업이기 때문이다.
클래스로 해보면 다음과 같은 코드가 될 수 있을 것이다.
interval;
componentDidMount() {
this.interval = setInterval(this.changeHand, 100);
}
componentWillUnmount() {
clearInterval(this.interval);
}
이를 리액트로 바꿔보면 useEffect를 사용할 수 있고 아래의 코드가 될 수 있다.
useEffect(() => {
console.log('다시 실행');
interval.current = setInterval(changeHand, 100);
return () => {
console.log('종료');
clearInterval(interval.current);
}
}, []);
const changeHand = () => {
if (imgCoord === rspCoords.바위) {
setImgCoord(rspCoords.가위);
} else if (imgCoord === rspCoords.가위) {
setImgCoord(rspCoords.보);
} else if (imgCoord === rspCoords.보) {
setImgCoord(rspCoords.바위);
}
};
useEffect는 componentDidMount와 componentDidUpdate, componentWillUnmount
이 세 개를 모두 합친 개념인데 이 3가지와 완벽히 1대1 대응은 안 되지만
비슷하게 구현할 수 있다.
useEffect의 첫번째 인자인 콜백 함수 안에서
return 전까지의 코드는 componentDidMount,
return 되는 함수는 componentWillUnmount, 그리고
useffect의 두번째 인자인 배열이 변할 때 첫번째 인자인 콜백함수가 실행됨으로써
componentDidUpdate와 유사하게 만들어줄 수 있다.
그러나 함수형 컴포넌트는
"업데이트"를 기준으로 컴포넌트를 재생성, 재실행하기 때문에
만약 배열의 변화나 prevprops의 변화로
componentDidUpdate처럼 구현한 코드가 실행되어야할 상황이 발생하면
componentWillUnmount처럼 구현한 코드(return 하는 코드) 또한 실행하게 된다.
이게 무슨 소리일까'?
만약 위의 코드에서 useEffect 두번째 인자에 imgCoord를 추가했다고 생각해보자.
useEffect(() => {
console.log('다시 실행');
interval.current = setInterval(changeHand, 100);
return () => {
console.log('종료');
clearInterval(interval.current);
}
}, [imgCoord]);
const changeHand = () => {
if (imgCoord === rspCoords.바위) {
setImgCoord(rspCoords.가위);
} else if (imgCoord === rspCoords.가위) {
setImgCoord(rspCoords.보);
} else if (imgCoord === rspCoords.보) {
setImgCoord(rspCoords.바위);
}
};
imgCoord 값이 변화하면 업데이트가 된다.
클래스와 훅의 라이프사이클 비교
클래스로 만드는 경우 setInterval을 한 번 실행한 후,
컴포넌트가 DOM에 Unmount가 될 때
clearInterval이 될 것이라고 기대한다.
하지만 위의 훅 코드는
setInterval로 0.1초 간격으로 계속해서 실행되고 있는 것이 아니라
closeInterval이 바로 실행되고 다시 setInterval이 실행되고 있다.
사실상 setInterval이 아니라 setTimeout과 같이 작동하고 있는 것이다.
그 이유는 아래와 같다.
1.
우선 클래스 컴포넌트와 훅으로 만든 컴포넌트의 가장 큰 차이점은
클래스는 state가 바뀔 때마다 render함수를 다시 그려주는 반면,
함수형 컴포넌트는 새로 렌더를 해야하는 상황이면
해당 컴포넌트가 자체가 다시 처음부터 생성된다.
그리고 업데이트가 될 때마다
useEffect가 실행된다.
2.
useEffect는 componentWillUnmount와는 정리, 구독 취소의 시점이 다르다.
componentWillUnmount는 말 그대로 마운트 해제되는 때에 정리가 실행된다.
하지만 useEffect로 return 콜백함수는 렌더링이 실행될 때마다 실행된다.
따라서
위의 코드 useEffect에서 setInterval이 실행되면서
imgCoord가 바뀌고
imgCoord가 바뀌면서 다시 렌더링이 되야하기 때문에
구독, 취소 정리가 되어
return되는 함수인 안에 있는 clearInterval이 실행되고,
imgCoord가 바뀌었기 때문에 다시 useEffect가 실행되고
다시 setInterval이 작동한다
아까 말했던 것처럼 setInterval이 아니라 setTImeout처럼 작동하고 있다고 볼 수 있다.
아래에서
effect가 업데이트 시마다 실행되는 이유와
리액트가 effect를 정리(clean-up)하는 시점은 정확히 언제일까요?
를 읽어보면 더 이해가 잘 될 것 같다.
Using the Effect Hook – React
A JavaScript library for building user interfaces
ko.reactjs.org
'Front' 카테고리의 다른 글
CDN과 다이나믹 캐시 (0) | 2022.09.20 |
---|---|
postMessage (0) | 2022.09.01 |
Webpack과 Babel 기본 설정(2)- Create-react-app를 사용하지 않는다면! (4) | 2021.01.02 |
Webpack과 Babel 기본 설정(1)- Create-react-app를 사용하지 않는다면! (2) | 2021.01.01 |
React Framework를 사용하는 이유 (0) | 2020.12.13 |