[웹 게임을 만들며 배우는 React] 로또 추첨기 (setTimeout, useMemo, useCallback, hooks tips)

안지수·2023년 4월 14일
0
post-custom-banner
  • setInterval, setTimeout은 사용 후 componentWillUnmount에서 꼭 clear 해줘야 함!!

👑 setTimeout

-> 일정 시간 텀을 두기 위해 사용
-> hook에서는 ref가 처리
-> 사용 후 언제나 clear 해줘야 함

👑 로또 추첨기 class 컴포넌트


-> 컴포넌트 시작되자마자 setTimeout 실행 됨. 1초, 2초, 3초....뒤에 공이 등장함

-> setTimeout 사용했으면, 꼭 clear 해줘야 함

-> 한 번 더 버튼 누르면, 초기화해주고 setTimeout 다시 실행해줘야 함

-> 한 번 더 버튼을 눌렀을 때만, componentDidUpdate 수행하여 setTimeout 다시 실행하도록 해줘야 함

import React, { Component } from 'react';
import Ball from './Ball';

function getWinNumbers() { //숫자 미리 뽑아놓음
  console.log('getWinNumbers');
  const candidate = Array(45).fill().map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
  }
  const bonusNumber = shuffle[shuffle.length - 1];
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c);
  return [...winNumbers, bonusNumber];
}

class Lotto extends Component {
  state = {
    winNumbers: getWinNumbers(), // 당첨 숫자들
    winBalls: [],
    bonus: null, // 보너스 공
    redo: false,
  };

  timeouts = [];

  runTimeouts = () => {
    console.log('runTimeouts');
    const { winNumbers } = this.state;
    for (let i = 0; i < winNumbers.length - 1; i++) {
      this.timeouts[i] = setTimeout(() => {
        this.setState((prevState) => {
          return {
            winBalls: [...prevState.winBalls, winNumbers[i]],
          };
        });
      }, (i + 1) * 1000);
    }
    this.timeouts[6] = setTimeout(() => {
      this.setState({
        bonus: winNumbers[6],
        redo: true,
      });
    }, 7000);
  };

  componentDidMount() {
    console.log('didMount');
    this.runTimeouts();
    console.log('로또 숫자를 생성합니다.');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('didUpdate');
    if (this.state.winBalls.length === 0) {
      this.runTimeouts();
    }
    if (prevState.winNumbers !== this.state.winNumbers) {
      console.log('로또 숫자를 생성합니다.');
    }
  }

  componentWillUnmount() {
    this.timeouts.forEach((v) => {
      clearTimeout(v);
    });
  }

  onClickRedo = () => {
    console.log('onClickRedo');
    this.setState({
      winNumbers: getWinNumbers(), // 당첨 숫자들
      winBalls: [],
      bonus: null, // 보너스 공
      redo: false,
    });
    this.timeouts = [];
  };

  render() {
    const { winBalls, bonus, redo } = this.state;
    return (
      <>
        <div>당첨 숫자</div>
        <div id="결과창">
          {winBalls.map((v) => <Ball key={v} number={v} />)}
        </div>
        <div>보너스!</div>
        {bonus && <Ball number={bonus} />}
        {redo && <button onClick={this.onClickRedo}>한 번 더!</button>}
      </>
    );
  }
}

export default Lotto;

👑 로또 추첨기 함수 컴포넌트

useEffect로 바꾸기


-> 뒤에가 빈 배열이면, componentDidMount와 동일!
-> componentDidMount, componentDidUpadate 둘 다 수행
-> return 부분이 componentWillUnmount 역할
-> 기본적으로 componentDidMount 실행, 두번째 값이 맞으면 componentDidUpdate 실행

useMemo, useRef, useCallback


-> 두 번째 인자가 바뀌지 않는 한, 앞에꺼 다시 실행되지 않음. 불필요하게 get~함수가 여러 번 호출되는 것을 방지

  • useMemo: 복잡한 함수 결괏값을 기억, ([]바뀌기 전까지)
  • useRef: 일반값을 기억
  • useCallback: 함수 자체를 기억 ([]바뀌기 전까지)
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import Ball from './Ball';

function getWinNumbers() {
  console.log('getWinNumbers');
  const candidate = Array(45).fill().map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
  }
  const bonusNumber = shuffle[shuffle.length - 1];
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c);
  return [...winNumbers, bonusNumber];
}

const Lotto = () => {
  const lottoNumbers = useMemo(() => getWinNumbers(), []);
  const [winNumbers, setWinNumbers] = useState(lottoNumbers);
  const [winBalls, setWinBalls] = useState([]);
  const [bonus, setBonus] = useState(null);
  const [redo, setRedo] = useState(false);
  const timeouts = useRef([]);

  useEffect(() => {
    console.log('useEffect');
    for (let i = 0; i < winNumbers.length - 1; i++) {
      timeouts.current[i] = setTimeout(() => {
        setWinBalls((prevBalls) => [...prevBalls, winNumbers[i]]);
      }, (i + 1) * 1000);
    }
    timeouts.current[6] = setTimeout(() => {
      setBonus(winNumbers[6]);
      setRedo(true);
    }, 7000);
    return () => {
      timeouts.current.forEach((v) => {
        clearTimeout(v);
      });
    };
  }, [timeouts.current]); // 빈 배열이면 componentDidMount와 동일
  // 배열에 요소가 있으면 componentDidMount랑 componentDidUpdate 둘 다 수행

  useEffect(() => {
    console.log('로또 숫자를 생성합니다.');
  }, [winNumbers]);

  const onClickRedo = useCallback(() => {
    console.log('onClickRedo');
    console.log(winNumbers);
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);
    timeouts.current = [];
  }, [winNumbers]);

  return (
    <>
      <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball number={bonus} onClick={onClickRedo} />}
      {redo && <button onClick={onClickRedo}>한 번 더!</button>}
    </>
  );
};

export default Lotto;

👑 hook 함수인 useMemo, useCallback

  • 메모제이션 기법: 기존에 수행한 연산의 결괏값을 어디간에 저장해두고, 동일한 입력이 들어오면 재활용하는 방법 -> 중복 연산 피할 수 있음
  • useMemo: 메모제이션된 값을 반환
  • useCallbacK: 메모제이션된 함수를 반환

👑 Hooks 팁들

  • 조건문 안에 hooks 넣으면 안됨
  • useEffect 안에 넣으면 안됨 ([]가 바뀌면 실행한다)

    -> 뒤에가 빈 배열: componentDidMount 만

    -> componentDidUpdate 만, componentDidMount 실행되지만 아무것도 안함
    ----> 위의 2 패턴은 기억하기!!!!

⭕ 나의 언어로 정리:
setTimeout이나 setInterval과 같은 함수들 사용 시, componentDidMount~~와 같은 라이프사이클 함수를 사용한다.
그래서 이번 강의에서는 class component에서 라이프사이클 함수들 사용하는 방법과 그 다음에는 hooks로 변환시키는 방법을 공부하였다. (useEffect 사용 방법) 그리고, useMemo(return 값을 기억), useCallback(함수 자체 기억)을 익혔다.

profile
지수의 취준, 개발일기
post-custom-banner

0개의 댓글