웹 게임을 만들며 배우는 React(반응속도체크)

짜스의 하루 ·2024년 5월 21일

반응 속도 계산!

자, 파란색 화면을 누르면 주황색 화면이 나온다. 이때, '초록색이 되면 눌러주세요'라는 문구와 함께 랜덤한 시간이 지나면, 초록색 화면을 누른다! 초록색 화면을 빠르게 누르고 나면,

이렇게 나의 평균 시간이 나오게 된다.
단 한번 한 결과가 아니라 내가 5번 했으면, 5번 한 값이 평균을 구해서 평균 시간을 보여주는 것이다!

코드를 작성하기 전, state에 정의할 부분을 찾아야 한다

우선, 상자의 색깔 (상태에 따라 변하기 때문에 : state), 상자 속 메시지 (message), 평균 시간을 계산한 값(result)로 정의할 수 있다.

import React, { Component } from 'react';

class ResponseChecked extends Component {
  state = {
    state: 'waiting',
    message: '클릭해서 시작하세요',
    result: [],
  };

  setTime;
  startTime;
  endTime;

  onClickScreen = () => {
    const { state } = this.state;
    if (state === 'waiting') {
      this.setState({
        state: 'ready',
        message: '초록색으로 변하면 클릭하세요',
      });
      this.setTime = setTimeout(() => {
        this.setState({
          state: 'now',
          message: '지금 누르세요!',
        });
        this.startTime = new Date(); // 시작 시간 설정
      }, Math.floor(Math.random() * 1000) + 2000);
    } else if (state === 'now') {
      this.endTime = new Date(); // 종료 시간 설정
      this.setState((prevState) => ({
        state: 'waiting',
        message: '클릭해서 시작하세요',
        result: [...prevState.result, this.endTime - this.startTime], // 결과 추가
      }));
    } else if (state === 'ready') {
      clearTimeout(this.setTime);
      this.setState({
        state: 'waiting',
        message: '성급! 초록색 화면이 나오면 눌러주세요 !',
      });
    }
  };

  renderAverage = () => {
    const { result } = this.state;
    return result.length === 0 ? null : (
      <div>평균 시간: {result.reduce((a, c) => a + c) / result.length} ms</div>
    );
  };

  render() {
    const { state, message } = this.state;
    return (
      <>
        <div id="screen" className={state} onClick={this.onClickScreen}>
          {message}
        </div>
        {this.renderAverage()}
      </>
    );
  }
}

export default ResponseChecked;


변할 값들을 state 안에 정해두었다. state 는 'waiting'을 초기값으로,
message 는 '클릭해서 시작하세요', result는 빈 배열을 초기값으로 지정해 두었다.


className 속성을 {state}로 지정함으로써 해당 요소의 클래스를 동적으로 변경할 수 있다.
이 경우, state 값에 따라 해당 요소의 클래스가 변경되므로, 해당 요소의 스타일이 변화한다. --> 이것은 특히 UI의 상태에 따라 다른 스타일을 적용하고 싶을 때 유용하다.


이제 onClickScreen() 함수에 대해서 자세하게 살펴보자
여기서 중요한 점은 state가 ready 일 때, now 일 때, waiting 일 때 로 state를 설정하고, css도 변화한다는 것이다.

  • 'waiting' 상태일 때:
    상태를 'ready'로 변경하고, 메시지를 '초록색으로 변하면 클릭하세요'로 설정한다.
    일정 시간 후 (랜덤) 에 상태를 'now'로 변경하고, 메시지를 '지금 누르세요!'로 설정한다.
    이 때의 시간을 startTime으로 기록

  • 'now' 상태일 때:
    사용자가 클릭한 시간을 기록하고, 'now' 상태에서 클릭까지 걸린 시간을 계산한다.
    상태를 'waiting'으로 변경하고, 메시지를 '클릭해서 시작하세요'로 설정한다.
    클릭까지 걸린 시간을 결과에 추가한다.

  • 'ready' 상태일 때:
    기존에 설정한 타이머를 취소하고, 상태를 'waiting'으로 변경하며, 메시지를 '성급! 초록색 화면이 나오면 눌러주세요 !'로 설정한다.

