As i wish

[React Lotto] useEffect, useMemo, useCallback 본문

React JS

[React Lotto] useEffect, useMemo, useCallback

어면태 2019. 7. 11. 23:36

안녕하세요. 엄티입니다. 오늘은 Lotto Game 을 만들어 보겠습니다.

제 포스팅은 '제로초' 님의 웹게임 강좌를 기반으로 제가 복습한 형태로 진행 됩니다.

 

제로초님의 웹게임 강좌

 

리액트 무료 강좌(웹게임) - YouTube

 

www.youtube.com

 

일단 지난번에 라이프사이클에 대하여 배웠는데, 오늘은 조금 더 자세하게 배웠습니다.

일단 기본적인 라이프 사이클은

 

constructor -> render -> ref -> componentDidMount -> (setState/props changed), shouldComponentUpdaet(true) -> render -> componentDidUpdate -> (부모가 없앨 때) componentWillUnmount -> 소멸

이런식이고요 지난 포스팅을 보시고 싶으신 분은 링크를 눌러주세요.

 

라이프사이클

 

[React 가위, 바위, 보] 라이프 사이클

오늘은 가위바위보 게임을 바탕으로 React에 라이프 사이클, 고차 함수, Hooks useEffect에 대하여 포스팅 해보겠습니다. 계속 해서 언급 하지만 제 포스팅은 '제로초' 님의 리액트 강좌를 바탕으로 합니다. 제로..

eomtttttt-develop.tistory.com

무튼 오늘은 Hooks에 대하여 더 자세히 알아보고 지난번에 하지 않았던 componentDidUpdate에 대하여 알아봤습니다.

일단 오늘 만들 게임은 다음과 같습니다.

위 처럼 WinBalls 가 6개 나오노 Bonus Ball 이 하나 더 나온 뒤 Redo 버튼이 생겨서 다시 시작할 수 있는 그런 게임이죠.

그럼 코드로 보겠습니다.

자세한 설명은 코드에 해놨으니 확인해 주세요 .

프로젝트 세팅은 늘 동일합니다.

 

리액트 프로젝트 세팅

 

[React JS] 프로젝트 셋팅 및 webpack, hot loader적용

리액트 프로젝트를 수동으로 셋팅 할 때에 필요한 프로젝트 셋팅과 개발의 편의를 위한 웹팩 적용을 포스팅 해보겠습니다. 기존 포스팅 한것을 한곳에 모은 것이기 때문에 링크를 클릭하여 확인해 주시면 되겠습니..

eomtttttt-develop.tistory.com

 

Lotto.jsx

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

const getWinNumbers =() => {
  console.log('Get win numbers');

  const candidates = Array(45).fill().map((v, i) => i + 1); // Make 1 ~ 45 Arrays
  const shuffled = [];

  while(candidates.length > 0) {
    shuffled.push(candidates.splice(Math.floor(Math.random() * candidates.length), 1)[0]);
  } // Splice candidates Array when candidates length is 0

  const winNumbers = shuffled.slice(0, 6); // Slice 6
  const bonus = shuffled[shuffled.length - 1]; // Slice last element

  return [...winNumbers, bonus]; // Combine two element
}

class Lotto extends Component {
  state = {
    winNumbers: getWinNumbers(),
    winBalls: [],
    bonus: null,
    redo: false,
  }

  timeouts = [];

  componentDidMount() {
    this.setWinBallsTimers(); // Start timer
  }

  componentDidUpdate (prevProps, prevState) {
    console.log('Component did update');
    if (this.state.winBalls.length === 0) {
      this.setWinBallsTimers(); // When call winBalls state length is 0
    }
  }

  componentWillUnMount() {
    this.timeouts.forEach((v) => {
      clearTimeout(v); // Clear timers
    });
  }

  setWinBallsTimers = () => {
    const { winNumbers } = this.state;

    winNumbers.forEach((item, index) => {
      this.timeouts = setTimeout(() => {
        if (index === winNumbers.length - 1) {
          this.setState({
            bonus: item,
            redo: true
          }); // Set bonus ball and set redo(Show Redo button)
        } else {
          this.setState((prevState) => {
            return {
              winBalls: [...prevState.winBalls, item]
            };
          });
        }
      }, (index + 1) * 500) // Set timeout by index
    });
  }

