As i wish

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

React JS

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

어면태 2019. 7. 8. 09:43

오늘은 가위바위보 게임을 바탕으로 React에 라이프 사이클, 고차 함수, Hooks useEffect에 대하여 포스팅 해보겠습니다.

계속 해서 언급 하지만 제 포스팅은 '제로초' 님의 리액트 강좌를 바탕으로 합니다.

 

제로초님의 웹게임 강좌

 

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

 

www.youtube.com

먼저 라이프사이클에 대하여 알아보겠습니다.

리액트 라이프 사이클은 하나의 component의 생명 주기를 의미 하는데요.

처음 component가 rendering 되고 없어 질 때 까지 를 의미 합니다.

여러가지가 있는데, 기본적으로

componentDidMount, componentDidUpdate, componentWillUnMount

요 3가지가 가장 중요하죠.

대략적인 의미는 다음과 같습니다.

componentDidMount: render 가 실행되고 그 다음에 실행, 리랜더링 일어날 시에는 실행 안됨. Component 가 첫 렌더링 된 후
   - 비동기 요청을 많이 한다.

* componentWillUnmount: Component 가 제거되기 직전
   - 비동기 요청 정리 한다.

* componentDidUpdate: 리랜더링 후 실행

 

더 자세하게 알고 싶으면 다른 블로그를 참조 해주세요.

리액트 라이프사이클

 

[React.JS] 강좌 7편 Component LifeCycle API | VELOPERT.LOG

이 튜토리얼은 2018년에 새로운 강의로 재작성되었습니다 [새 튜토리얼 보기] 이번 강좌에서는 React.js 컴포넌트의 LifeCycle API 에 관하여 배워보겠습니다. LifeCycle API는, 컴포넌트가 DOM 위에 생성되기 전 후 및 데이터가 변경되어 상태를 업데이트하기 전 후로 실행되는 메소드 들 입니다. 이 메소드를 왜 쓰겠어.. 쓸일이 있겠어.. 라고 생각 하실 수 있는데, 가끔 이를 사용하지 않으면 해결 할 수 없는 난관에 가끔 부딪치기

velopert.com

무튼 이름만 봐도 쉽게 무슨 의미인지는 알 수 있죠.

기본적인 순서를 보자면

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

이런식이라고 보시면 됩니다. 

 

오늘 만들어 볼 게임에 대한 결과부터 보겠습니다.

이렇게 그림이 가위, 바위, 보 순서로 계속해서 변화하고, 사용자가 밑에 버튼을 눌러서 이겼는지, 졌는지, 비겼는지를 알아보는 단순한 게임이죠.

 

프로젝트 셋팅 및 webpack, hot loader 적용

 

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

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

eomtttttt-develop.tistory.com

오늘은 Class, hooks 를 각각 RcpGame.jsx, RcpGameHook.jsx 로 만들어 보겠습니다.

 

RcpGame.jsx

import React, { Component } from 'react';

// 클래스의 경우 -> constructor -> render -> ref -> componentDidMount
// (setState/props 바뀔때) -> shouldComponentUpdate(true) -> render -> componentDidUpdate
// 부모가 나를 없앴을 때 -> componentWillUnmount -> 소멸

const rspCoords = {
  rock: '0',
  cissor: '-142px',
  paper: '-284px',
};

const scores = {
  rock: 1,
  cissor: 0,
  paper: -1,
};

const computerChoice = (imageCoord) => {
  return Object.entries(rspCoords).find((value) => {
    return value[1] === imageCoord;
  })[0];
};

class RcpGame extends Component {
  state = {
    imageCoord: rspCoords.rock,
    result: '',
    score: 0,
  };

  interval;

  componentDidMount() {
    this.interval = setInterval(this.changeHand, 100);
  }

  componentDidUpdate () {
    // Nothing
  }

  componentWillUnMount() {
    clearInterval(this.interval);
  }

  changeHand = () => {
    const { imageCoord } = this.state;

    if (imageCoord === rspCoords.rock) {
      this.setState({
        imageCoord: rspCoords.cissor
      });
    } else if (imageCoord === rspCoords.cissor) {
      this.setState({
        imageCoord: rspCoords.paper
      });
    } else if (imageCoord === rspCoords.paper) {
      this.setState({
        imageCoord: rspCoords.rock
      });
    }
  };

  onClickBtn = (choice) => () => {
    const { imageCoord } = this.state;
    clearInterval(this.interval);

    const userScore = scores[choice];
    const compScore = scores[computerChoice(imageCoord)];

    const scoreDiff = userScore - compScore;

    if (scoreDiff === 0) {
      this.setState({
        result: 'Draw, Select: ' + choice
      });
    } else if (scoreDiff === 1 || scoreDiff === -2) {
      this.setState((prevState) => {
        return {
          result: 'Win, Select: ' + choice,
          score: prevState.score + 1
        };
      });
    } else {
      this.setState((prevState) => {
        return {
          result: 'Loose, Select: ' + choice,
          score: prevState.score - 1
        };
      });
    }

    setTimeout(() => {
      this.interval = setInterval(this.changeHand, 100);
    }, 1000);
  }

