#5 useEffect, 라이프사이클 / 가위바위보 게임

sham·2021년 8월 22일
0

인프런에 올라간 제로초님의 강의를 보고 정리한 내용입니다.
https://www.inflearn.com/course/web-game-react


코드

클래스 버전

ResponseCheck.jsx

import React, {Component} from 'react';

const rspCoord = {
	바위 : "0",
	가위 : "-142px",: '-284px',
}

const scores = {
	가위 : 1,
	바위 : 0,: -1,
}

const computerChoice = (imgCoord) => {
	
	return Object.entries(rspCoord).find((v) => {
		return v[1] === imgCoord;
	})[0];
}

class RSP extends Component {

	state = {
		result : "1",
		imgCoord : "0",
		score : 0,
	}
	interval;
	
	componentDidMount()
	{
		this.interval = setInterval(this.changeHand, 100);

	}
	componentDidUpdate()
	{
		console.log("componentDidUpdate");
	} 
	componentWillUnmount()
	{
		console.log("componentWillUnmount");
		clearInterval(this.interval);

	}
	changeHand = () => {
		{
			const {imgCoord} = this.state;	
			if (imgCoord === rspCoord.바위) {
				this.setState({
					imgCoord : rspCoord.가위
				});
			} else if (imgCoord === rspCoord.가위) {
				this.setState({
					imgCoord : rspCoord.});
			} else if (imgCoord === rspCoord.) {
					this.setState({
						imgCoord : rspCoord.바위
					});
			}
		}
	}
	onClickBtn = (choice) => (e) => {
		console.log(e);
		clearInterval(this.interval);
		const myScore = scores[choice];
		const cquScore = scores[computerChoice(this.state.imgCoord)];
		const diff = myScore - cquScore;
		if (diff === 0) {
			this.setState({
				result : "비겼습니다."
			})
		} else if ([-1, 2].includes(diff)) {
			this.setState((prevState) => {
				return {
					result : "이겼습니다!",
					score : prevState.score + 1,
				}
			})
		} else {
			this.setState((prevState) => {
				return {
					result : "졌습니다...",
					score : prevState.score - 1,
				}
			})
		}
		setTimeout(() => {
			this.interval = setInterval(this.changeHand, 100);
		}, 2000);
	}
	render() {
		const {result, score, imgCoord } = this.state;
		console.log("rendering");
		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={this.onClickBtn('바위')}>바위</button>
			  <button id="scissor" className="btn" onClick={this.onClickBtn('가위')}>가위</button>
			  <button id="paper" className="btn" onClick={this.onClickBtn('보')}></button>
			</div>
			<div>{result}</div>
			<div>현재 {score}</div>
		  </>
		)
	}
}

export default RSP;

함수 버전

ResponseCheck.jsx

import React, {useRef, useState, useEffect, memo} from 'react';

const rspCoord = {
	바위 : "0",
	가위 : "-142px",: '-284px',
}

const scores = {
	가위 : 1,
	바위 : 0,: -1,
}

const computerChoice = (imgCoord) => {
	
	return Object.entries(rspCoord).find((v) => {
		return v[1] === imgCoord;
	})[0];
}

