과거 프로그래밍 자료들/React

[웹 게임을 만들며 배우는 React] - 가위바위보, 커스텀 훅

평부 2022. 9. 29. 12:20

 

 

출처: https://www.inflearn.com/course/web-game-react/dashboard

 

[무료] 웹 게임을 만들며 배우는 React - 인프런 | 강의

웹게임을 통해 리액트를 배워봅니다. Class, Hooks를 모두 익히며, Context API와 React Router, 웹팩, 바벨까지 추가로 배웁니다., - 강의 소개 | 인프런...

www.inflearn.com

 

https://ba-gotocode131.tistory.com/240 에서 이어짐

 

[웹 게임을 만들며 배우는 React] - 가위바위보, 라이프사이클

출처: https://www.inflearn.com/course/web-game-react/dashboard [무료] 웹 게임을 만들며 배우는 React - 인프런 | 강의 웹게임을 통해 리액트를 배워봅니다. Class, Hooks를 모두 익히며, Context API와 Reac..

ba-gotocode131.tistory.com

 

* 커스텀 훅

- hooks을 직접 만듦(특정한 hooks이 반복되는 경우)

- 아래의 예시의 경우 interval, clearInterval이 반복됨

 

[RSP.jsx]

 const interval = useRef();

  useEffect(() => {
   ...
    interval.current = setInterval(changeHand, 100);
    return () => {
     ...
      clearInterval(interval.current);
    };
  }, [imgCoord]); 
  
  const onClickBtn = (choice) => () => {
    clearInterval(interval.current);
    ...
         setTimeout(() => {
          interval.current = setInterval(changeHand, 100);
        }, 1000);
    }

 

 

[useInterval.jsx - 두 번째 파라미터에 바로 callback을 넣을 경우]

문제 있는 코드

callback이 바뀜에 따라서 clearInterval과 setInterval이 바뀌는 그 잠깐의 시간동안 딜레이가 계속 발생함

import { useRef, useEffect } from "react";

function useInterval(callback, delay) {
  useEffect(() => {
    //delay가 바뀔때만 새로 바뀜
    if (delay !== null) {
      //1. 이렇게 넣을 경우 callback이 바뀌는 것을 인식 못 함
      //따라서 옛날 함수가 그대로 실행됨
      let id = setInterval(callback, delay); //callback을 넣으면 그대로 옛날코드 실행
      return () => clearInterval(id);
    }
  }, [delay, callback]); //여기다 callback 넣으면 1.의 문제 해결
  //여전히 문제 존재 : callback이 바뀔 때도 clearInterval과 setInterval이 한 번씩 호출됨
  return callback;
}
export default useInterval;

//1초 뒤에 가위
//1.1초 뒤에 changeHand
//2.1초 뒤에 바위(2초 뒤 바위를 예상했는데 0.1초 증가)
//2.2초 뒤에 changeHand
//3.2초 뒤의 보(3초 뒤 바위를 예상했는데 0.2초 증가)

 

 

(해결)[useInterval.jsx -  useRef 사용]

- useRef  장점 : 항상 최신 객체를 참조할 수 있음

▶ 딜레이가 발생하지 않게 하기 위해 clearInterval을 아예 안 하게 만듦

= callback이 바뀔 때 clearInterval을 하는 것이 아닌 setInterval에서 ref로 항상 최신 callback을 담아둔 다음

(current()에 담음) 최신 callback을 실행하게 함

useRef 사용 시 setInterval과 clearInterval은 delay가 바뀔 때만 실행

= changeHand가 바뀌든 말든 setInterval과 clearInterval은 영향 받지 않고 항상 최신 callback만 받음

▶ 바로 callback을 쓰는 게 아니라 tick으로 한 번 감쌈 = 최신 callback을 담아두게 하기 위함

import { useRef, useEffect } from "react";

// const [isRunning, setRunning] = useState(true);
// useInterval(
//   () => {
//     console.log("hello");
//   },
//   isRunning ? 1000 : null
// );

//delay를 null로 만들면 interval이 멈춤
function useInterval(callback, delay) {
  const saveCallback = useRef();

  useEffect(() => {
    saveCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      saveCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay); //callback을 넣으면 그대로 옛날코드 실행
      return () => clearInterval(id);
    }
  }, [delay]);
  return saveCallback.current;
}
export default useInterval;

//1초 뒤에 가위
//1.1초 뒤에 changeHand
//2초 뒤에 바위
//2.1초 뒤에 changeHand
//3초 뒤의 보

 

 

[RSP.jsx - 커스텀 훅이 적용된 경우]

import React, { useState, useRef, useEffect } from "react";
import useInterval from "./useInterval"; //커스텀 훅 불러옴

const rspCoords = {
  바위: "0",
  가위: "-142px",
  보: "-284px",
};

const scores = {
  가위: 1,
  바위: 0,
  보: -1,
};

const computerCoice = (imgCoord) => {
  return Object.entries(rspCoords).find(function (v) {
    return v[1] === imgCoord;
  })[0];
};

const RSP = () => {
  const [result, setResult] = useState("");
  const [imgCoord, setImgCoord] = useState(rspCoords.바위);
  const [score, setScore] = useState(0);
  //커스텀 훅
  const [isRunning, setIsRunning] = useState(true);

  const changeHand = () => {
    if (imgCoord === rspCoords.바위) {
      setImgCoord(rspCoords.가위);
    } else if (imgCoord === rspCoords.가위) {
      setImgCoord(rspCoords.보);
    } else if (imgCoord === rspCoords.보) {
      setImgCoord(rspCoords.바위);
    }
  };

  //커스텀 훅
  useInterval(changeHand, isRunning ? 100 : null);

  const onClickBtn = (choice) => () => {
    if (isRunning) {
      //멈춰있을 때 클릭하는 것 막기
      setIsRunning(false);
      const myScore = scores[choice];
      const comScore = scores[computerCoice(imgCoord)];
      const diff = myScore - comScore;
      if (diff === 0) {
        setResult("비겼습니다");
      } else if ([-1, 2].includes(diff)) {
        setResult("이겼습니다!");
        setScore((prevScore) => prevScore + 1);
      } else {
        setResult("졌습니다!");
        setScore((prevScore) => prevScore - 1);
      }
      setTimeout(() => {
        setIsRunning(true);
      }, 1000);
    }
  };
  return (
    <>
      <div
        id="computer"
        style={{
          background: `url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${imgCoord} 0`,
        }}
      />
      <div>
        <button id="rock" className="btn" onClick={onClickBtn("바위")}>
          바위
        </button>
        <button id="scissor" className="btn" onClick={onClickBtn("가위")}>
          가위
        </button>
        <button id="paper" className="btn" onClick={onClickBtn("보")}>
          보
        </button>
      </div>
      <div>{result}</div>
      <div>현재 {score}점</div>
    </>
  );
};

export default RSP;

 

 

* 가위바위보가 동작 중일 때는 true, 멈출 때는(결과 보여줄 때) false