[React.js] React Web Games - Response Check (Class & Hooks)
1. 반응 속도 체크
빨간색 상자가 초록색으로 변한 뒤 클릭하면, 색깔 변화와 클릭 사이의 시간을 체크하는 게임이다.
2. Class
import React, { Component } from "react";
class ResponseCheckClass extends Component {
state = {
state: "waiting",
message: "클릭해서 시작하세요.",
result: [],
};
timeout;
startTime;
endTime;
onClickScreen = () => {
const { state } = this.state;
if (state === "waiting") {
this.setState({ state: "ready", message: "초록색이 되면 클릭하세요." });
this.timeout = setTimeout(() => {
this.setState({ state: "now", message: "지금 클릭" });
this.startTime = new Date();
}, Math.floor(Math.random() * 1000) + 2000);
} else if (state === "ready") {
clearTimeout(this.timeout);
this.setState({
state: "waiting",
message: "너무 성급하시군요! 초록색이 된 후에 클릭하세요!",
});
} else if (state === "now") {
this.endTime = new Date();
this.setState((prevState) => {
return {
state: "waiting",
message: "클릭해서 시작하세요",
result: [...prevState.result, this.endTime - this.startTime],
};
});
}
};
onReset = () => {
this.setState({
result: [],
});
};
renderAverage = () => {
const { result } = this.state;
return result.length === 0 ? null : (
<>
<div>
평균 시간:
{result.reduce((a, c) => a + c / result.length)}
ms
</div>
<button onClick={this.onReset}>리셋</button>;
</>
);
};
render() {
const { state, message } = this.state;
return (
<>
<div id="screen" className={state} onClick={this.onClickScreen}>
{message}
</div>
{this.renderAverage()}
</>
);
}
}
export default ResponseCheckClass;
먼저 state를 정의한다. state의 상태가 색깔이 칠해진 div의 클래스가 될 것이다.
또, 시간 체크와 관련된 timeout, startTime, endTime의 변수들은 state의 바깥에 정의하는데, 이렇게 하면 이 값들은 변경되어도 render 함수를 호출하지 않게 된다. 따라서 불필요한 렌더를 방지할 수 있다.
onClickScreen = () => {
const { state } = this.state;
if (state === "waiting") {
this.setState({ state: "ready", message: "초록색이 되면 클릭하세요." });
this.timeout = setTimeout(() => {
this.setState({ state: "now", message: "지금 클릭" });
this.startTime = new Date();
}, Math.floor(Math.random() * 1000) + 2000);
} else if (state === "ready") {
clearTimeout(this.timeout);
this.setState({
state: "waiting",
message: "너무 성급하시군요! 초록색이 된 후에 클릭하세요!",
});
} else if (state === "now") {
this.endTime = new Date();
this.setState((prevState) => {
return {
state: "waiting",
message: "클릭해서 시작하세요",
result: [...prevState.result, this.endTime - this.startTime],
};
});
}
};
이후 메인 로직인 onClickScreen을 작성하는데, 구조 분해 할당으로 state를 this.state.state로 설정하고, 이 값이 waiting, ready, now로 바뀜에 다른 코드를 실행하도록 조건문을 만든다. waiting일 때는 아직 게임을 시작하기 전으로, 이 상태에서 클릭된다면 state의 값을 ready, message는 초록색이 되면 클릭하라는 안내문으로 만든다. 즉, 게임을 시작하는 것이다. 이후 timeout 변수에 setTimeout의 실행 결과를 할당하는데, setTImeout은, 0에서 2초 사이의 랜덤한 시간 간격을 두고 state의 값을 now로, message를 지금 클릭으로 바꾼 뒤, 해당 시간을 startTime 변수에 기록하도록 설정한다.
state의 값이 ready 상태일 때 클릭된다면, 아직 now가 되기 전에 사용자가 멋대로 클릭한 상황이므로, this.timeout을clear하고, state를 다시 waiting으로, message를 초록색이 된 후에 클릭하라는 안내로 바꾼다. state가 now일 때는 클릭된 시간을 endTime에 기록하고, endTime에서 startTime을 빼서 result 배열에 기록한다. 그리고, 게임을 다시 시작하기 위해 state와 message를 다시 바꾼다. result 배열에 값을 기록할 때는, prevState를 활용하는데, 이는 평균 반응 속도를 구하기 위한 것이다. 전개 구문을 활용하여 이전에 저장된 result 배열을 가져오고, 마지막으로 구한 반응 속도 값을 추가해 주는 것이다.
onReset = () => {
this.setState({
result: [],
});
};
renderAverage = () => {
const { result } = this.state;
return result.length === 0 ? null : (
<>
<div>
평균 시간:
{result.reduce((a, c) => a + c / result.length)}
ms
</div>
<button onClick={this.onReset}>리셋</button>;
</>
);
};
다음으로는 게임을 초기화하는 onReset 함수를 만들어 result 배열의 값을 비운다. 이를 리셋 버튼에 연결한다.
또, renderAverage 함수를 만들고, result의 길이가 0일 때, 즉 게임을 처음 실행할 때는 아무것도 반환하지 않도록 하고, 그렇지 않은 경우 평균 시간을 구해 표시하도록 한다. reduce는 누산기로, 누적된 값인 a에, 현재 값인 c(배열의 모든 각각의 요소)/result.length를 더한다. 이는 곧 endTime - startTime의 평균 값이 된다.
또, JSX 안에서는 if를 사용할 수 없는데, 따라서 조건문을 만들 때는 condition ? code1 : code2(삼항연산자, condition이 true이면 code1을, false이면 code2를 시행한다.)를 이용하거나, code1 && code2(code1이 true이면 code를 실행하고, false이면 실행하지 않는다)를 이용한다.
renderAverage = () => {
const { result } = this.state;
return result.length !== 0 && (
<>
<div>
평균 시간:
{result.reduce((a, c) => a + c / result.length)}
ms
</div>
<button onClick={this.onReset}>리셋</button>
</>
);
};
즉, renderAverage 함수의 return 부분은 위와 같이 && 연산자를 사용해서도 나타낼 수 있다.
그리고 JSX에서 false, undefined, null은 태그를 사용하지 않음을 의미한다.
render() {
const { state, message } = this.state;
return (
<>
<div id="screen" className={state} onClick={this.onClickScreen}>
{message}
</div>
{this.renderAverage()}
</>
);
}
마지막 render 함수는 div를 반환하고, state를 클래스 명으로 전달하고, div의 값으로 message를 보여주도록 한다.
3. Hooks
import React, { useState, useRef } from "react";
const ResponseCheckHooks = () => {
const [state, setState] = useState("waiting");
const [message, setMessage] = useState("클릭해서 시작하세요");
const [result, setResult] = useState([]);
const timeout = useRef(null);
const startTime = useRef();
const endTime = useRef();
const onClickScreen = () => {
if (state === "waiting") {
setState("ready");
setMessage("초록색이 되면 클릭하세요.");
timeout.current = setTimeout(() => {
setState("now");
setMessage("지금 클릭");
startTime.current = new Date();
}, Math.floor(Math.random() * 1000) + 2000);
} else if (state === "ready") {
clearTimeout(timeout.current);
setState("waiting");
setMessage("너무 성급하시군요! 초록색이 된 후에 클릭하세요!");
} else if (state === "now") {
endTime.current = new Date();
setState("waiting");
setMessage("클릭해서 시작하세요");
setResult((prevResult) => {
return [...prevResult, endTime.current - startTime.current];
});
}
};
const onReset = () => {
setResult([]);
};
const renderAverage = () => {
return result.length === 0 ? null : (
<>
<div>
평균 시간:
{result.reduce((a, c) => a + c / result.length)}
ms
</div>
<button onClick={onReset}>리셋</button>;
</>
);
};
return (
<>
<div id="screen" className={state} onClick={onClickScreen}>
{message}
</div>
{renderAverage()}
</>
);
};
export default ResponseCheckHooks;
Hooks로 바꾸면 위와 같다. state를 각각 useState로 선언해 준다. 또, 클래스형 컴포넌트에서 timeout, startTime, endTime은 state 객체의 바깥쪽에 선언했는데, 이는 Hooks에서는 useRef를 사용한다. 클래스형 컴포넌트에서와 마찬가지로, 불필요한 렌더링을 막기 위한 장치이다. useRef는 실행되어도 컴포넌트를 리렌더링하지 않는다. useRef를 사용한 변수들을 사용할 때는 current를 붙여 준다.