React 라이브러리

10. 가위 바위 보 게임 만들기: 라이프 사이클, 고차함수와 Q&A

문정훈 2021. 11. 18. 02:30

1-1. 컴포넌트의 라이프 사이클

class C1 extends Component {
  state = {
   
  };

  interval;

  changeHand = () => {
    ...
  }
  //처음 1번만 랜더링이 성공하면 호출됨-> 비동기 요청을 주로함
  componentDidMount() {
    this.interval = setInterval(this.changeHand, 100);
  }

  //리 랜더링될 때 호출되는 메소드이다.
  componentDidUpdate() {

  }
  //컴포넌트가 제거되기 직전에 호출된다.  -> 비동기 요청 정리를 주로함
  comopnentWillUnmount() {
    clearInterval(this.interval);
  }
  
  render() 
    return {
    ....
    }
}

1) componentDidMount

render 가 일어나면 리액트가 해당 컴포넌트를 jsxDOM에 붙이는데 붙이는 순간에 특정 행동을 지정할 수 있다.

이것은 render가 처음 실행되고 render가 성공적으로 실행되었다면 이 메소드가 실행된다. 이 메소드는 setState로 재랜더링이 될 때는 수행되지 않는다.

 

2) componentWillUnmount

컴포넌트가 제거 될 수 있는 상황이 있다. 그 상황은 부모가 자식 컴포는트를 없애는 경우가 있다. 컴포넌트가 제거 되기 직전에 호출되는 메소듸이다.

 

3) componentDidUpdate

재랜더링될 때 호출되는 메소드이다.

 

3가지 메소드를 언제 주로 사용하냐?

여태까지는 어떤 이벤트가 발생하면 그 이벤트(함수)로 인해 setState를 발생시키고 컴포넌트의 state의 변화를 줌으로써 랜더링을 다시하였다.

 

하지만 사용자가 어떤 특정 동작을 수행하지 않고 setState를 호출할 수 있는 방법 중 하나로

3가지 메소드를 활용하면 된다.

 

예를 들어 화면에 가위 바위 보 이미지를 계속 해서 변화하면서 보여주고 싶은 경우

3가지 메소드에서 setState를 호출하여 랜더링을 자동으로 반복적으로 해줄 수 있다.

 

정리하면

3가지 메소드를 컴포넌트의 라이프 사이클이라고 통칭하며

컴포넌트가 생성되고 사라지고 업데이트 되는데 특정 행동(작업)을 취할 수 있게 해주는 메소드들이다.

 

1-2. 컴포넌트의 라이프 사이클 순서

클래스의 경우 클래스가 생성되면 아래 순서대로 라이프 사이클이 실행된다.

경우1)

constructor -> render -> ref -> componentDidMount -> render(재랜더링) ->
shouldComponentUpdate ->componentDidMount

 

경우2)

constructor -> render -> ref -> componentDidMount -> ComponentWillUnmount

 

 

위 두 경우를 같이 설명하면

처음 클래스가 호출되면 생성자가 우선 실행되고 render 함수가 실행될 것이다.

render 안에 태그의 ref 속성이 실행되고 componentDidMount 1번 실행된다.

여기서 만약 부모에 의해 컴포넌트가 소멸되면 경우2)의 흐름과 같이 되고

재런더링이 일어난다면 shouldComponentUpdate 의 호출로 리랜더링 여부를 결정하고(return true라면 랜더링을 다시함.)

그리고 재랜더링이 이루어진다면 componentDidMount가 호출된다.

 

 

1-3. 라이프 사이클에 setInterval 적용해 가위 바위보 게임 만들기

예를 들어 위 코드와 같이 주로 componentDidMount에서는 비동기 요청 작업이 이루어진다. setInterval과 같은 비동기 호출은 clearInterval을 해주지 않으면 컴포넌트가 제거 되어도 이벤트 큐에서 계속해서 이벤트 루프에 의해 콜스택으로 불러와져 실행되는 (메모리 누수)의 문제점이 발생한다.

