컴퓨터와 가위바위보를 해서 점수를 누적해 나가는 게임이다.
1. Class
import React, { Component } from "react";
const rspCoords = {
바위: "0",
가위: "-142px",
보: "-284px",
};
const scores = {
가위: 1,
바위: 0,
보: -1,
};
const computerChoice = (imgCoord) => {
return Object.entries(rspCoords).find(function (v) {
return v[1] === imgCoord;
})[0];
};
class RSPClass extends Component {
state = {
result: "",
imgCoord: rspCoords.바위,
score: 0,
};
interval;
componentDidMount() {
this.interval = setInterval(this.changeHand, 100);
}
componentWillUnmount() {
clearInterval(this.interval);
}
changeHand = () => {
const { imgCoord } = this.state;
if (imgCoord === rspCoords.바위) {
this.setState({
imgCoord: rspCoords.가위,
});
} else if (imgCoord === rspCoords.가위) {
this.setState({
imgCoord: rspCoords.보,
});
} else if (imgCoord === rspCoords.보) {
this.setState({
imgCoord: rspCoords.바위,
});
}
};
onClickBtn = (choice) => {
const { imgCoord } = this.state;
clearInterval(this.interval);
const myScore = scores[choice];
const cpuScore = scores[computerChoice(imgCoord)];
const diff = myScore - cpuScore;
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);
}, 1000);
};
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 RSPClass;
클래스형 컴포넌트는 전체적으로 위와 같다. 가위, 바위, 보가 함께 있는 이미지를 사용하여 보여지는 부분을 바꿔 가위, 바위, 보가 번갈아 나오는 것처럼 만들었다.
const rspCoords = {
바위: "0",
가위: "-142px",
보: "-284px",
};
const scores = {
가위: 1,
바위: 0,
보: -1,
};
const computerChoice = (imgCoord) => {
return Object.entries(rspCoords).find(function (v) {
return v[1] === imgCoord;
})[0];
};
먼저 클래스 밖에 this와 무관한 값들을 정의한다. rspCoords는 바위, 가위, 보가 시작되는 좌표를 지정한다. 바위는 0에서 가위는 142px 지점에서, 보는 284px 지점에서 시작하여 사각형 모양으로 선택하여 화면에 표시할 것이다. scores는 가위에 1, 바위에 0, 보에 -1을 할당한다. 이를 계산하여 승패를 결정하는 로직을 만들 것이다. Object.entries() 메소드는, 매개변수로 전달받은 객체를 [[key1, value1], [key2, value2], [key3, value3] ... ] 형태로 바꿔준다. 여기서 find API를 이용해 1번째 인덱스, 즉 value의 값이 imgCoord와 일치하는 [key, value] 쌍을 찾고, 그 key값을 반환하도록 한다. imgCoord는 클래스 내부에서 정의할 것이다.
class RSPClass extends Component {
state = {
result: "",
imgCoord: rspCoords.바위,
score: 0,
};
interval;
componentDidMount() {
this.interval = setInterval(this.changeHand, 100);
}
componentWillUnmount() {
clearInterval(this.interval);
}
changeHand = () => {
const { imgCoord } = this.state;
if (imgCoord === rspCoords.바위) {
this.setState({
imgCoord: rspCoords.가위,
});
} else if (imgCoord === rspCoords.가위) {
this.setState({
imgCoord: rspCoords.보,
});
} else if (imgCoord === rspCoords.보) {
this.setState({
imgCoord: rspCoords.바위,
});
}
};
onClickBtn = (choice) => {
const { imgCoord } = this.state;
clearInterval(this.interval);
const myScore = scores[choice];
const cpuScore = scores[computerChoice(imgCoord)];
const diff = myScore - cpuScore;
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);
}, 1000);
};
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 RSPClass;
result, imgCoord, score를 state로 설정하고 각각 초기값을 입력한다. interval도 값이 자주 변하므로 렌더링과 무관하게 설정하기 위해 ref를 사용한다. 이후, 가위 바위 보가 번갈아가면서 나올 수 있도록 생명 주기 함수를 이용한다.
클래스형 컴포넌트의 생명 주기는 다음과 같다.
constructor
render
ref
componentDidMount
setState/props가 변경될 때: shouldComponentUpdate(true) > rerender
componentDidUpdate
부모 컴포넌트로부터 제거될 때: componentWillUnmount
소멸
componentDidMount는 render가 실행되면 react가 컴포넌트를 DOM에 붙여 주는데, 그 순간에 특정한 동작을 실행시켜 준다. 단, 최초로 render가 실행될 때만 실행되며, 이후 setState에 의해 render 함수가 재실행될 때는 동작하지 않는다. setInterval 등의 비동기 요청을 componentDidMount에서 처리한다.
componentDidUpdate는 render 함수가 재실행될 때 동작한다.
componentWillUnmount는 부모 컴포넌트에 의해 컴포넌트가 제거될 때 실행된다. clearInterval 등의 비동기 요청을 정리하는데 사용된다.
여기에서는 componentDidMount에 setInterval을 사용하여 0.1초마다 changeHand를 실행시키는 함수를, componentWillUnmount에 clearInterval로 Interval을 종료시켰다. 사실 여기서는 부모 컴포넌트가 없으므로 현재 코드에서 componentWillUnmount를 쓰는 것에 큰 의미는 없지만, 이 처리를 하지 않을 경우, 페이지를 이동해도 계속해서 setInterval이 작동하여 성능 저하를 가져올 수 있으므로 주의해야 한다.
changeHand = () => {
const { imgCoord } = this.state;
if (imgCoord === rspCoords.바위) {
this.setState({
imgCoord: rspCoords.가위,
});
} else if (imgCoord === rspCoords.가위) {
this.setState({
imgCoord: rspCoords.보,
});
} else if (imgCoord === rspCoords.보) {
this.setState({
imgCoord: rspCoords.바위,
});
}
};
render 함수 바깥에서 정의될 changeHand의 로직은 위와 같다. 이미지의 좌표가 바위일 때는 가위로, 가위일 때는 보로, 보일 때는 바위로 바꿔 주는 함수이다.
onClickBtn = (choice) => {
const { imgCoord } = this.state;
clearInterval(this.interval);
const myScore = scores[choice];
const cpuScore = scores[computerChoice(imgCoord)];
const diff = myScore - cpuScore;
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);
}, 1000);
};
또, render 바깥에서 정의되는 onClickBtn은, 먼저 clearInterval로 가위바위보의 변경을 멈춘다. 전달된 매개변수 choice에는 클릭된 버튼의 값인 "가위", "바위", "보" 중의 하나가 저장되어 있을 것이다. 이를 key로 하여, scores에서 value를 선택하여 myScore에 입력하고, 클래스 밖에서 정의한 computerChoice의 값을 key로 하여 scores에서 선택된 value를 cpuScore에 입력한다. myScore에서 cpuScore를 빼고, 그 값을 diff에 전달한 뒤, 이 값에 따라 승패를 결정한다. diff가 0이라면, myScore와 cpuScore는 같을 것이므로 비겼다는 결과를 출력한다. -1이나 2면 내가 이기는 경우이므로 점수에 1을 더하고, 그렇지 않은 경우는 1을 뺀다. 이후, 1초 뒤에 타임아웃을 걸어 다시 changeHand가 0.1초 단위로 실행되도록 한다.
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>
</>
);
}
render 함수 부분은 위와 같이 JSX로 구성해 두었다.
2. Hooks
import React, { useState, useRef, useEffect } from "react";
const rspCoords = {
바위: "0",
가위: "-142px",
보: "-284px",
};
const scores = {
가위: 1,
바위: 0,
보: -1,
};
const computerChoice = (imgCoord) => {
return Object.entries(rspCoords).find(function (v) {
return v[1] === imgCoord;
})[0];
};
const RSPHooks = () => {
const [result, setResult] = useState("");
const [imgCoord, setImgCoord] = useState(rspCoords.바위);
const [score, setScore] = useState(0);
const interval = useRef();
useEffect(() => {
interval.current = setInterval(changeHand, 100);
return () => {
clearInterval(interval.current);
};
}, [imgCoord]);
const changeHand = () => {
if (imgCoord === rspCoords.바위) {
setImgCoord(rspCoords.가위);
} else if (imgCoord === rspCoords.가위) {
setImgCoord(rspCoords.보);
} else if (imgCoord === rspCoords.보) {
setImgCoord(rspCoords.바위);
}
};
const onClickBtn = (choice) => () => {
clearInterval(interval.current);
const myScore = scores[choice];
const cpuScore = scores[computerChoice(imgCoord)];
const diff = myScore - cpuScore;
if (diff === 0) {
setResult("비겼습니다.");
} else if ([-1, 2].includes(diff)) {
setResult("이겼습니다!");
setScore((prevScore) => prevScore + 1);
} else {
setResult("졌습니다...");
setScore((prevScore) => prevScore - 1);
}
setTimeout(() => {
interval.current = setInterval(changeHand, 100);
}, 1000);
};
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 RSPHooks;
Hooks로 고치면 위와 같다. 클래스 밖에서 정의한 변수화 함수들은 그대로 두고, state도 객체 형태로 있던 것을 구조 분해 할당으로 각각 정의한다. 값이 변해도 render되지 않게 하기 위해 useRef로 interval을 정의한다.
useEffect(() => {
interval.current = setInterval(changeHand, 100);
return () => {
clearInterval(interval.current);
};
}, [imgCoord]);
Hooks에는 생명주기 함수가 없으나, 그 대신 useEffect로 비슷한 효과를 줄 수 있다. 단, 생명주기 함수와 일대일로 대응되지는 않는다. useEffect는 componentDidMount, componentDidUpdate, componenetWillUnmount의 기능을 합친 것과 비슷한 효과를 주는데, 함수의 내용 부분에서 componentDidMount, componentDidUpdate와 같은 시기에, 즉, 컴포넌트가 마운트되고 업데이트될 때(render될 때) 해당 함수의 내용이 실행된다. 또한, useEffect의 return 부분에서는 componentWillUnmount의 역할을 수행한다. useEffect의 두 번째 매개변수로는 배열을 전달하는데, 이 배열에는 useEffect로 다루고 싶은 state 값이 요소로 들어간다. 즉, 배열에 담긴 요소의 값이 변할 때 useEffect가 실행된다. 이는 배열이므로 여러 개의 state를 한 개의 useEffect에서 다룰 수 있으며, useEffect 역시 여러 개를 만들 수 있다.
클래스형 컴포넌트의 생명 주기 함수인 componentDidMount, componentDidUpdate, componentWillUnmout는 특정 타이밍에 다루고 싶은 state를 모두 다룰 수 있으나, Hooks의 useEffect는, 타이밍과 관계없이 배열에 전달된 state들만을 다룰 수 있다는 점이 주된 차이점이다.
const onClickBtn = (choice) => () => {
clearInterval(interval.current);
const myScore = scores[choice];
const cpuScore = scores[computerChoice(imgCoord)];
const diff = myScore - cpuScore;
if (diff === 0) {
setResult("비겼습니다.");
} else if ([-1, 2].includes(diff)) {
setResult("이겼습니다!");
setScore((prevScore) => prevScore + 1);
} else {
setResult("졌습니다...");
setScore((prevScore) => prevScore - 1);
}
setTimeout(() => {
interval.current = setInterval(changeHand, 100);
}, 1000);
};
또한, 리액트에서 자주 쓰는 패턴 중 하나로 위와 같은 형태로 화살표 함수를 연속해서 쓸 수 있는데, 이를 고차함수, 고위함수(Higher-Order Function)라고 한다. JSX를 다음과 같이 바꾸면 위와 같이 고차함수를 사용할 수 있다.
onClick={() => onClickBtn("가위")}
onClick={onClickBtn("가위")}
[React.js] React Web Games - Lotto (Class & Hooks) (0) | 2021.08.01 |
---|---|
[React.js] React Web Games - Response Check (Class & Hooks) (0) | 2021.07.25 |
[React.js] React Web Games - Number Baseball (Class & Hooks) (0) | 2021.07.23 |
[React.js] React Web Games - Multiplication Game (Webpack) (0) | 2021.07.19 |
[React.js] React Web Games - Multiplication Game (Functional Component) (0) | 2021.07.19 |
댓글 영역