[웹 게임을 만들며 배우는 React] - 숫자야구(useState), 렌더링 문제
출처: https://www.inflearn.com/course/web-game-react/dashboard
* Try.jsx를 따로 빼서 NumberBaseball.jsx에서 호출
* useState
참고 : https://react.vlpt.us/basic/07-useState.html
▶ 컴포넌트에서 상태 관리하기
▶ 값을 넣을 경우(1.)와 함수를 넣을 경우(2.) 존재
▶ 함수를 넣을 경우 answer는 함수의 리턴값으로 값이 설정됨(다시 실행되지 않음)
//1. answer에 1이 설정됨
const [answer, setAnswer] = useState(1);
//2. 함수를 넣을 경우
const getNumbers = () => {
// 숫자 네 개를 겹치지 않고 랜덤하게 뽑는 함수
const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i += 1) {
const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(chosen);
}
return array;
};
const [answer, setAnswer] = useState(getNumbers);
//const [answer, setAnswer] = useState(getNumbers());
//=> 함수가 아닌 함수 호출도 가능은 하나 그럴 경우 한 번만 사용하면 되는 함수가 여러 번 불러오게 됨
* class 사용
[NumberBaseballClass.jsx]
▶ push 사용 불가 : 바로 배열에 값을 넣기보단 기존 배열 복사 후 새로운 값을 넣어야 함
▶ 배열 복사 참고 : https://enfanthoon.tistory.com/137
this.setState((prevState) => {
return {
result: "홈런!",
tries: [...prevState.tries, { try: value, result: "홈런!" }],
};
});
import React, { Component, createRef } from "react";
import TryClass from "./TryClass";
function getNumbers() {
// 숫자 네 개를 겹치지 않고 랜덤하게 뽑는 함수
const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i += 1) {
const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(chosen);
}
return array;
}
class NumberBaseballClass extends Component {
state = {
result: "",
value: "",
answer: getNumbers(), // ex: [1,3,5,7]
tries: [],
};
onSubmitForm = (e) => {
const { value, tries, answer } = this.state;
e.preventDefault();
if (value === answer.join("")) {
this.setState((prevState) => {
return {
result: "홈런!",
tries: [...prevState.tries, { try: value, result: "홈런!" }],
};
});
alert("게임을 다시 시작합니다!");
this.setState({
value: "",
answer: getNumbers(),
tries: [],
});
this.inputRef.current.focus();
} else {
// 답 틀렸으면
const answerArray = value.split("").map((v) => parseInt(v));
let strike = 0;
let ball = 0;
if (tries.length >= 9) {
// 10번 이상 틀렸을 때
this.setState({
result: `10번 넘게 틀려서 실패! 답은 ${answer.join(",")}였습니다!`,
});
alert("게임을 다시 시작합니다!");
this.setState({
value: "",
answer: getNumbers(),
tries: [],
});
this.inputRef.current.focus();
} else {
for (let i = 0; i < 4; i += 1) {
if (answerArray[i] === answer[i]) {
strike += 1;
} else if (answer.includes(answerArray[i])) {
ball += 1;
}
}
this.setState((prevState) => {
return {
tries: [
...prevState.tries,
{ try: value, result: `${strike} 스트라이크, ${ball} 볼입니다` },
],
value: "",
};
});
this.inputRef.current.focus();
}
}
};
onChangeInput = (e) => {
console.log(this.state.answer);
this.setState({
value: e.target.value,
});
};
inputRef = createRef(); // this.inputRef
render() {
const { result, value, tries } = this.state;
return (
<>
<h1>{result}</h1>
<form onSubmit={this.onSubmitForm}>
<input
ref={this.inputRef}
maxLength={4}
value={value}
onChange={this.onChangeInput}
/>
</form>
<div>시도: {tries.length}</div>
<ul>
{tries.map((v, i) => {
return <TryClass key={`${i + 1}차 시도 :`} tryInfo={v} />;
})}
</ul>
</>
);
}
}
export default NumberBaseballClass;
[TryClass.jsx]
import React, { Component } from "react";
class TryClass extends Component {
render() {
const { tryInfo } = this.props;
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
}
}
export default TryClass;
[client.jsx]
import React from "react";
import { createRoot } from "react-dom/client";
import NumberBaseball from "./NumberBaseballClass";
createRoot(document.getElementById("root")).render(<NumberBaseball />);
* Hooks 사용
[NumberBaseball.jsx]
▶ 위에서 언급한 useState에 함수 넣기와 setAnswer(getNumbers())는 구분해야 함
▶ 참고 : https://ba-gotocode131.tistory.com/202?category=987174
//함수
const [answer, setAnswer] = useState(getNumbers); //lazy init
//함수 호출
setAnswer(getNumbers()); //리턴한 array를 setAnswer로 받아줌(호출해서 넣어줌)
import React, { useState, useRef, useCallback } from "react";
import Try from "./Try";
const getNumbers = () => {
// 숫자 네 개를 겹치지 않고 랜덤하게 뽑는 함수
const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i += 1) {
const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(chosen);
}
return array;
};
const NumberBaseballClass = () => {
const [result, setResult] = useState("");
const [value, setValue] = useState("");
const [answer, setAnswer] = useState(getNumbers); //lazy init
const [tries, setTries] = useState([]);
const inputEl = useRef(null);
const onSubmitForm = useCallback(
(e) => {
e.preventDefault();
if (value === answer.join("")) {
setTries((t) => [
...t,
{
try: value,
result: "홈런!",
},
]);
setResult("홈런!");
alert("게임을 다시 시작합니다!");
setValue("");
setAnswer(getNumbers());
setTries([]);
inputEl.current.focus();
} else {
// 답 틀렸으면
const answerArray = value.split("").map((v) => parseInt(v));
let strike = 0;
let ball = 0;
if (tries.length >= 9) {
// 10번 이상 틀렸을 때
setResult(`10번 넘게 틀려서 실패! 답은 ${answer.join(",")}였습니다!`);
alert("게임을 다시 시작합니다!");
setValue("");
setAnswer(getNumbers());
setTries([]);
inputEl.current.focus();
} else {
console.log("답은", answer.join(""));
for (let i = 0; i < 4; i += 1) {
if (answerArray[i] === answer[i]) {
strike += 1;
} else if (answer.includes(answerArray[i])) {
ball += 1;
}
}
setTries((t) => [
...t,
{
try: value,
result: `${strike} 스트라이크, ${ball} 볼입니다`,
},
]);
setValue("");
inputEl.current.focus();
}
}
},
[value, answer]
);
const onChangeInput = useCallback((e) => setValue(e.target.value), []);
return (
<>
<h1>{result}</h1>
<form onSubmit={onSubmitForm}>
<input
ref={inputEl}
maxLength={4}
value={value}
onChange={onChangeInput}
/>
</form>
<div>시도: {tries.length}</div>
<ul>
{tries.map((v, i) => {
return <Try key={`${i + 1}차 시도 :`} tryInfo={v} />;
})}
</ul>
</>
);
};
export default NumberBaseballClass;
[Try.jsx]
import React, { Component } from "react";
const Try = ({ tryInfo }) => { //props 자리가 구조분해한 tryInfo가 들어감
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
};
export default Try;
[client.jsx]
import React from "react";
import { createRoot } from "react-dom/client";
import NumberBaseball from "./NumberBaseball";
createRoot(document.getElementById("root")).render(<NumberBaseball />);
* 렌더링 문제 : 성능 최적화
- 자식 컴포넌트가 리렌더링 되는 경우
1. props가 바뀌는 경우
2. state가 바뀌는 경우
3. 부모 컴포넌트가 바뀌는 경우 자식 컴포넌트도 영향 받음 -> 성능에 문제 있을 수 있음
▶ props를 통해 NumberBaseball(부모)와 Try(자식)간 부모자식간의 관계가 생김
▶ NumberBaseball이 리렌더링 될 때마다 Try가 리렌더링됨
(1234 0 스트라이크, 2 볼입니다 | 4568 1 스트라이크, 2 볼입니다 | 9989 1 스트라이크, 0 볼입니다 등등 리렌더링됨)
해결책 1 : shouldComponentUpdate 사용
import React, { Component } from "react";
//렌더링 컴포넌트를 보기 위한 예시
class Test extends Component {
state = {
counter: 0,
};
shouldComponentUpdate(nextProps, nextState, nextComponent) {
//언제 렌더링이 될지를 설정해야 함
if (this.state.counter !== nextState.counter) {
return true; //렌더링 됨
}
return false; //렌더링 안 됨
}
onClick = () => {
this.setState({});
};
render() {
console.log("렌더링", this.state);
return (
<div>
<button onClick={this.onClick}>클릭</button>
</div>
);
}
}
export default Test;
해결책 2 : PureComponent (클래스) | memo(함수 컴포넌트) 사용
▶ 단, 클래스에서만 적용됨
▶ 함수 컴포넌트는 사용할 수 없는가? => memo 사용
PureComponent | memo | |
state 달라졌을 경우 | 막아줌 | 막아주지 못함 |
props 달라졌을 경우 | 막아줌 | 막아주지 못함 |
부모 컴포넌트 리렌더링 시 자식 컴포넌트 리렌더링 될 때 |
막아줌 | 막아줌 |
(함수 컴포넌트) [Try.jsx]
//수정 전
import React from "react";
const Try = ({ tryInfo }) => {
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
};
Try.displayName = "Try";
export default Try;
//수정 후
import React, { memo } from "react";
const Try = memo(({ tryInfo }) => {
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
});
Try.displayName = "Try"
export default Try;
(클래스) [TryClass.jsx]
//수정 전
import React, { Component } from "react";
class TryClass extends Component {
render() {
const { tryInfo } = this.props;
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
}
}
export default TryClass;
//수정 후
import React, { PureComponent } from "react";
class TryClass extends PureComponent {
render() {
const { tryInfo } = this.props;
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
);
}
}
export default TryClass;