const RSP = () => {
	const [result, setResult] = useState("");
	const [imgCoord, setImgCoord] = useState("0");
	const [score, setScore] = useState(0);
	const interval = useRef(null);
	
	useEffect(() => { //componentDidMount, componentDidUpdate 역할
		interval.current = setInterval(changeHand, 300);
		return () => { // componentWillUnmount 역할
			clearInterval(interval.current);
		}		
	}, [imgCoord]);

	const changeHand = () => {
			if (imgCoord === rspCoord.바위) {
				setImgCoord(rspCoord.가위);
			} else if (imgCoord === rspCoord.가위) {
				setImgCoord(rspCoord.);
			} else if (imgCoord === rspCoord.) {
				setImgCoord(rspCoord.바위);
			}
	}
	const onClickBtn = (choice) => (e) => {
		console.log(e);
		clearInterval(interval.current);
		const myScore = scores[choice];
		const cquScore = scores[computerChoice(imgCoord)];
		const diff = myScore - cquScore;
		if (diff === 0) {
			setResult("비겼습니다.");
		} else if ([-1, 2].includes(diff)) {
			setResult("이겼습니다!");
			setScore((prevScore) => {
				return (prevScore + 1);
			})
		} else {
			setResult("졌습니다...");
			setScore((prevScore) => {
				return (prevScore - 1);
			})
		}
		setTimeout(() => {
			interval.current = setInterval(changeHand, 100);
		}, 2000);
	}
		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;

#5-1 라이프사이클

맨 처음은 그냥 컴포넌트를 만든 후, 성능 상으로 문제가 있을 때 PureComponent를 생각하는 것이 좋다.

import React, {Component, PureComponent, useRef, useState, memo, createRef} from 'react';

class RSP extends Component {

	state = {
		result : "",
		imgCoord : 0,
		score : 0,
	}
	render() {
		const {result, score, imgCoord } = this.state;
		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={this.onClickBtn('바위')}>바위</button>
			  <button id="scissor" className="btn" onClick={this.onClickBtn('가위')}>가위</button>
			  <button id="paper" className="btn" onClick={this.onClickBtn('보')}></button>
			</div>
			<div>{result}</div>
			<div>현재 {score}</div>
		  </>
		)
	}
}

export default RSP;

기본 코드 구조.

인자로 들어가는 imgCoord는 해당 이미지의 좌표를 지정하는 역할을 하게 된다.

라이프사이클

extend된 컴포넌트가 client.jsx 파일에서 렌더링 되어 DOM에 달라 붙는 순간, 특정한 동작을 할 수 있다.

컴포넌트가 생기고 사라지는 동안의 순간순간에 컴포넌트를 관리할 수 있다.

shouldComponentUpdate도 라이프사이클 중 하나다.

componentDidMount()

render()가 처음 실행되어 성공적으로 실행되었다면 실행된다.

리렌더링이 일어났을 때는 실행되지 않는다

비동기 요청이 많이 일어난다.

shouldComponentUpdate(nextProps, nextState, nextContext)

값이 변했다면 return true,

변하지 않는다면 return false

componentDidUpdate()

리렌더링 후에 실행된다.

componentWillUnmount()

부모 컴포넌트에 의해 제거되기 직전 실행된다.

componentDidMount에서 실행되고 있는 비동기 요청 정리가 많이 일어난다.

라이프사이클의 흐름

클래스의 경우

생성(렌더링) : constructor → render → ref → componentDidMount

변경 : setState/props 변경 → shouldComponentUpdate(true) → render -> componentDidUpdate

삭제 : componentWillUnmount → 소멸


#5-2 setInterval과 라이프사이클 연동하기

import React, {Component, PureComponent, useRef, useState, memo, createRef} from 'react';

class RSP extends Component {

	state = {
		result : "1",
		imgCoord : 0,
		score : 0,
	}
	interval;
	componentDidMount()
	{
		console.log("componentDidMount");
		this.interval = setInterval(() => {
			console.log("roop");
		}, 1000);

	}
	componentDidUpdate()
	{
		console.log("componentDidUpdate");
	} 
	componentWillUnmount()
	{
		console.log("componentWillUnmount");
		clearInterval(this.interval);

	}
	onClickBtn = () => {
	}
	render() {
		const {result, score, imgCoord } = this.state;
		console.log("rendering");
		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={this.onClickBtn('바위')}>바위</button>
			  <button id="scissor" className="btn" onClick={this.onClickBtn('가위')}>가위</button>
			  <button id="paper" className="btn" onClick={this.onClickBtn('보')}></button>
			</div>
			<div>{result}</div>
			<div>현재 {score}</div>
		  </>
		)
	}
}

export default RSP;

