[React] 웹 게임-6 로또 추첨기

ji_silver·2020년 8월 10일
0

[React] 웹 게임

목록 보기
6/8
post-thumbnail

1. 로또 추첨기 컴포넌트

Lotto.jsx

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

function getWinNumbers() {
    console.log('getWinNumbers');
    const candidate = Array(45).fill().map((v, i) => i + 1); // 45개의 숫자를 candidate 배열에 넣기
    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, //재실행
    };

    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'

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 여러 번 사용하기

  • for문에 let 사용 시 클로저 문제 발생 x
  • setTimemout() 사용 시 끝나고 항상 clear 해줘야 함 (메모리 누수 문제 발생)
...
timeouts = [];

componentDidMount() {
    const { winNumbers } = this.state;
    for (let i = 0; i < winNumbers.length - 1; i++) { // 보너스 공 때문에 1 빼줌
        this.timeouts[i] = setTimeout(() => {
            this.setState((prevState) => {
                return {
                    winBalls: [...prevState.winBalls, winNumbers[i]],
                };
            });
        }, (i + 1) * 1000); // 순서대로 1초, 2초, 3초 ...
    }
    this.timeouts[6] = setTimeout(() => {
        this.setState({
            bonus: winNumbers[6], // 마지막공이 보너스 공
            redo: true,
        });
    }, 7000);
}

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

3. componentDidUpdate

componentDidUpdate()를 이용하여 버튼 클릭 시 재실행하게 하기

...
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() {
    this.runTimeouts();
}

componentDidUpdate(prevProps, prevState) { // 전 props, 전 state값이 바뀔 때 실행
    if (this.state.winBalls.length === 0) { // redo 클릭 시 winBalls는 빈 배열
        this.runTimeouts();
    }
}

onClickRedo = () => {
    this.setState({
        winNumbers: getWinNumbers(),
        winBalls: [],
        bonus: null,
        redo: false,
    });
    this.timeouts = [];
};

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

Class를 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); // 45개의 숫자를 candidate 배열에 넣기
    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 [winNumbers, setWinNumbers] = useState(getWinNumbers());
    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 () => { // componentWillUnMount는 return
            timeouts.current.forEach((v) => {
                clearTimeout(v);
            });
        };
    }, [timeouts.current]); // 빈 배열이면 componentDidMount와 동일
    //배열에 요소가 있으면 componentDidMount랑 componentDidUpdate 둘 다 수행

    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;
  • timeouts.current[i] = ... 는 current 배열에 요소를 넣어준거라 바뀌지 않고, timeouts.current = [] 는 current에 직접 넣어 값이 바뀜

5. useMemo와 useCallback

1) useMemo

  • 복잡한 함수 결과값을 기억하는 함수
  • state값이 바뀔 때마다 랜더링 되면서 호출되기 때문에 결과값을 기억한 후 재사용가능
const Lotto = () => {
    const lottoNumbers = useMemo(() => getWinNumbers(), []);
    const [winNumbers, setWinNumbers] = useState(lottoNumbers);
    ...

2) useCallback

  • useMemo는 결과값을 재사용 할 때 사용하지만, useCallback은 함수 자체를 기억해둬서 재실행되도 새로 생성되지 않고 재사용
  • 단, useCallback안에서 state 사용 시 배열 안에 넣어줘야 함. 배열 안에 값이 바뀌면 새로 실행
  • 자식 컴포넌트에게 props로 함수를 넘길 때 꼭 사용
const onClickRedo = useCallback(() => {
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);
    timeouts.current = [];
}, [winNumbers]);

Hooks에 대한 자잘한 팁들

  • Hooks는 순서가 매우 중요!
  • 조건문 안에 절대 넣으면 안됨, 함수나 반복문 안에도 웬만하면 넣지 말기

profile
🚧개발중🚧

0개의 댓글