React 스터디 6주차

RIHO·2022년 2월 25일

React STUDY

목록 보기
7/11
post-thumbnail

1. setTimeOut

  • 부모 컴포넌트가 자식 컴포넌트를 삭제할 때 늘 setTimeOut을 clear 해 주어야 한다. 그렇지 않으면 메모리 문제 및 에러가 발생할 수 있다.
  • 따라서 componentWillUnmount를 이용하여 정리해 주어야 한다.
  • setInterval 또한 마찬가지!
    componentWillUnmount() {
        this.timeouts.forEach((v) => {
            clearTimeout(v);
        })
    }

2. 로또 추첨기 (class)

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 < this.state.winNumbers.length - 1; i++) {
            // let을 사용하면 클로저 문제 X
            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();
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('didUpdate');
        if(this.state.winBalls.length === 0) {
            // winBalls의 길이가 0이 될 때 componentDidUpdate 실행 
            // 조건문으로 감싸지 않으면 매번 실행됨
            this.runTimeouts();
        }
    }

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

    onClickRedo = () => { // state 초기화 
        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;

3. 로또 추첨기 (Hooks)

useEffect를 사용하여 componentDidUpdate 기능을 구현하는 것이 주 목적이다.

import React, { useState, useRef, useEffect } from 'react';
import Ball from './ball';

function getWinNumbers() {
    // state를 사용하지 않는 함수는 분리
    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 [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++) {
            // let을 사용하면 클로저 문제 X
            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]); // input이 빈 배열이면 componentDidMount와 동일
    // 배열에 요소가 있으면 componentDidMount와 componentDidUpdate 둘 다 수행
    // componentWillUnmount 는 return

    const onClickRedo = () => { // state 초기화 
        console.log('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;

4. useMemo


3번 항목까지 진행하였을 때 콘솔 창을 보면, getWinNumbers가 연속으로 실행되고 있다. Hooks의 특성 때문에, 함수 전체가 처음부터 다시 실행되는 현상이 반복되기 때문이다.

이때 사용하는 것이 useMemo이다. 함수를 다시 실행시키지 않고 기억할 수 있게끔 한다.


두번째 요소가 바뀌기 전까지는 계속 같은 값을 기억한다.

Hooks가 getWinNumbers의 리턴값을 기억하고 있으므로, getWinNumbers는 한 번만 실행된다.

useMemo : 복잡한 함수의 결과값을 기억
useRef : 일반 값을 기억

5. useCallback

앞서 배운 useMemo가 값(함수의 리턴값)을 기억하는 Hooks이라면, useCallback은 함수 자체를 기억하는 것이다.

이와 같이 onClickRedo 함수를 useCallback으로써 기억하면, 함수 컴포넌트가 재실행되더라도 onClickRedo가 새로 생성되지 않는다.

자식 컴포넌트에 props로 함수를 넘겨줄 때에는 useCallback을 사용하는 것이 좋다. 그렇지 않으면 렌더링할 때마다 매번 새로운 함수가 생성된다.

6. 기타 팁

  • Hooks는 순서가 매우 중요하므로, 최상위에 선언하는 것이 좋다. Hooks 선언을 조건문 안에 넣어서는 안 되며, 함수나 반복문 안에 넣는 것 또한 바람직하지 않다. useEffect 내부에서도 마찬가지이다.
profile
Front-End / 기록용

0개의 댓글