본문 바로가기
FrontEnd/React.js

[React.js] 제로초 웹게임 - 4. 반응속도체크 (useRef)

by Chaedie 2022. 10. 24.
728x90

강의를 통해 알게된 것

useRef의 용법 중 DOM 요소 조작이 아닌, 변수 컨테이너로 사용하는 용법에 대해 알아보았습니다. 이걸 알았다면 부트캠프에서 진행한 팀 프로젝트가 한층 더 완성도가 높았을텐대 많이 아쉽네요. 어찌 보면 프론트엔드 공부를 아직 많이~ 못했기에 그럴수도 있겠죠. 매일 매일 부족함을 많이 느낍니다.

React.memo를 사용해서 props가 변하지 않을 때 리렌더링 되지 않게 시도해보았지만 잘 되지 않았다. 내가 모르는 부분이 더 있나보다. 일단 진도를 쭉쭉 나가서 한 사이클 돌린 뒤에 생각해봐야 할 문제인것 같다. 프로페셔널 프론트엔드 개발자가 되고자 하는 사람이 아직 리액트 1회독도 못했다니 😭

얼른! 한사이클 돌리고 오겠습니다. 고지가 얼마 안남았네요. 화이팅!


처음에 내가 짠 코드

import React, { useState } from 'react';

function ReactionCheck() {
  const [bgColor, setBgColor] = useState('blue');
  const [isPlaying, setIsPlaying] = useState(false);
  const [startTime, setStartTime] = useState();
  const [result, setResult] = useState();

  const startGame = () => {
    setIsPlaying(true);
    setTimeout(() => {
      setBgColor('aqua');
      setStartTime(new Date().getTime());
    }, 1000);
  };

  const onClickBox = () => {
    let endTime = new Date().getTime();

    if (isPlaying === false) return;

    setBgColor('black');
    setIsPlaying(false);
    setResult(endTime - startTime + 'ms');
  };

  return (
    <>
      <h1>리액션 체크!</h1>
      <button onClick={startGame}>시작버튼!</button>
      <h2>아쿠아색이 되면 눌러주세요!</h2>
      <div onClick={onClickBox} className={`gameBox ${bgColor}`}></div>
      <h3>반응 속도 : {result}</h3>
    </>
  );
}

export default ReactionCheck;

강의 코드를 참고해서 개선한 코드

import React, { useState } from 'react';

function ReactionCheck() {
    const [state, setState] = useState('none');
  const [startTime, setStartTime] = useState();
  const [results, setResults] = useState([]);
  const [timeTrigger, setTimeTrigger] = useState();

  const startGame = () => {
    setState('waiting');
    const timeout = setTimeout(() => {
      setState('clickable');
      setStartTime(new Date().getTime());
    }, Math.floor(Math.random() * 1000) + 1000);

    setTimeTrigger(timeout);
  };

  const onClickBox = () => {
    if (state === 'none') return;
    if (state === 'waiting') {
      clearTimeout(timeTrigger);
      setState('none');
      return;
    }

    saveResult();
  };

  const saveResult = () => {
    let endTime = new Date().getTime();
    setState('none');
    let result = endTime - startTime;
    setResults(prev => [...prev, result]);
  };

  const renderAverage = () => {
    return (
      results.length !== 0 && (
        <h3>평균 반응 속도 : {results.reduce((acc, cur) => acc + cur, 0) / results.length || 0} ms</h3>
      )
    );
  };

  return (
    <>
      <h1>리액션 체크!</h1>
      <button onClick={startGame}>시작버튼!</button>
      <h2>아쿠아색이 되면 눌러주세요!</h2>
      <div onClick={onClickBox} className={`gameBox ${state}`}></div>
      <>{renderAverage()}</>
    </>
  );
}

export default ReactionCheck;

변경점 1) state라는 state를 둬서, ‘none’, ‘waiting’, ‘clickable’이라는 상태로 나누어주었다. 이 상태에 따라 bgColor도 변하도록 했다.

변경점 2) renderAverage라는 함수를 선언해서 jsx안에서 조건문을 통해 render가 나뉘는걸 빼 보았다. 개인적으로 이게 더 보기 안좋은것 같은데… ? 차라리 jsx를 return 하기 보다는 반응 속도 계산 식만 빼는게 나아보인다. 어쨌든 이렇게 부분적인 렌더함수를 만들어 줄 수 있다는 점을 배웠다.

변경점 3) startGame 함수에서 무조건 1000ms 뒤에 클릭 하는게 아니라 랜덤값을 줘서 긴장감을 높여주었다.🤣

변경점 4) ‘waiting’ 상태일 때 반칙으로 먼저 눌러 버리면 clearTimeout을 사용해서 timeout을 없애줬다. 근데 class형 컴포넌트에서는 state이외의 변수를 저장해두면 rerendering되더라도 render 바깥에서 선언해두면 변수로 쓸 수 있는데 함수 컴포넌트는 변수로 사용하면 리렌더될때 초기화 되버려서 사용을 못하더라. 그래서 어쩔 수 없이 timeout을 state로 지정해둬서 clear할 때 사용해줬다.