따라서 comopnentWillUnmount내부에서는 주로 비동기 호출의 정리가 이루어진다.


2-1 고차함수와 Q&A

1) 자바스크립트에서 태그에 직접 이벤트 등록

<button id = "button1" onClick = "func('hello')">Click!</Button>

자바스크립트에서 html 태그에 이벤트를 지정한 방법을 작성한 것인데 위 코드는 실제로 아래와 같은 함수를 생성 시키는 것이다. 

function onClick(event) {
  func('hello');
}

 

 

따라서 func메소드 내에서는 event값을 사용할 수 있는 것이다. 

event.target은 click 이벤트트를 호출한 태그를 가리키게 되는데 예를 들어 아래 코드를 보자

func 함수 내부에서 아래와 같이 호출이 가능해진다. 

function func(v) {
  console.log(v); // 'hello'
  console.log(event.target.id); // 'button1'
}

 

 

 

2) 리액트에서 태그에 직접 이벤트 등록

<button id ='rock' className = 'btn' onClick={(event) => this.onClickBtn('바위')}>바위</button>

render 함수 내에 위와 같은 코드가 있다고 가정.

 

function onClick(event) {
  tempFunc = (event) => {
    this.onClickBtn('바위');
  }
}

그럼 위 코드와 같이 함수가 생성된다고 보면 되고 화살표 함수로 event를 넘겨받았으므로 onClickBtn함수에서 event.target.id하면 'rock' 문자열을 얻게 된다. 

 

 

3) 고차함수

class C1 extends Component {

  onClickBtn = (clickValue) => {
    console.log(cilckValue);
    console.log(event.target.id);
  }
  
  render() {
     ...
     <button id ='rock' className = 'btn' onClick={(event) => this.onClickBtn('hello')}>hello</button>
     ...
  }
}

위 첫 코드는 리액트 컴포넌트의 render 함수 내부의 선언이고 두번 째 코드는 컴포넌트 클래스의 메소드를 가진 프로퍼티라고 할 때 위 두 코드와 같이 작성이 될 수 있는데 위 코드를 아래와 같이 수정할 수 있다. 

 

 

class C1 extends Component {

  onClickBtn = (clickValue) => (event) => {
    console.log(cilckValue);
    console.log(event.target.id);
  }
  
  render() {
     ...
     <button id ='rock' className = 'btn' onClick={this.onClickBtn('hello')}>hello</button>
     ...
  }
}

위 패턴은 리액트에서 자주 사용되는 고차함수 형태의 패턴이다. 

 


3-1 hooks에서 라이프 사이클: useEffect

아래 코드는 imgCord의 값이 setImgCord로 setState되면 묵, 찌, 빠 이미지가 출력된다.

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

const changeHand = () => {
   ...
}
const RSP = ()=> {
  const [result, setResult] = useState(''); 
  const [imgCord, setImgCord] = useState(rspCords.바위);
  const [score, setScore] = useState(0);
  const interval = useRef();

  useEffect(()=>{ // componentDidMount, componentDidUpdate 역할
     console.log('실행'):
     interval.current = setInterval(changeHand, 100);
     return () => { //componentWillUmmount 역할 
       console.log('중지'):
       clearInterval(interval.current);
     }
  }, [imgCord]);

  const changeHand = () => {
    const {result, score, imgCord} = this.state;
      if(imgCord === rspCords.바위) {
        this.setState({
          imgCord: rspCords.가위,
        });
      } 
      else if(imgCord === rspCords.가위)  {
        this.setState({
          imgCord: rspCords.보,
        });
      }
      else if(imgCord === rspCords.보) {
        this.setState({
          imgCord: rspCords.바위,
        });
      }
  }
}

<useEffect란>

hooks에서는 클래스에서 라이프 사이클 역할을 useEffet가 담당하며 위 코드와 같은 형태를 가진다. 

