1. Number Baseball
무작위로 주어진 네 개의 숫자를 순서를 포함하여 맞추는 게임이다. 숫자의 자리와 값을 맞추면 스트라이크(?), 값은 맞췄으나 자리가 틀린 경우에는 볼이 된다. 총 10회의 기회 안에 맞추면 된다.
2. NumberBaseball.jsx
import React, { Component } from "react";
import Try from "./Try";
function getNumbers() {
const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i++) {
const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(chosen);
}
console.log(array);
return array;
}
class NumberBaseball extends Component {
state = {
result: "",
value: "",
answer: getNumbers(),
tries: [],
};
onSubmitForm = (e) => {
e.preventDefault();
if (this.state.value === this.state.answer.join("")) {
this.setState({
result: "홈런!",
tries: [
...this.state.tries,
{ try: this.state.value, result: "홈런!" },
],
});
alert("게임을 다시 시작합니다!");
this.setState({
value: "",
answer: getNumbers(),
tries: [],
});
} else {
const answerArray = this.state.value.split("").map((v) => parseInt(v));
let strike = 0;
let ball = 0;
if (this.state.tries.length >= 9) {
this.setState({
result: `10 번 넘게 틀려서 실패! 답은 ${this.state.answer.join(
","
)} 였습니다!`,
});
alert("게임을 다시 시작합니다!");
this.setState({
value: "",
answer: getNumbers(),
tries: [],
});
} else {
for (let i = 0; i < 4; i += 1) {
if (answerArray[i] === this.state.answer[i]) {
strike += 1;
} else if (this.state.answer.includes(answerArray[i])) {
ball += 1;
}
}
this.setState({
tries: [
...this.state.tries,
{
try: this.state.value,
result: `${strike} 스트라이크, ${ball} 볼입니다.`,
},
],
value: "",
});
}
}
};
onChangeInput = (e) => {
this.setState({
value: e.target.value,
});
};
render() {
const { result, value, tries } = this.state;
return (
<>
<h1>{result}</h1>
<form onSubmit={this.onSubmitForm}>
<input maxLength={4} value={value} onChange={this.onChangeInput} />
</form>
<div>시도: {tries.length}</div>
<ul>
{tries.map((v, i) => (
<Try key={`${i + 1}차 시도: `} tryInfo={v} />
))}
</ul>
</>
);
}
}
export default NumberBaseball;
클래스의 전체 내용은 위와 같다.
function getNumbers() {
const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i++) {
const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(chosen);
}
console.log(array);
return array;
}
겹치지 않는 숫자 4개를 무작위로 선택하는 함수이다. 이 함수에는 this를 사용하지 않았으므로, 컴포넌트 밖에서 정의할 수 있다. candidate에 후보가 될 숫자들을 입력해 두고, 빈 배열인 array를 선언한다. 무작위로 골라진 숫자는 array에 들어갈 것이다. candidate에 있는 숫자를 무작위로 골라내는 작업은, splice로 한 칸을 잘라내는 방식으로 작성하고, array에 4개가 들어갈 때까지 반복할 것이다.
class NumberBaseball extends Component {
state = {
result: "",
value: "",
answer: getNumbers(),
tries: [],
};
onSubmitForm = (e) => {
e.preventDefault();
if (this.state.value === this.state.answer.join("")) {
this.setState({
result: "홈런!",
tries: [
...this.state.tries,
{ try: this.state.value, result: "홈런!" },
],
});
alert("게임을 다시 시작합니다!");
this.setState({
value: "",
answer: getNumbers(),
tries: [],
});
} else {
const answerArray = this.state.value.split("").map((v) => parseInt(v));
let strike = 0;
let ball = 0;
if (this.state.tries.length >= 9) {
this.setState({
result: `10 번 넘게 틀려서 실패! 답은 ${this.state.answer.join(
","
)} 였습니다!`,
});
alert("게임을 다시 시작합니다!");
this.setState({
value: "",
answer: getNumbers(),
tries: [],
});
} else {
for (let i = 0; i < 4; i += 1) {
if (answerArray[i] === this.state.answer[i]) {
strike += 1;
} else if (this.state.answer.includes(answerArray[i])) {
ball += 1;
}
}
this.setState({
tries: [
...this.state.tries,
{
try: this.state.value,
result: `${strike} 스트라이크, ${ball} 볼입니다.`,
},
],
value: "",
});
}
}
};
onChangeInput = (e) => {
this.setState({
value: e.target.value,
});
};
render() {
const { result, value, tries } = this.state;
return (
<>
<h1>{result}</h1>
<form onSubmit={this.onSubmitForm}>
<input maxLength={4} value={value} onChange={this.onChangeInput} />
</form>
<div>시도: {tries.length}</div>
<ul>
{tries.map((v, i) => (
<Try key={`${i + 1}차 시도: `} tryInfo={v} />
))}
</ul>
</>
);
}
}
export default NumberBaseball;
클래스 내부에서는 가장 먼저 state를 설정했다. result(정답 여부), value(사용자가 입력한 숫자)를 빈 문자열로, answer(정답)은 앞서 설정한 getNumbers로 무작위 4개의 숫자를 담은 배열로, tries(시도)는 빈 배열로 설정했다.
다음으로 onSubmitForm 함수를 설정한다. 먼저 새로고침을 막기 위해 preventDefault를 걸고, 이후 사용자가 입력한 value가 answer과 같은지를 확인한다. 같다면 result의 값을 홈런으로 바꿔준다. tries에도 기존의 tries(이전 시도까지 업데이트된 tries)에, 객체 형태로 try와 result를 추가한다. 이 때, try의 값은 사용자가 입력한 네 개의 숫자, result의 값은 홈런이 된다. 이후 게임을 다시 시작한다는 알람을 띄우고, value, answer, tries를 초기화한다. 다음 else문, 즉 value와 answer가 다른 경우에는 시도가 다음 번으로 넘어가야 한다. 따라서 answerArray라는 배열을 만들고, 여기에 value에 속한 각각의 문자열 형태의 숫자들을 정수로 바꿔 준다. 또, strike와 ball을 체크하기 위해 각각 변수를 선언하고, tries의 길이가 9보다 크거나 같은 경우, 즉 10회 이후로는 게임에서 패배한 것으로 간주하기 위해 result로 정답을 공개하고 게임을 다시 시작하기 위해 변수들을 초기화한다. 10회 이전에는 answerArray의 모든 원소가 각각 answer과 같은지를 확인하여 같다면 strike에 1을 더하고, answerArray의 모든 각각의 원소를 answer가 포함하고 있다면 ball에 1을 더한 뒤, 스트라이크와 볼을 표시해 준다. 이렇게 해서, 네 숫자의 값만 같다면 ball, 자리까지 같다면 strike가 1씩 증가하며, 네 숫자의 값과 자리가 모두 정답과 같다면 홈런이 되는 로직이 완성된다.
onChangeInput은 value의 값을 사용자가 변경할 수 있게 하기 위해 만들어 두었다.
참고로, 리액트에서 화살표 함수를 이용하는 이유는 this 때문이다. 화살표 함수를 쓰지 않으면 this가 window를 가리키기 때문에, 화살표 함수를 쓰거나, 혹은 constructor에서 함수를 bind해야 한다. 이제는 bind를 거의 쓰지 않으므로 화살표 함수를 쓰는 것이 좋다.
또, render() 함수 내에서 setState를 쓰면, setState가 다시 render를 호출하게 되므로 무한 재귀 현상이 벌어지므로 주의해야 한다.
render() {
const { result, value, tries } = this.state;
return (
<>
<h1>{result}</h1>
<form onSubmit={this.onSubmitForm}>
<input maxLength={4} value={value} onChange={this.onChangeInput} />
</form>
<div>시도: {tries.length}</div>
<ul>
{tries.map((v, i) => (
<Try key={`${i + 1}차 시도: `} tryInfo={v} />
))}
</ul>
</>
);
}
render 함수 부분만 떼어서 보면 위와 같다. 구조 분해 할당으로 result, value, tries를 this.state 형태로 바꿔 주었다. 또, h1 태그로 result를 보여주고, form 태그에 값이 제출될 때 onSubmitForm이 실행되도록 했다. 또, input의 maxLength는 4로 정해 두고, value는 입력되는 값을 받도록 했다. 시도 횟수를 나타내는 div 태그를 표시하고, tries 배열에 map을 적용하여, 모든 try에 대해 Try 컴포넌트를 실행하도록 했다. Try 컴포넌트에는 key와 tryInfo 프롭스를 전달한다. 참고로, map을 쓸 때에는 반드시 프롭스에 고유한 key 값을 전달해야 한다. 여기서 key는 i+1을 사용했으나, 일반적인 경우, key 값으로 index가 들어가게 되면 성능을 최적화할 수 없다.
3. Try.jsx
import React, { Component } from "react";
class Try extends Component {
render() {
const { tryInfo } = this.props;
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
}
}
export default Try;
자식 컴포넌트인 Try는 위와 같다. 전달받은 props인 tryInfo를 this.props 형태로 만든다. tryInfo의 값인 v에는, tries 배열의 각각의 원소가 { try: this.state.value, result: `${strike} 스트라이크, ${ball} 볼입니다.`, } 형태로 존재할 것이다. 이것을 표시해 주기 위해 div 태그에서 각각 출력되도록 한다.
4. createRef
inputRef = createRef();
Class형 컴포넌트와 Hooks에서 Ref를 쓰는 방식이 약간 다른데, 이를 조금이나마 통일하기 위해 Class형 컴포넌트에서도 Hooks처럼 current를 사용해 Ref를 쓸 수 있다. 바로 createRef를 쓰는 것인데, createRef를 import하고, ref에 createRef()를 실행시킨다.
this.inputRef.current.focus();
이 과정을 거친 후로는 위와 같이 클래스형 컴포넌트에서도 current를 붙여 Hooks와 비슷하게 사용할 수 있다.
5. to Hooks - NumberBaseballHooks.jsx
import React, { Fragment, useState } from "react";
import TryHooks from "./TryHooks";
function getNumbers() {
const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i++) {
const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(chosen);
}
console.log(array);
return array;
}
const NumberBaseballHooks = () => {
const [result, setResult] = useState("");
const [value, setValue] = useState("");
const [answer, setAnswer] = useState(getNumbers());
const [tries, setTries] = useState([]);
const onSubmitForm = (e) => {
e.preventDefault();
if (value === answer.join("")) {
setResult("홈런!");
setTries((prevTries) => {
return [...prevTries, { try: value, result: "홈런!" }];
});
alert("게임을 다시 시작합니다!");
setValue("");
setAnswer(getNumbers());
setTries([]);
} else {
const answerArray = value.split("").map((v) => parseInt(v));
let strike = 0;
let ball = 0;
if (tries.length >= 9) {
setResult(`10 번 넘게 틀려서 실패! 답은 ${answer.join(",")} 였습니다!`);
alert("게임을 다시 시작합니다!");
setValue("");
setAnswer(getNumbers());
setTries([]);
} else {
for (let i = 0; i < 4; i += 1) {
if (answerArray[i] === answer[i]) {
strike += 1;
} else if (answer.includes(answerArray[i])) {
ball += 1;
}
}
setTries((prevTries) => [
...prevTries,
{ try: value, result: `${strike} 스트라이크, ${ball} 볼입니다.` },
]);
setValue("");
}
}
};
const onChangeInput = (e) => {
setValue(e.target.value);
};
return (
<Fragment>
<h1>{result}</h1>
<form onSubmit={onSubmitForm}>
<input maxLength={4} value={value} onChange={onChangeInput} />
</form>
<div>시도: {tries.length}</div>
<ul>
{tries.map((v, i) => (
<TryHooks key={`${i + 1}차 시도: `} tryInfo={v} />
))}
</ul>
</Fragment>
);
};
export default NumberBaseballHooks;
getNumbers는 Class 밖에서 정의했으므로 그대로 가져다 쓰면 된다.
NumberBaseballHooks는 화살표 함수 형태로 바꿔 주고, state 역시 각각 나누어 useState로 새롭게 정의한다.
또, 클래스 내부에서 정의되었던 함수인 onSubmitForm, onChangeInput도 const로 정의해 주고, this.state를 지우고, this.setState를 각각 useState로 정의한대로 각각의 state의 함수로 맞춰 준다.
6. to Hooks - TryHooks.jsx
import React from "react";
const TryHooks = ({ tryInfo }) => {
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
};
export default TryHooks;
자식 컴포넌트인 TryHooks 역시 Hooks 형태로 바꾼다. 이 때, props는 위와 같이 매개변수처럼 전달한다. props는 렌더 함수를 자주 호출하게 되어 성능 저하의 원인이 되기도 하며, 부모로부터 전달받은 props는 절대 자식 컴포넌트에서 바꾸면 안된다. 부모 컴포넌트에서 에러가 발생할 가능성이 생기기 때문이다. 만약 꼭 자식 컴포넌트에서 props의 값을 바꿔야만 한다면, props를 state 안으로 넣어서 바꿔야 한다.
const [try, setTry] = useState(tryInfo.try);
const [result, setResult] = useState(tryInfo.result);
위와 같이, props를 state화하여 사용할 수도 있으나, 그래도 가급적이면 부모 컴포넌트에서 변경하는 것이 좋다.
또, 이를테면, 부모인 A에서 자식인 B를 거쳐서 손자인 C까지 props를 전달하는 경우, 설령 B에서는 해당 props를 사용하지 않고, C에서만 사용한다고 하더라도 B에도 props를 전달해야 한다. 그러나 이 경우, 불필요한 코드가 B에서 발생하기도 하고, 불필요한 render가 발생할 가능성이 생기므로, A에서 C로 직접 전달해야 할 필요가 생기는데, Context나 Redux가 이러한 역할을 수행할 수 있다.
7. PureComponent & memo
클래스형 컴포넌트에서는 setState가 호출될 때마다 render함수를 호출하는데, 불필요한 render를 막기 위해서는 보통 shouldComponentUpdate를 이용해 이를 수정한다. shouldComponentUpdate는 state값이 바뀔 때, true를 return할 때만 render 함수를 호출한다.
shouldComponentUpdate(nextProps, nextState, nextContext) {
if(this.state.counter !== nextState.counter) {
return true;
} else {
return false;
}
위와 같이, 값이 바뀌지 않을 때는 false를 반환하도록 하여 불필요한 렌더링을 막는 것이다.
이러한 shouldComponentUpdate의 역할을 대신 해 주는 것이 클래스형 컴포넌트에서는 PureComponent, Hooks에서는 memo이다.
import React, { memo } from "react";
const TryHooks = memo(({ tryInfo }) => {
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
});
export default TryHooks;
클래스형 컴포넌트에서는 Component 대신 PureComponent를 import 및 extends하고, Hooks에서는 위와 같이 memo를 import하고, memo 함수로 컴포넌트가 정의되는 부분을 감싸 주면 된다.
두 기능 모두 shouldComponentUpdate에 비해 간편하지만, 배열이나 객체와 같은 참조형 자료들에 완벽하게 대응하지 못하는 경우가 있다. 이를테면, 배열에 새로운 요소가 추가되었다면, 해당 배열을 가리키는 변수는 배열의 주소 값을 저장하고 있으므로, 실제 배열이 변경된 것을 인식하지 못하는 등의 문제점이 있을 수 있다. (이는 spread 문법으로 해결이 가능하다.) 또한, 배열과 객체가 복잡하게 꼬여 있는 경우에도 제대로 인식하지 못하기도 한다.
따라서 render가 호출되는 상황을 보다 세밀하게 조정하고자 한다면 shouldComponentUpdate를 쓰는 것이 요구되기도 한다.
[React.js] React Web Games - Rock, Scissor, Pap (Class & Hooks) (0) | 2021.07.27 |
---|---|
[React.js] React Web Games - Response Check (Class & Hooks) (0) | 2021.07.25 |
[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 |
[React.js] React Web Games - Multiplication Game (0) | 2021.07.08 |
댓글 영역