  render() {
    const { imageCoord, result, score } = this.state;

    return (
      <>
        <div id="computer" style={{ backgroundImage: 'url(https://en.pimg.jp/023/182/267/1/23182267.jpg)', backgroundPosition: `${imageCoord} 0`}} />
        <div>
          <button id="rock" className="btn" onClick={this.onClickBtn('rock')}>바위</button>
          <button id="scissor" className="btn" onClick={this.onClickBtn('cissor')}>가위</button>
          <button id="paper" className="btn" onClick={this.onClickBtn('paper')}>보</button>
        </div>
        <div>{result}</div>
        <div>현재 {score}점</div>
      </>
    );
  }
}

export default RcpGame;

자세한 코드 설명은 주석으로 대체 하였지만, 코드에서 보심과 같이 componentDidMount 에서 interval 를 통해 그림이 계속 바뀌도록 했고 만약 component의 생명이 끝나게 되면 componentWillUnmount 를 통하여 interval를 제거 해줬습니다.

만약 interval를 제거해주지 않는다면, 계속해서 interval이 살아있어서 2개, 3개, 4개 등 무한대로 interval이 돌겠죠?

 

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>RcpGame</title>
    <style>
        #computer {
            width: 142px;
            height: 200px;
            background-position: 0 0;
        }
    </style>
    <style>
    </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>

 

간단한 스타일이 들어가 있는 index.html

 

client.jsx

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

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

// import RcpGame from './RcpGame'; // Class 사용
import RcpGameHook from './RcpGameHook'; // Hooks 사용

// const Hot = hot(RcpGame);
const Hot = hot(RcpGameHook);

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

 

마지막으로 hooks 로 바꾼 코드 입니다.

 

RcpGameHook.jsx

import React from 'react';
const { useState, useRef, useEffect } = React;

// 클래스의 경우 -> constructor -> render -> ref -> componentDidMount
// (setState/props 바뀔때) -> shouldComponentUpdate(true) -> render -> componentDidUpdate
// 부모가 나를 없앴을 때 -> componentWillUnmount -> 소멸

const rspCoords = {
  rock: '0',
  cissor: '-142px',
  paper: '-284px',
};

const scores = {
  rock: 1,
  cissor: 0,
  paper: -1,
};

const computerChoice = (imageCoord) => {
  return Object.entries(rspCoords).find((value) => {
    return value[1] === imageCoord;
  })[0];
};

const RcpGame = () => {
  const [imageCoord, setImageCoord] = useState(rspCoords.rock);
  const [result, setResult] = useState('');
  const [score, setScore] = useState(0);

  const interval = useRef(null);

  // 첫 번째 인수는 함수 두 번째 인수는 변수인데, 이 두 번째 인수가 클로저 문제를 해결해 준다.
  // 즉, 두 번째 인수 배열에 넣은 값 들이 바뀔 때에 useEffect 가 실행된다.
  useEffect(() => { // componentDidMount, componentDidUpdate 역할 (1대1 대응은 아님)
    console.log('Restart');
    // useRef: Use .current
    interval.current = setInterval(changeHand, 500000);
    return () => { // componentWillUnmount 역할
      console.log('End');
      clearInterval(interval.current);
    }
  }, [imageCoord]);

  const changeHand = () => {
    if (imageCoord === rspCoords.rock) {
      setImageCoord(rspCoords.cissor);
    } else if (imageCoord === rspCoords.cissor) {
      setImageCoord(rspCoords.paper);
    } else if (imageCoord === rspCoords.paper) {
      setImageCoord(rspCoords.rock);
    }
  };

  const onClickBtn = (choice) => () => {
    clearInterval(interval.current);

    const userScore = scores[choice];
    const compScore = scores[computerChoice(imageCoord)];

    const scoreDiff = userScore - compScore;

    if (scoreDiff === 0) {
      setResult('Draw, Select: ' + choice);
    } else if (scoreDiff === 1 || scoreDiff === -2) {
      setResult('Win, Select: ' + choice);
      setScore((prevScore) => {
        return prevScore + 1;
      });
    } else {
      setResult('Loose, Select: ' + choice);
      setScore((prevScore) => {
        return prevScore - 1;
      });
    }

    setTimeout(() => {
      interval.current = setInterval(changeHand, 100);
    }, 1000);
  }

  return (
    <>
      <div id="computer" style={{ backgroundImage: 'url(https://en.pimg.jp/023/182/267/1/23182267.jpg)', backgroundPosition: `${imageCoord} 0`}} />
      <div>
        <button id="rock" className="btn" onClick={onClickBtn('rock')}>바위</button>
        <button id="scissor" className="btn" onClick={onClickBtn('cissor')}>가위</button>
        <button id="paper" className="btn" onClick={onClickBtn('paper')}>보</button>
      </div>
      <div>{result}</div>
      <div>현재 {score}점</div>
    </>
  );
};

export default RcpGame;

 

hooks 에선 component 시리즈를 사용하지 않고 useEffect 를 사용하는데요. 이는 componentDidMount, componentDidUpdate, componentWillUnmount 역할을 한번에 처리 합니다.

 

다음과 같이 useEffect 에 첫 번째 인수는 함수, 두 번째 인수는 변수인데, 이 배열에 넣은 변수 들이 바뀔 때에 useEffect가 실행 됩니다. 

그래서 실제 로그를 찍어 보시면 어떻게 동작하시는지 쉽게 파악 하실 수 있습니다.

 

RcpGame folder structure

Comments