클래스에서 라이프 사이클은 hooks에서 일대일 대응응인 개념이 아니라 라이프 사이클 역할과 비슷한 역할로 생각하면 된다.

 

<useEffect 배열에 들어가는 값은?>

그리고 useEffect의 첫 매개변수는 콜백함수를 주고 두 번 째 매개변수로 배열을 지정하는데 배열에 들어가는 값은 무엇인가?

첫 번째 매개변수인 콜백함수에서 사용하는 클로저(비동기 함수의 콜백함수)에서 사용하는 프로퍼티를 적어준다. 

(두 번째 인자인 배열에 들어가는 값을 정리한 글은 11. 포스팅 읽으면 이해됨...)

위 코드 예시를 보면 

useEffet 역시 비동기함수이다. 따라서 클래스의 render가 다 찍히고 클래스가 종료하고 난 뒤 비동기 함수가 콜 스택으로 불려와 실행되는데 그때 비동기 함수 useEffetc의 "실행"이 진행된되다. 

 

<userEffect가 재 실행될 조건>

여기서 중요한 내용은 

우선 위 코드를 실행해보면 100초 단위로 console.log('실행'); console.log('종료'); 두 코드가 계속해서 실행된데 즉

useEffect가 계속해서 반복실행하게 된다. 그 이유는? 위 코드가 비동기와 랜더링이 어떻게 일어나는지 알아야한다.

※ 전제는 클래스로 선언된 컴포넌트는 랜더링이 다시 일어나면 render 함수가 다시 호출되는 것이지만 hooks에서는 함수 코드 전체가 다시 실행되는 것이다. !

위 코드의 실행 흐름을 비동기와 리랜더링 과점에 중점을 두고 살펴보면

우선 함수 코드가 처음 실행되면 useEffect가 실행되는데 이는 비동기 함수 이므로 이벤트 큐로 넘어가 "평가"가 끝나고 "실행" 하기 직전에 멈추고 잠시 대기한다. 

콜스택에서 RSP 함수의 코드의 실행이 끝나면 즉 첫 랜더링 작업이 끝나면 (이 말은 즉 콜 스택이 비게 된다.) 

이벤트 큐에 있던 비동기 호출(useEffect)가 불러오는데 이때 useEffect의 내용을 보면 0.1초 후에 changeHand 메소드를 호출한다. 

changeHand 내부에서는 setState를 사용하고 있므로 랜더링이 다시 일어나 함수 RSP 코드가 다시 실행된다. 

따라서 컴포넌트가 다시 실행되므로 //componentWillUmmount 역할을 하는 useEffect의 콜백함수가 호출되므로 console.log('종료"); 가 실행된다. 그리고 재랜더링에 의해 useEffect 코드가 다시 실행된다. 

 

정리하면 0.1초 간격으로 이미지의 전환이 이루어지는 것은 맞다. setInterval에 의해서 0.1초 간격으로 실행되는 것이 아닌 함수RSP 의 코드가 0.1초 간격으로 다시 실행(재랜더링) 되고 또한 0.1초 간격으로 setInterval이 생성되고 지워지고를 반복해서 화면에선 0.1초 간격으로 이미지가 전환되어 보이는 것이다.   

 

실행 순서 정리=>

  1. RSP 함수 첫 실행(첫 랜더링) 
  2. useEffect 첫 실행으로 setInterval이 호출됨(이벤트 큐->콜 스택 호출) 0.1초 후 changeHand 메소드 호출
  3. changeHand 내부에서 setState를 사용하므로 재랜더링
  4. claerInterval(interval.current) 코드가 실행되며 setInterval이 종료됨
  5. 함수 RSP가 다시 실행됨 
  6. useEffect 실행으로 setInterval이 호출됨(이벤트 큐->콜 스택 호출) 0.1초 후 changeHand 메소드 호출 -> ,,, 무한 반복 

