공부기록/웹 개발

[웹 게임을 만들며 배우는 React] 5장

_우지 2022. 8. 29. 15:05

클래스 컴포넌트의 라이프 사이클

  • componentDidMount (처음 렌더가 실행되고 componentDidMount 가 실행된다.)
  • componentDidUpdate (리렌더가 발생하고 실행된다.)
  • componentWillUnMoun (컴포넌트가 제거되기 직전에 실행된다.)

constructor -> 렌더링 -> ref -> componentDidMount -> (setState/props 바뀔때)-> shouldComponentUpdate ->

render -> componentDidUpdate

// 부모가 나를 없앴을 때 -> componentWillUnmount -> 소멸 

 

Closure 문제

비동기 함수인 setInterval 안에서 비동기 함수 밖에 있는 변수를 참조하면 Closure 가 발생하게 된다.

다음 코드는 setInterval 밖에 있는 변수인 imgCoord 를 참조한 상황이다.

  componentDidMount() {
    // 컴포넌트가 첫 렌더링된 후, 여기에 비동기 요청을 많이 해요
    const { imgCoord } = this.state;
    this.interval = setInterval(() => {
      if (imgCoord === rspCoords.바위) {
        this.setState({
          imgCoord: rspCoords.가위,
        });
      } else if (imgCoord === rspCoords.가위) {
        this.setState({
          imgCoord: rspCoords.보,
        });
      } else if (imgCoord === rspCoords.보) {
        this.setState({
          imgCoord: rspCoords.바위,
        });
      }
    }, 100);
  }

 

이렇게 참조할 변수를 setInterval 안에 넣어주었어야했다.

  componentDidMount() {
    // 컴포넌트가 첫 렌더링된 후, 여기에 비동기 요청을 많이 해요
    this.interval = setInterval(() => {
      const { imgCoord } = this.state;
      if (imgCoord === rspCoords.바위) {
        this.setState({
          imgCoord: rspCoords.가위,
        });
      } else if (imgCoord === rspCoords.가위) {
        this.setState({
          imgCoord: rspCoords.보,
        });
      } else if (imgCoord === rspCoords.보) {
        this.setState({
          imgCoord: rspCoords.바위,
        });
      }
    }, 100);
  }

 

이번장에서 배운것은 

setInterval 을 사용하면 꼭 clearInterval 을 사용해서 메모리 누수를 없애줘야한다 정도랄까.

 

 

고차함수 패턴

다음과 같이 button 안에 () => 애로우 펑션으로 되어있는 함수를 고차 함수 패턴으로 없애 줄 수 있다.

인라인 형태로 저렇게 선언이 되면 렌더링 될때마다 함수가 계속 재생성 되므로 좋지않다. 따라서 따로 빼주는 것이 옳다.

  onClickBtn = (choice) => {
    ...
  };
  
  
  render() {
    const { result, score, imgCoord } = this.state;
    return (
      <>
        <div>
          <button
            id="rock"
            className="btn"
            onClick={() => this.onClickBtn("바위")}
          >
            바위
          </button>
      </>
    );
  }

 

코드를 보면 onClick 안에 () => 를 똑 떼서 onClickBtn 함수의 (choice) => 옆에 붙여준 형태이다.

물론 함수형 컴포넌트라면 이래도 함수는 재생성 되고 useCallback 을 사용해서 최적화를 해야한다.

  onClickBtn = (choice) => () =>  {
    ...
  };
  
  
  render() {
    const { result, score, imgCoord } = this.state;
    return (
      <>
        <div>
          <button
            id="rock"
            className="btn"
            onClick={this.onClickBtn("바위")}
          >
            바위
          </button>
      </>
    );
  }

 

자주쓰는 변수 상수객체로 빼주기

자주쓰는 숫자, 문자열은 하드코딩하지말고 다음처럼 상수로 빼주자.

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

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

 

useEffect

useEffect 에 대해서 다시한번 정리해보자. 

useEffect 도 componentDidMount 처럼 한번 딱 실행 된다. 이후의 componentDidUpdate 는 deps 에 따라 결정이 되는데 , deps 안에 state 가 변경될때마다 useEffect가 재 실행되는 것이다. 이때 재실행 되기 전에 componentWillUnmount 처럼 clearInterval 이 실행되게 된다.

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

 

useEffect 와 componentDidMount 비교

componentDidMount() {
  this.setState({
    imgCoord: 3,
    score: 1,
    result: 2,
  })
}

useEffect(() => {
  setImgCoord();
  setScore();
  setResult();
}, []);

 

useLayoutEffect

화면이 리사이징 될때 실행되는 useEffect라고 생각하자.

 

 

useInterval hooks

useInterval 커스텀 훅을 사용할 수 있다.

코드는 다음과 같다.

import { useRef, useEffect } from "react";

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);

  return savedCallback.current;
}

export default useInterval;

 

수정 전

const RSP = () => {
  ...
  const interval = useRef();

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

  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);
    }
  };

  ...
};

export default RSP;

 

수정 후

코드가 꽤 줄어들었습니다. 수정전에는 useEffect의 deps imgCoord 를 통해서 업데이트를 하였다면, useInterval 훅은 isRunning 이라는 state 를 사용하여 삼항연산자를 통해 null 일때, delay 를 주었을때로 분기처리를 하는 것이 인상적이였습니다.

쉽게 생각하면 isRunning 이라는 state를 하나 생성해서 setInterval 을 컨트롤 한다고 생각하는 편이 좋을 것 같습니다.

const RSP = () => {
  ...
  const [isRunning, setIsRunning] = useState(true);
  const changeHand = () => {
    if (imgCoord === rspCoords.바위) {
      setImgCoord(rspCoords.가위);
    } else if (imgCoord === rspCoords.가위) {
      setImgCoord(rspCoords.보);
    } else if (imgCoord === rspCoords.보) {
      setImgCoord(rspCoords.바위);
    }
  };

  useInterval(changeHand, isRunning ? 100 : null);

  const onClickBtn = (choice) => () => {
    if (isRunning) {
      setIsRunning(false);
      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(() => {
        setIsRunning(true);
      }, 1000);
    }
  };

  ...
};