⁉️ 함수 컴포넌트 일 때 state 이외에 변수 사용하는 방법이 없을까? 분명히 있어야 정상이다. 이게 안된다면 함수 컴포넌트의 매력이 엄청나게 떨어지게 된다. (단순 변수로 사용하고 싶어도 state로 선언해야하니 불필요한 rerender가 많아지니까)

💁‍♂️ 자 ~ 함수 컴포넌트에서 변수 사용법 여기 있습니다. 리액트 초보 티가 너무 많이 났네요~ **12. useRef 로 컴포넌트 안의 변수 만들기 그러고 보니 타입스크립트 할 때 useRef안의 값의 타입이 DOM일 때도 있었지만 container역할을 할 때! 도 있었어요 그땐 자세히 몰랐는데 지금 보니까 그말이 이 말이었네요 😅

const [state, setState] = useState('none');
  const [results, setResults] = useState([]);
  const timeout = useRef();
  const startTime = useRef();

  const startGame = () => {
    setState('waiting');
    timeout.current = setTimeout(() => {
      setState('clickable');
      startTime.current = new Date().getTime();
    }, Math.floor(Math.random() * 1000) + 1000);
  };

  const onClickBox = () => {
    if (state === 'none') return;
    if (state === 'waiting') {
      clearTimeout(timeout.current);
      setState('none');
      return;
    }

    saveResult();
  };

  const saveResult = () => {
    let endTime = new Date().getTime();
    setState('none');
    let result = endTime - startTime.current;
    setResults(prev => [...prev, result]);
  };

✅ 와우! 대박! ㅎㅎ 이걸 몰라가지고 여태껏 엄청난 리렌더링을 유발시켰었네요!

부트캠프에서 팀 프로젝트 할 때 이걸 알았더라면 scroll 위치를 저장할 때 state가 아니라 useRef에 저장했으면 리렌더링을 훨씬 줄일 수 있었는데 바보짓이었네요 🤣

역시 사람은 배워야합니다. 너무 뿌듯하네요~!! 제로초 님 강의 너무 좋네요.

성능 체크

return (
    <>
      <h1>리액션 체크!</h1>
      <button onClick={startGame}>시작버튼!</button>
      <h2>아쿠아색이 되면 눌러주세요!</h2>
      <div onClick={onClickBox} className={`gameBox ${state}`}></div>
// ---------------------------------------------------------
      {results.length !== 0 && (
        <>
          <h3>평균 반응 속도 : {getResultsAvg()} ms</h3>
          <button onClick={reset}>리셋</button>
        </>
      )}
    </>
  );

class에선 렌더 함수 안에, function에선 return문 안에 줄 그은 곳 위 아래에 리셋이 되었을 때 굳이 윗부분이 리렌더 되지 않아도 된다. 이게 싫다면 윗 부분을 컴포넌트로 분리해서 React.memo()로 래핑해주면 불필요한 리렌더링을 막아줄 수 있다.

import React, { useRef, useState } from 'react';

function ReactionCheck() {
  const [state, setState] = useState('none');
  const [results, setResults] = useState([]);
  const timeout = useRef();
  const startTime = useRef();

  const startGame = () => {
    setState('waiting');
    timeout.current = setTimeout(() => {
      setState('clickable');
      startTime.current = new Date().getTime();
    }, Math.floor(Math.random() * 1000) + 1000);
  };

//...

const reset = () => {
    setResults([]);
    clearTimeout(timeout.current);
    setState('none');
  };

return (
    <>
      <ReactionHeader startGame={startGame}></ReactionHeader>
      {/* <ReactionHeader startGame={startGame}></ReactionHeader> */}
      {/* <h1>리액션 체크!</h1>
      <button onClick={startGame}>시작버튼!</button>
      <h2>아쿠아색이 되면 눌러주세요!</h2> */}
      <div onClick={onClickBox} className={`gameBox ${state}`}></div>
      {results.length !== 0 && (
        <>
          <h3>평균 반응 속도 : {getResultsAvg()} ms</h3>
          <button onClick={reset}>리셋</button>
        </>
      )}
    </>
  );
}

export default ReactionCheck;

const ReactionHeader = React.memo(function ReactionHeader({ startGame }) {
  return (
    <>
      <h1>리액션 체크!</h1>
      <button onClick={startGame}>시작버튼!</button>
      <h2>아쿠아색이 되면 눌러주세요!</h2>
    </>
  );
});

React.memo를 사용해서 props가 변하지 않으면 리렌더링이 되지 않도록 하였다. 하지만 데브툴에서 확인 해본 결과 리셋버튼을 눌렀을 때 ReactionHeader부분이 리렌더링 되는걸로 확인되었다.

⁉️ 왜그럴까? 왜 React.memo가 소용없을까?

아마도 startGame 내부의 값이 바뀌어서가 아닐까?

흠 .. 이것저것 찾아봐도 못찾았다. 혹시나 내용이 나올까? 해서 챕터4의 강의를 다 들어봤지만 나오지 않았다… 구글링 결과 벨로퍼트 리액트에서 useCallback, useMemo 등이 나오면서 설명을 해주시던데, 처음부터 따라간게 아니라 그런가 이해가 안되더라. 일단은 모르는채로 넘어가야겠다. 다음에 React.memo가 나오면 꼭 다시 해봐야지.

댓글