  setBonusNumber = () => {
    const { bonus, redo } = this.state;
    // Using if statement
    return (
      <>
        {!!bonus ? <Ball number={bonus}/> : null}
        {!!redo ? <button onClick={this.clickRedo}>Redo</button> : null}
      </>
    )
  }

  clickRedo = () => {
    console.log('Click redo');
    this.setState({
      winNumbers: getWinNumbers(),
      winBalls: [],
      bonus: null,
      redo: false,
    }); // Clear states
    this.timeouts = []; // Clear timers
  }

  render() {
    const { winBalls } = this.state;

    return (
      <>
        <div>
          WinBalls
        </div>
        {
          winBalls.map((value) => { // Seperate by for loop
            return <Ball key={value} number={value}/>
          })
        }
        <div>
          Bonus Ball
        </div>
        {this.setBonusNumber()}
      </>
    );
  }
}

export default Lotto;

 

Ball.jsx 는 두 가지 방법으로 만들어 봤는데요. 함수형과 Class 형 필요하신걸로 쓰시면 될 것 같네요.

Ball.jsx

import React, { PureComponent, Component } from 'react';

// Same with PureComponent
const Ball = React.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>
  )
});

// class Ball extends PureComponent {

//   getBackgroundColor(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 background;
//   }

//   render() {
//     const { number } = this.props;

//     return (
//       <>
//         <div className="ball" style={{'backgroundColor': this.getBackgroundColor(number)}}>{number}</div>
//       </>
//     )
//   }
// }

export default Ball;

 

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Lotto</title>
    <style>
        .ball {
            display: inline-block;
            border: 1px solid black;
            border-radius: 20px;
            width: 40px;
            height: 40px;
            line-height: 40px;
            font-size: 20px;
            text-align: center;
            margin-right: 20px;
        }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <!-- 웹팩으로 빌드한 ./dist/app.js 파일 -->
    <!-- <script src="./dist/app.js"></script> -->

    <!-- webpack-dev-server 사용시 dist 폴더 사용 안함 -->
    <!-- but webpack.config.js 에서 publicPath 사용시 dist 폴더 사용 가능 -->
    <script src="./app.js"></script>
  </body>
</html>

client.jsx

import React from 'react';
import ReactDOM from 'react-dom';

import { hot } from 'react-hot-loader/root';

import Lotto from './Lotto'; // Class 사용
import LottoHook from './Lotto-hook'; // Hooks 사용

// const Hot = hot(Lotto);
const Hot = hot(LottoHook);

ReactDOM.render(<Hot />, document.getElementById('root')); // Class 사용

 

그 다음 제일 중요한 hooks 입니다.

Lotto-hook.jsx

import React from 'react';
import Ball from './Ball';

const { useState, useRef, useEffect, useMemo, useCallback } = React;

const getWinNumbers = () => {
  console.log('Get win numbers');

  const candidates = Array(45).fill().map((v, i) => i + 1); // Make 1 ~ 45 Arrays
  const shuffled = [];

  while(candidates.length > 0) {
    shuffled.push(candidates.splice(Math.floor(Math.random() * candidates.length), 1)[0]);
  } // Splice candidates Array when candidates length is 0

  const winNumbers = shuffled.slice(0, 6); // Slice 6
  const bonus = shuffled[shuffled.length - 1]; // Slice last element

  return [...winNumbers, bonus]; // Combine two element
}

