6-1. 로또 추첨기 컴포넌트

setTimeout으로 무작위로 생성한 7개의 숫자를 순서대로 노출하는 컴포넌트를 만들어본다.

  1. 먼저 7개의 순서를 무작위로 생성하는 별도의 함수가 필요하다.
function getWinNumbers() {
	// 0 - 45까지 들어있는 배열 생성
	const candidate = Array(45).fill().map((v, i) => i + 1);
	// 랜덤 숫자를 넣는 빈 배열 생성
	const shuffle = [];
	// candidate가 빈배열이 될 때까지 아래 코드를 동작시킨다.
	while (candidate.length > 0){
		// 랜덤한 값을 shuffle에 push
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	// 맨 마지막 bonusNumber 가져오기
	const bonusNumber = shuffle[shuffle.length - 1];
	// 0 - 6번쨰 숫자를 가져와 오름차순으로 정렬
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c);
	// 값 리턴
  return [...winNumbers, bonusNumber];
}
  1. 또한 해당 점수를 컴포넌트로 렌더링 해 줄 Ball 컴포넌트가 필요하다.
import React, { memo } from "react";

// 컴포넌트를 다른 컴포넌트로 감싸주는 것을 HOC(high order component)라고 한다.
const Ball = memo(({ number }) => {
  let background;
  if (number <= 10) {
    background = "red";
  } else if (number <= 20) {
    background = "orange";
  } else if (number <= 30) {
    background = "yellow";
  } else if (number <= 40) {
    background = "blue";
  } else {
    background = "green";
  }

  return (
    <div className="ball" style={{ background }}>
      {number}
    </div>
  );
});
export default Ball;

가장 마지막에 오는 자식 컴포넌트의 경우 보통 PureComponent나 memo를 넣어 불필요한 리렌더링을 방지해준다. 위 코드처럼 컴포넌트를 다른 컴포넌트로 감싸주는 방식을 HOC(high order component)라고 하며, 위와 같은 함수형 컴포넌트는 PureComponent가 아니기 때문에 별도 memo 메서드로 감싸주어야 PureComponent 역할을 한다.

6-2. setTimeout 여러 번 사용하기

이번 게임에서는 그동안 배웠던 조건문과 반복문을 통 틀어서 사용해볼 수 있다.

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

function getWinNumbers() { ... }

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

  timeouts = [];

  componentDidMount() {
    const { winNumbers } = this.state;
    // 1. let을 사용하면 클로저 문제가 발생하지 않는다.
    for (let i = 0; i < winNumbers.length - 1; i++) {
			// 2. setTimeout 여러번 사용하기
      this.timeouts[i] = setTimeout(
        () =>
          this.setState((prevState) => {
            return {
              winBalls: [...prevState.winBalls, winNumbers[i]],
            };
          }),
        (i + 1) * 1000
      );
    }
    // 2. setTimeout 여러번 사용하기: bonus 점수 노출
    this.timeouts[6] = setTimeout(
      () =>
        this.setState({
          bonus: winNumbers[6],
          redo: true,
        }),
      7000
    );
  }

  componentWillUnmount() {
    // 3. 반드시 setTimeout을 clear해줘야 한다.
    this.timeouts.forEach((t) => clearTimeout(t));
  }

  onClickRedo = () => {};

  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;
  1. for 문에서 let을 사용하면 클로저 문제가 발생하지 않는다.
  2. setTimeout을 여러번 사용하기, for 문 안에서 setTimeout을 순차적으로 실행해주고, 해당 값을 this.timeout에 배열 값으로 넣어준다.
  3. setTimeout, setInterval 등은 반드시 componentWillUnmount 지점 등 clear 처리를 해줘야 한다. 그렇지 않으면 메모리 누수가 발생하여 성능 저하를 일으킨다.

6-3. componentDidUpdate

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

function getWinNumbers() { ... }

class Lotto extends Component {
  state = { ...  };
  timeouts = [];

	// 1. setTimeout 동작부분 분리
  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() {
		// 2. setTimeout 실행 
    this.runTimeouts();
  }

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

  componentDidUpdate(prevProps, prevState) {
		// 4. 변경되는 시점 체크 (!this.state.bonus or !this.state.redo 가능)
    if (!this.state.winBalls.length) {
			// setTimeout 실행
      this.runTimeouts();
    }
  }

  onClickRedo = () => {
    // 3. 초기화 시켜준다.
    this.setState({
      winNumbers: getWinNumbers(),
      winBalls: [],
      bonus: null,
      redo: false,
    });
    this.timeouts = [];
  };

  render() {
    return ( {/*  */} );
  }
}

export default Lotto;
  1. setTimeout 동작 부분 코드를 별도 함수로 분리한다. (재실행 시에도 사용해야하므로)
  2. 필요한 위치(componentDidMount, componentDidUpdate)에서 runTimeouts 실행
  3. onClickRedo라는 이벤트에서 state 값을 초기화 시켜준다. → state가 변경되어 componentDidUpdate 발생함
  4. 변경되는 시점을 체크해서, 해당 경우에 runTimeouts 함수를 실행시킨다.

6-4. useEffect로 업데이트 감지하기

클래스형 컴포넌트를 Hooks로 바꿔보자.