componentDidMount()에 setInterval을 설정해주었다. 1000 밀리초 주기로 함수의 내용을 무한반복한다.

따로 처리를 해주지 않으면 프로그램이 꺼져도, 창이 새로고침 되도 setInterval은 계속해서 돌아간다. 쓸데없는 메모리가 새는 셈이다.

clearInterval로 작동을 멈출 수 있다. class 객체에 setInterval을 할당하고 componentWillUnmount에서 해당 반복을 멈춘다.

import React, {Component, PureComponent, useRef, useState, memo, createRef} from 'react';

const rspCoord = {
	가위 : "0",
	바위 : "-142px",: '-284px',
}

const score = {
	가위 : "1",: "0",: "-1",
}

class RSP extends Component {

	state = {
		result : "1",
		imgCoord : "0",
		score : 0,
	}
	interval;
	componentDidMount()
	{
		console.log("check");

		this.interval = setInterval(() => {
			const {imgCoord} = this.state;
			console.log(imgCoord);
	
			if (imgCoord === rspCoord.바위) {
				this.setState({
					imgCoord : rspCoord.가위
				});
			} else if (imgCoord === rspCoord.가위) {
				this.setState({
					imgCoord : rspCoord.});
			} else if (imgCoord === rspCoord.) {
					this.setState({
						imgCoord : rspCoord.바위
					});
			}
		}, 2000);

	}
	componentDidUpdate()
	{
		console.log("componentDidUpdate");
	} 
	componentWillUnmount()
	{
		console.log("componentWillUnmount");
		clearInterval(this.interval);

	}
	onClickBtn = () => {

	}
	render() {
		const {result, score, imgCoord } = this.state;
		console.log("rendering");
		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={this.onClickBtn('바위')}>바위</button>
			  <button id="scissor" className="btn" onClick={this.onClickBtn('가위')}>가위</button>
			  <button id="paper" className="btn" onClick={this.onClickBtn('보')}></button>
			</div>
			<div>{result}</div>
			<div>현재 {score}</div>
		  </>
		)
	}
}

export default RSP;

setInterval로 비동기로 가위바위보를 반복하는 함수를 실행하게 한다.

비동기에서 함수 외부의 변수를 참조하면 클로저 문제가 발생한다.


#5-3 가위바위보 게임 만들기

import React, {Component, PureComponent, useRef, useState, memo, createRef} from 'react';

const rspCoord = {
	바위 : "0",
	가위 : "-142px",: '-284px',
}

const scores = {
	가위 : 1,
	바위 : 0,: -1,
}

const computerChoice = (imgCoord) => {
	
	return Object.entries(rspCoord).find((v) => {
		return v[1] === imgCoord;
	})[0];
}

class RSP extends Component {

	state = {
		result : "1",
		imgCoord : "0",
		score : 0,
	}
	interval;
	
	componentDidMount()
	{
		this.interval = setInterval(() => this.changeHand(this.state.imgCoord), 100);

	}
	componentDidUpdate()
	{
		console.log("componentDidUpdate");
	} 
	componentWillUnmount()
	{
		console.log("componentWillUnmount");
		clearInterval(this.interval);

	}
	changeHand = (imgCoord) => {
		{
			console.log(imgCoord);
	
			if (imgCoord === rspCoord.바위) {
				this.setState({
					imgCoord : rspCoord.가위
				});
			} else if (imgCoord === rspCoord.가위) {
				this.setState({
					imgCoord : rspCoord.});
			} else if (imgCoord === rspCoord.) {
					this.setState({
						imgCoord : rspCoord.바위
					});
			}
		}
	}
	onClickBtn = (choice) => {
		clearInterval(this.interval);
		const myScore = scores[choice];
		const cquScore = scores[computerChoice(this.state.imgCoord)];
		const diff = myScore - cquScore;
		if (diff === 0) {
			this.setState({
				result : "비겼습니다."
			})
		} else if ([-1, 2].includes(diff)) {
			this.setState((prevState) => {
				return {
					result : "이겼습니다!",
					score : prevState.score + 1,
				}
			})
		} else {
			this.setState((prevState) => {
				return {
					result : "졌습니다...",
					score : prevState.score - 1,
				}
			})
		}
		setTimeout(() => {
			this.interval = setInterval(() => this.changeHand(this.state.imgCoord), 100);
		}, 2000);
	}
	render() {
		const {result, score, imgCoord } = this.state;
		console.log("rendering");
		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={() => this.onClickBtn('바위')}>바위</button>
			  <button id="scissor" className="btn" onClick={() => this.onClickBtn('가위')}>가위</button>
			  <button id="paper" className="btn" onClick={() => this.onClickBtn('보')}></button>
			</div>
			<div>{result}</div>
			<div>현재 {score}</div>
		  </>
		)
	}
}