위 1번부터 6번까지의 과정이 0.1초 동안 이루어지므로 (엄밀히는 0.1초가 딱 아니겠지만..) 화면에 랜더링이 마치 setInterval이 0.1초 간격으로 실행되는 것 처럼 보이는 것이다. 

 

 

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

const changeHand = () => {
   ...
}
const RSP = ()=> {
  const [result, setResult] = useState(''); 
  const [imgCord, setImgCord] = useState(rspCords.바위);
  const [score, setScore] = useState(0);
  const interval = useRef();

  useEffect(()=>{ // componentDidMount, componentDidUpdate 역할
     console.log('실행'):
     interval.current = setInterval(changeHand, 100);
     return () => { //componentWillummount 역할 
       console.log('중지'):
       clearInterval(interval.current);
     }
  }, [imgCord]);

  const changeHand = () => {
    console.log('hello');
  }
}

그럼 위와 같이 changeHand내부가 setState를 호출하지 않는 코드로 변겨해보았다. 

그럼 실행결과는?

"실행"이 호출되고 0.1초 간격으로 무한히 hello 만찍히고 화면에는 아무런 재랜더링이 없다. (이미지가 변경되지 않는다)

 

그 이유는 재랜더링이 없으므로 RSP 함수를 다시 실행하지도 않기 때문에 clearInterval 문을 포함한 콜백함수 역시 호출되지 않는다. 

즉 setInterval 비동기 메소드에 의해 0.1초 간격으로 console.log('hello')만 계속 실행되는 것이다. 

 

 

 

3-2. useEffect로 ComponentDidUpdate 구현하기

useEffect도 역시 두 번째 인자인 배열이 비어있다면 componentDidMount로만 동작을 하는데 

만약 두 번째 인자인 배열이 비어있다면 랜더링이 다시 일어날 때 useEffect의 콜백함수가 재호출되지 않는다.

 componentDidUpdate의 기능을 못한다. 

 

만약 useEffect의 두 번쨰 인자인 배열에 useCallback과 같이 변경되는 값이 선언되어 있다면 함수 컴포넌트가 재랜더링되어 함수 코드가 다시 실행 될 때 useEffec의 콜백함수가 호출된다. !!

만약 useEffect의 배열에 작성된 프로퍼티가 변경되지 않고, 다른 코드 부분에서 setState가 되어 재랜더링을 한다면?-> useEffect의 콜백함수가 다시 호출되지 않는다. 그 이유는 useEffect의 두 번째 인자인 배열 작성된 프로퍼티가 변경되지 않았기 떄문이다.

 

 

※ 헷갈릴거 정리useMemo, useEffect, useCallback세 함수 모두 인자의 형태가 동일하다 하지만 콜 백함수가 어떻게 쓰이는지가 각각 다르다. (이를 역할에 따른 동작이라고 치면..)하지만 두 번째 인자인 배열에 작성되는 값(프로퍼티)가 변경된다면 세 함수 다 콜백함수가 다시 각 역할에 따른 동작으로 동작하게 된다. 

 

 useMemo의 콜백함수의 동작은 콜백함수의 값을 기억하는 것이다. 두 번째 인자인 배열의 값이 변경되면 콜백함수가 디시 실행되어 새로운 값을 기억한다. 

 

 useEffect의 콜백함수의 동작은 처음 랜더링이 되면 콜백함수가 실행되고 배열에 값이 변경 여부에 따라 리랜더링 도리 때 첫 콜백함수가 다시 호출될지 말지를 결정한다.

 

 useCallback은 콜 백함수 자체를 기억하고 두 번째 배열의 인자가 변경되면 콜백함수가 다시 선언(실행이 아님) 즉 평가 된다.


4-1. 가위 바위 보 게임 : hooks로 만든 Component

const React = require('react');
const {useState, useRef, useEffect} = React;

const rspCoords = {
  바위: '0',
  가위: '-142px',
  보: '-284px',
};

const scores = {
  가위: 1,
  바위: 0,
  보: -1,
};

