[웹 게임을 만들며 배우는 React] ⑥ 로또 추첨기

chez_bono·2020년 2월 1일
0
post-thumbnail

🔗 웹 게임을 만들며 배우는 React

1. 로또 추첨기 컴포넌트

  • 가장 하위의 자식 컴포넌트이고 state를 사용하지 않는 컴포넌트는 PureComponent나 함수 컴포넌트(+ memo)로 만드는 것이 좋음

Lotto.jsx

import React, { Component } from 'react';

function getWinNumbers() {
  	// 1~ 45가 들어있는 배열 생성
    const candidate = Array(45).fill().map((v, i) => i + 1);
    const shuffle = [];
  	// 1~45를 랜덤하게 섞기
    while (candidate.length > 0) {
        shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
    }
  // shuffle의 마지막 수를 보너스 숫자로
    const bonusNumber = shuffle[shuffle.length - 1];
  // shuffle의 0~6번째 수를 오름차순 정렬하여 당첨 숫자로
    const winNumbers = shuffle.splice(0, 6).sort((p, c) => p - c);
    return [...winNumbers, bonusNumber];
}

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

    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;

Ball.jsx

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

// 함수 컴포넌트
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;

2. setTimeout 여러 번 사용하기

  • 시작하자마자부터 시간 간격을 두고 숫자가 보여야 하므로 componentDidMount에서 setTimeout 사용
  • let을 사용하면 클로져 문제가 발생하지 않음 (ES6부터 개선된 부분)

    ❓ 클로져 문제..?

  • componentWillUnmount에서 clearTimeout하여 메모리 누수 방지

Lotto.jsx

...
timeouts = [];

componentDidMount() {
  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); // 1초 간격으로 출력
  }
  this.timeouts[6] = setTimeout(() => {  // 보너스공
    this.setState({
      bonus: winNumbers[6],
      redo: true,
    }, 7000);
  });
}

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

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

3. componentDidUpdate

Lotto.jsx

...
timeouts = [];

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); // 1초 간격으로 출력
  }
  this.timeouts[6] = setTimeout(() => {  // 보너스공
    this.setState({
      bonus: winNumbers[1],
      redo: true,
    });
  }, 7000);
};

componentDidMount() {
  this.runTimeouts();
}

componentDidUpdate(prevProps, prevState) {
  // winBalls가 세팅되지 않은 경우 (한번더로 초기화 한 후)
  if (this.state.winBalls.length === 0) {
    this.runTimeouts();
  }
}
...

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

  • useEffect의 두 번째 인자가
    • 빈 배열이면 componentDidMount와 동일
    • 요소가 있으면 componentDidMount, componentDidUpdate 둘 다 수행
  • Lotto.jsx를 Hooks로 변경
import React, { useState, useRef, useEffect } 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.splice(0, 6).sort((p, c) => p - c);
    return [...winNumbers, bonusNumber];
}

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

    useEffect(() => {
        for(let i = 0 ; i < winNumbers.length - 1; i++){
            timeouts.current[i] = setTimeout(() => {
                setWinBalls((prevBalls) => [...prevBalls, winNumbers[i]]);
            }, (i + 1) * 1000); // 1초 간격으로 출력
        }
        timeouts.current[6] = setTimeout(() => {  // 보너스공
            setBonus(winNumbers[1]);
            setRedo(true);
        }, 7000);
        return () => {
            timeouts.current.forEach((v) => {
                clearTimeout(v);
            });
        }
    }, [timeouts.current]); // winBalls.length === 0으로 설정하면 처음 실행부터 적용되므로 중복된 숫자가 2번 나옴

    const onClickRedo = () => {  // 초기화
        setWinNumbers(getWinNumbers());
        setWinBalls([]);
        setBonus(null);
        setRedo(false);
        timeouts.current = [];
    };

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

export default Lotto;

6. useMemo와 useCallback

1) useMemo

  • 함수의 리턴값을 기억
  • getNumbers가 반복 실행되지 않도록 실행한 결과값을 임의로 저장해두는 데 사용

Lotto.jsx

const Lotto = () => {
    const lottoNumbers = useMemo(() => getWinNumbers(), []);
    const [winNumbers, setWinNumbers] = useState(lottoNumbers);
  ...

2) useCallback

  • 함수 자체를 기억
  • 실행 자체가 부담이 되는 함수에 사용
  • 자식 컴포넌트에 함수를 전달하는 경우 useCallback 사용 필수
    👉 넘겨주는 함수는 항상 같기 때문에 리렌더링 방지
  • useCallback에서 쓰이는 state를 두 번째 인자에 넣어주어야 변경을 감지

Lotto.jsx

const onClickRedo = useCallback(() => {  // 초기화
  setWinNumbers(getWinNumbers());
  setWinBalls([]);
  setBonus(null);
  setRedo(false);
  timeouts.current = [];
}, [winNumbers]);

6. Hooks에 대한 자잘한 팁들

  • 선언 순서가 매우 중요!
  • 조건문 안에는 절대 🙅‍♀️
  • 함수나 반복문 안에도 웬만하면 🙅‍♀️
  • useEffect에서 componentDidUpdate 기능만 사용하고 싶은 경우
   const mounted = useRef(false);
    useEffect(() => {
        if(!mounted.current){
            mounted.current = true;
        }else{
            // ajax
        }
    }, [바뀌는값]);
profile
목표는 행복한 베짱이

0개의 댓글