export default RSP;

사용자가 버튼을 누름 → Interval 멈추고 → 점수, 결과 계산 → 다시 Interval

setTimeout으로 2초가 지난 후 setInterval을 실행하게끔.

this.interval = setInterval(() => this.changeHand(this.state.imgCoord), 100);
부분에서 imgCoord 를 인자로 주게 변형해보았음. 문제없이 작동.


#5-4. 고차 함수와 Q&A

유용한 패턴(고차 함수)

onClickBtn = (choice) => (e) => {
		console.log(e);
		clearInterval(this.interval);
		const myScore = scores[choice];
		const cquScore = scores[computerChoice(this.state.imgCoord)];
		const diff = myScore - cquScore;
		if (diff === 0) {
			this.setState({
				result : "비겼습니다."
			})
		} else if ([-1, 2].includes(diff)) {
			this.setState((prevState) => {
				return {
					result : "이겼습니다!",
					score : prevState.score + 1,
				}
			})
		} else {
			this.setState((prevState) => {
				return {
					result : "졌습니다...",
					score : prevState.score - 1,
				}
			})
		}
		setTimeout(() => {
			this.interval = setInterval(this.changeHand, 100);
		}, 2000);
	}
	render() {
		const {result, score, imgCoord } = this.state;
		console.log("rendering");
		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={this.onClickBtn('바위')}>바위</button>
			  <button id="scissor" className="btn" onClick={this.onClickBtn('가위')}>가위</button>
			  <button id="paper" className="btn" onClick={this.onClickBtn('보')}></button>
			</div>
			<div>{result}</div>
			<div>현재 {score}</div>
		  </>
		)
	}

render에서 메서드를 호출할 때 쓰는 화살표 함수를 호출할 함수의 선언 부분에 넣어줄 수 있다. (순서가 중요!)

위 코드에서 e는 button 태그에 대한 정보를 가지고 있다.

hooks의 라이프사이클

hooks에는 이러한 라이프사이클이 존재하지 않는다.

setState를 연달아 쓸 때?

연속된 setState가 있다면 리액트가 알아서 모아서 처리를 한꺼번에 한다.


#5-5. Hooks와 useEffect

import React, {useRef, useState, useEffect, memo} from 'react';

const rspCoord = {
	바위 : "0",
	가위 : "-142px",: '-284px',
}

const scores = {
	가위 : 1,
	바위 : 0,: -1,
}

const computerChoice = (imgCoord) => {
	
	return Object.entries(rspCoord).find((v) => {
		return v[1] === imgCoord;
	})[0];
}