const computerChoice = (imgCoord) => {
  return Object.entries(rspCoords).find(function(v) {
    return v[1] === imgCoord;
  })[0];
};


const RSP = () => {
  const [result, setResult] = useState('');
  const [imgCoord, setImgCoord] = useState(rspCoords.바위);
  const [score, setScore] = useState(0);
  const interval = useRef();

  useEffect(() => { // componentDidMount, componentDidUpdate 역할(1대1 대응은 아님)
    console.log('다시 실행');
    interval.current = setInterval(changeHand, 100);
    return () => { // componentWillUnmount 역할
      console.log('종료');
      clearInterval(interval.current);
    }
  }, []);

  const changeHand = () => {
    if (imgCoord === rspCoords.바위) {
      setImgCoord(rspCoords.가위);
    } else if (imgCoord === rspCoords.가위) {
      setImgCoord(rspCoords.보);
    } else if (imgCoord === rspCoords.보) {
      setImgCoord(rspCoords.바위);
    }
  };

  const onClickBtn = (choice) => () => {
    if (interval.current) {
      clearInterval(interval.current);
      interval.current = null;
      const myScore = scores[choice];
      const cpuScore = scores[computerChoice(imgCoord)];
      const diff = myScore - cpuScore;
      if (diff === 0) {
        setResult('비겼습니다!');
      } else if ([-1, 2].includes(diff)) {
        setResult('이겼습니다!');
        setScore((prevScore) => prevScore + 1);
      } else {
        setResult('졌습니다!');
        setScore((prevScore) => prevScore - 1);
      }
      setTimeout(() => {
        interval.current = setInterval(changeHand, 100);
      }, 1000);
    }
  };

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

module.exports = RSP;

 

4-2. 가위 바위 보 게임 : 클래스로 만든 Component

 

const React = require('react');
const { Component } = React;

const rspCords = {
  바위 : '0',
  가위: '-142px',
  보: '-284px',
};
const scores = {
  가위: '1',
  바위: '0',
  보: '-1',
};

const computerChoice=(imgCord)=>{
  return Object.entries(rspCords).find(function(v){
    return v[1] === imgCord;
  })[0];
};
class RSP extends Component {
  state = {
    result: '',
    score: 0,
    imgCord : '0',
  };

  interval;

  changeHand = () => {
    const {result, score, imgCord} = this.state;
      if(imgCord === rspCords.바위) {
        this.setState({
          imgCord: rspCords.가위,
        });
      } 
      else if(imgCord === rspCords.가위)  {
        this.setState({
          imgCord: rspCords.보,
        });
      }
      else if(imgCord === rspCords.보) {
        this.setState({
          imgCord: rspCords.바위,
        });
      }
  }
  //처음 1번만 랜더링이 성공하면 호출됨-> 비동기 요청을 주로함
  componentDidMount() {
    this.interval = setInterval(this.changeHand, 100);
  }

  //리 랜더링될 때 호출되는 메소드이다.
  componentDidUpdate() {

  }
  //컴포넌트가 제거되기 직전에 호출된다.  -> 비동기 요청 정리를 주로함
  comopnentWillUnmount() {
    clearInterval(this.interval);
  }
  
  onClickBtn = (choice)=> {
    console.log(event.target.id);
    const {result, score, imgCord} = this.state;
    clearInterval(this.interval);
    const myScore = scores[choice];
    const cpuScore = scores[computerChoice(imgCord)];
    const diff = myScore - cpuScore;
    if(diff==0) {
      this.setState({
        result:'비겼습니다.',
      })
    }
    else if([-1, 2].includes(diff)) {
      this.setState((preState)=> {
        return {
          result:'이겼습니다.',
          score: preState.score + 1,
        };
      });
    }
    else {
      this.setState((preState) => {
        return {
          result:'졌습니다.',
          score: preState.score - 1,
        }
      });
    }
    setTimeout(()=>{
      this.interval = setInterval(this.changeHand, 100);
    },2000);
  };


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

module.exports = RSP;