요런 느낌 ...?

까먹을 뻔 했던 중요한 코드

반응 시간의 평균을 표시하는 함수인 renderAverage를 정의하고 있다.

result 배열의 길이가 0이면, 즉 클릭 결과가 없으면 null을 반환한다. 이 경우에는 아무 것도 표시되지 않는다.
result 배열에 클릭 결과가 있는 경우, 배열의 요소들을 모두 더한 후 배열의 길이로 나누어 평균을 구한다. 이를 통해 사용자의 평균 반응 시간을 계산한다.

지금까지 한 것 중엔 가장 코드 짜기 수월했던 것 같다 ...(?) 근자감이다 💪🏻💪🏻💪🏻💪🏻💪🏻💪🏻💪🏻💪🏻

자 여기서, 저 renderAverage()함수를 따로 class로 뽑아보면 어떨까 ?


renderAverage()함수를 class로 !


요렇게 뽑아 보았다.

우선, <RenderAverage> 클래스를 불러오면서 (여기서 RenderAverage는 자식 컨포넌트가 된다)
<RenderAverage>에서 사용할 result와 onReset에 대해서 정의해 두었다(props로 전달한다)

이 정의해 둔 것들을 <RenderAverage> 에서 const {result, onReset} = this.props 로 받아온 뒤, 삼향연산자를 통해서 조건문을 실행한다.

실무에서는 요렇게 자주 뽑아서 사용한다고 한다!
한번 연습삼아서 뽑아보았다

이제 할 일은 class -> hooks로 변경

import React, { useState, useRef } from 'react';
import RenderAverage from './renderAverage';

const ResponseChecked = () => {
  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) => [
        ...prevResult,
        endTime.current - startTime.current,
      ]);
    }
  };

  const onReset = () => {
    setResult([]);
    setState('waiting');
    setMessage('클릭해서 시작하세요');
  };

  return (
    <>
      <div id="screen" className={state} onClick={onClickScreen}>
        {message}
      </div>
      <RenderAverage key={result.length} result={result} onReset={onReset} />
    </>
  );
};

export default ResponseChecked;

확실히 코드 자체가 길이가 많이 짧아진 느낌이 들긴 한다.
크게 달라진 부분은 없지만,

useRef부분을 잘 살펴봐야 한다.

왜 useState를 사용하지 않고, useRef를 사용했을까???

useState와 useRef의 차이

useState

  • useState는 상태 값을 저장하고 이를 변경할 수 있는 함수(setState) 를 제공한다. 컴포넌트가 다시 렌더링될 때 상태 값이 유지되며, 상태가 변경되면 컴포넌트가 다시 렌더링된다.
  • 용도: 컴포넌트의 상태를 관리할 때 사용한다. 상태 변경 시 컴포넌트가 다시 렌더링되어야 할 때 적합하다.

useRef

  • useRef는 변경 가능한 참조 객체를 생성한다. 이 객체는 .current라는 속성을 가지고 있으며, 컴포넌트가 다시 렌더링되어도 참조 객체의 값이 유지된다. 그러나 값이 변경되어도 컴포넌트가 다시 렌더링되지 않는다.
  • 용도: DOM 요소에 접근하거나, 상태 변경 없이 값이 변경되어야 할 때 사용한다. 주로 컴포넌트가 다시 렌더링될 필요가 없는 변수를 저장할 때 적합하다.
  • timeout이나 인터벌, 값은 변경되지만, 렌더링은 하고 싶지 않을 때, useRef를 사용한다.
    --> ref면 무조건 .current 로 접근해야 한다!!!!!!
profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글