const RSP = () => {
	const [result, setResult] = useState("");
	const [imgCoord, setImgCoord] = useState("0");
	const [score, setScore] = useState(0);
	const interval = useRef(null);
	
	useEffect(() => { //componentDidMount, componentDidUpdate 역할
		interval.current = setInterval(changeHand, 300);
		return () => { // componentWillUnmount 역할
			clearInterval(interval.current);
		}		
	}, [imgCoord]);

	const changeHand = () => {
			if (imgCoord === rspCoord.바위) {
				setImgCoord(rspCoord.가위);
			} else if (imgCoord === rspCoord.가위) {
				setImgCoord(rspCoord.);
			} else if (imgCoord === rspCoord.) {
				setImgCoord(rspCoord.바위);
			}
	}
	const onClickBtn = (choice) => (e) => {
		console.log(e);
		clearInterval(interval.current);
		const myScore = scores[choice];
		const cquScore = scores[computerChoice(imgCoord)];
		const diff = myScore - cquScore;
		if (diff === 0) {
			setResult("비겼습니다.");
		} else if ([-1, 2].includes(diff)) {
			setResult("이겼습니다!");
			setScore((prevScore) => {
				return (prevScore + 1);
			})
		} else {
			setResult("졌습니다...");
			setScore((prevScore) => {
				return (prevScore - 1);
			})
		}
		setTimeout(() => {
			interval.current = setInterval(changeHand, 100);
		}, 2000);
	}
		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;

hooks에 라이프사이클이 존재하지는 않지만, useEffect를 이용해서 흉내내는 것은 가능하다.

useEffect()

useRef나 useState처럼 함수 컴포넌트 안에 적어야 한다.

componentDidMount, componentDidUpdate, componentWillUnmount와 일대일 대응을 하지는 않지만, 세개를 전부 합친 것과 같은 역할을 한다.

useEffect(() => { //componentDidMount, componentDidUpdate 역할
		console.log("다시 실행");
		interval.current = setInterval(changeHand, 300);
		return () => { // componentWillUnmount 역할
			console.log("종료");
			clearInterval(interval.current);
		}		
	}, [imgCoord]);

첫 번째 인자는 실행할 함수, 두 번째 인자(배열)는 바뀌게 되는 state를 배열에 집어넣는다.

코드를 실행하면 setInterval인데도 불구하고 useEffect 함수를 실행하고 리턴하는 것을 반복하는 것을 알 수 있다.

두 번째 인자에 넣어준 state가 바뀔 때마다 useEffect가 계속 실행된다.

  • 두 번째 인자를 비워놓으면 무엇이 바뀌든 딱 한 번만 실행하게 된다.

setInterval을 해주었다고 해도 곧바로 clearInterval을 하기 때문에 사실상 setTimeout을 무한반복해주는 것이라고 봐도 무방하다.

  • state가 바뀔 때마다 함수 전체가 실행되서 렌더링된다는 hooks의 특징과 연결되는 것인가? 비단 useEffect 뿐만 아니라 함수 전체가 재실행되기 때문?

#5-6 클래스와 Hooks 라이프사이클 비교

클래스

componentDidMount() {
	this.setState({
		imgCoord : 1,
		score : 2,
		result : 3,
})
}

componentDidMount(), componentDidUpdate(), componentWillUnmount() 등 정해진 메서드에서만 처리할 수 있다.

각각의 라이프사이클에서 모든 state를 담당한다.

하나의 라이프사이클 당 모든 state

Hooks(함수)

useEffect(() => {
	setImgCoord();
	setScore();
	}		
}, [imgCoord, score]);
useEffect(() => {
	setImgCoord();
	setResult();
	}		
}, [imgCoord, result]);

UseEffect를 여러 번 쓰는 게 가능하다. 즉 state 마다 각기 다른 처리를 해줄 수 있다.

  • 단, 두번째 인자인 배열 내부에는 반드시 useEffect를 실행할 때마다 값이 바뀔 state를 넣어주어야 한다.

개별의 state에 모든 라이프사이클을 설정할 수 있다.

개별 혹은 다수의 state 당 모든 라이프사이클

UseLayoutEffect

화면이 완전히 바뀌고 난 다음(리사이징 된 다음)에 실행되는 useEffect와 달리 화면을 키우고 줄이는 등 레이아웃에 대한 변화를 감지할 수 있다.

profile
씨앗 개발자

0개의 댓글

관련 채용 정보