const LottoHook = () => {
  // useMemo is remind variable when changed second paramter
  // But this is [], so call once
  // If not around getWinNumbers by useMemo, getWinNumbers is call when every rerendering
  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('Use Effect by timeouts current');

    setWinBallsTimers();
    return () => {
      console.log('Clear timeouts current');
      timeouts.current.map((v) => {
        clearTimeout(v);
      });
    }
  }, [timeouts.current]);
  // Call by componentDidMount, componentDidUpdate(When timeouts.current is changed), componentWillUnmount(return)

  useEffect(() => {
    console.log('Same with componentDidMount');
  }, []) // Same with componentDidMount because second parameter is []

  const setWinBallsTimers = () => {
    winNumbers.forEach((item, index) => {
      timeouts.current[index] = setTimeout(() => {
        if (index === winNumbers.length - 1) {
          setBonus(item);
          setRedo(true);
        } else {
          setWinBalls((prevWinBalls) => {
            return [...prevWinBalls, item]
          });
        }
      }, (index + 1) * 500)
    });
  }

  const setBonusNumber = () => {
    return (
      <>
        {!!bonus ? <Ball number={bonus}/> : null}
        {!!redo ? <button onClick={clickRedo}>Redo</button> : null}
      </>
    )
  }

  // useCallback is remind function when winNumbers revised
  const clickRedo = useCallback(() => {
    console.log('Click redo');
    console.log('Win numbers', winNumbers);
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);

    timeouts.current = [];
  }, [winNumbers]);

  return (
    <>
      <div>
        WinBalls
      </div>
      {
        winBalls.map((value) => {
          return <Ball key={value} number={value}/>
        })
      }
      <div>
        Bonus Ball
      </div>
      {setBonusNumber()}
    </>
  );
}

export default LottoHook;

 

주의 깊게 보셔야 할 점이 useEffect, useMemo, useCallback 인데요. useEffect 는 지난 시간에 포스팅 했지만 다시 정리해 보겠습니다.

 

* useEffect: 안에 내용을 실행한다. 두 번째 인자가 바뀔 때까지, 즉 두 번째 인자가 빈 배열일 경우 componentDidMount 와 같은 역할을 한다. componentDidUpdate 에서는 안에 if 문을 사용하여 어떤 변수가 바뀔 때 만 특정 함수를 실행해 주도록 제어해주었다면, useEffect 는 변수를 기준으로 여러개를 만들어 줄 수 있다. 예를 들어 timeouts.current 가 바뀔 때 해야하는일, winNumbers 가 바뀔 때 해야하는일 등등 각각의 변수가 바뀔 때 해야하는 일을 정의할 때 useEffect를 사용한다. 따라서 여러개의 useEffect를 사용 할 수 있다.

 

* useMemo: 어떤 함수의 리턴 값을 기억한다. 두 번째 인자가 바뀔 때까지, 즉 두 번째 인자가 바뀔 때 까지 그 함수의 리턴값을 기억하고 다시 실행하지 않는다. Hooks는 State가 바뀔 때 마다 다시 실행하는 경향이 있는데  useMemo에 두 번째 인자로 빈 배열을 넣어주면 다시 불리지 않는다. 만약 두 번째 인자가 바뀌게 되면 다시 useMemo로 감싸져 있는 함수가 실행 된다.

 

* useCallback: 어떤 함수 자체를 기억한다. 두 번째 인자가 바뀔 때 까지, 즉 두 번째 인자가 바뀔 때 까지 함수 자체를 기억하기 때문에 위에 코드에서 만약 두 번째 인자를 빈 배열로 하게 되면 함수가 다시 생성되지 않아서 함수 내의 값 (예를 들어 로그에 winNumbers)가 같은 값으로 유지 된다. 따라서 이러한 경우는 어떤 함수를 props로 자식 component 에 넘겨 줄 때 감싸면 좋다. 이유는 만약 useCallback으로 감싸지 않으면 함수가 자꾸 생성되고 그 함수를 props로 받은 자식들은 계속해서 렌더링을 하기 때문에 좋지 못하기 때문이다.

 

이정도로 정리할 수 있을것 같네요.

 

그럼 마지막으로 폴더 구조만 보고 정리하겠습니다.

 

 

제 코드는 저의 깃허브에서 보실 수 있습니다.

 

깃허브 주소

 

paulo-eomttt/react-study

React study by react web game. Contribute to paulo-eomttt/react-study development by creating an account on GitHub.

github.com

 

Comments