과거 프로그래밍 자료들/React

[웹 게임을 만들며 배우는 React] - 로또추첨기, useEffect, useCallback

평부 2022. 9. 29. 15:26

 

 

출처: https://www.inflearn.com/course/web-game-react/dashboard

 

[무료] 웹 게임을 만들며 배우는 React - 인프런 | 강의

웹게임을 통해 리액트를 배워봅니다. Class, Hooks를 모두 익히며, Context API와 React Router, 웹팩, 바벨까지 추가로 배웁니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

* HOC (하이오더컴포넌트) = 고차 컴포넌트

▶ 컴포넌트 로직을 재사용하기 위해 사용되고 컴포넌트를 가져와 새 컴포넌트를 반환

▶ 컴포넌트를 인자로 받거나 반환하는 함수

▶ 컴포넌트를 감싸는 것 (예시에서는 memo 의미))

참고 : https://itprogramming119.tistory.com/entry/React-%EA%B3%A0%EC%B0%A8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%9E%80

 

[React] 고차 컴포넌트란? (HOC, Higher-Order Component)

  고차 컴포넌트는 컴포넌트 로직을 재사용하기 위해 사용되고 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다. 즉, 컴포넌트를 인자로 받거나 반환하는 함수입니다. * 하이오더 컴포넌

itprogramming119.tistory.com

import React, { memo } from "react";

const Ball = 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>
  );
});

export default Ball;

 

 

* 리액트 장점

- 기존의 javascript의 경우 document.querySelector나 jqeury 경우 직접 입력해 DOM을 건듦

- 반면 리액트는 state만 변경하고 reqct가 저절로 화면을 그려줌

 

 

* Hooks

- 순서가 바뀌면 안 됨
- 조건문 안에 절대 넣으면 안 되고 함수나 반복문 안에도 웬만하면 넣으면 안 됨
- uesState는 항상 최상위에 위치

 

 

* componentDidUpdate(class 컴포넌트에서만 사용)

▶ 갱신이 일어난 직후에 호출됨, 최초 렌더링에서는 호출되지 않음

▶ 조건문으로 감싸지 않으면 무한반복이 될 수 있음

출처 : https://ko.reactjs.org/docs/react-component.html

 

React.Component – React

A JavaScript library for building user interfaces

ko.reactjs.org

//componentDidUpdate(prevProps, prevState, snapshot)

//예시
componentDidUpdate(prevProps) {
  // 전형적인 사용 사례 (props 비교를 잊지 마세요)
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

 

 

[Ball.jsx]

import React, { memo } from "react";

const Ball = 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>
  );
});

export default Ball;

 

 

[LottoClass.jsx]

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 LottoClass extends Component {
  state = {
    winNumbers: getWinNumbers(), // 당첨 숫자들
    winBalls: [],
    bonus: null, // 보너스 공
    redo: false,
  };

  timeouts = [];

  runtimeouts = () => {
    console.log("runtimeouts");
    const { winNumbers } = this.state;
    for (let i = 0; i < winNumbers.length - 1; i++) {
      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();
  }

  //  onClickRedo 누를 때 setTimeout 다시 실행해야 함
  componentDidUpdate(prevProps, prevState) {
    console.log("didUpdate");
    //이 조건문이 중요(redo를 눌렀을때만, 조건문이 없을 경우 매 순간 runTimouts이 실행됨)
    if (this.state.winBalls.length === 0) {
      this.runtimeouts();
    }
  }

  //setTimeout, setInterval 정리(메모리 누수 발생 방지)
  componentWillUnmount() {
    this.timeouts.forEach((v) => {
      clearTimeout(v);
    });
  }

  onClickRedo = () => {
    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 LottoClass;

 

 

* useEffect

input 요소가 빈배열이면 componentDidMount와 동일

배열의 요소가 있으면 componentDidMount, componentDidUpdate 둘 다 수행

▶ useRef 사용 시 current와 함께 쓸 것

componentWillUnmount는 useEffect의 return

 

출처 : https://ko.reactjs.org/docs/hooks-reference.html#useeffect

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

//기본값
useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

 

class에서 hooks에서 useEffect 사용할 경우

 

더보기

클래스의 경우

//class

runtimeouts = () => {
    console.log("runtimeouts");
    const { winNumbers } = this.state;
    for (let i = 0; i < winNumbers.length - 1; i++) {
      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();
  }

  //  onClickRedo 누를 때 setTimeout 다시 실행해야 함
  componentDidUpdate(prevProps, prevState) {
    console.log("didUpdate");
    //이 조건문이 중요(redo를 눌렀을때만, 조건문이 없을 경우 매 순간 runTimouts이 실행됨)
    if (this.state.winBalls.length === 0) {
      this.runtimeouts();
    }
  }

  //setTimeout, setInterval 정리(메모리 누수 발생 방지)
  componentWillUnmount() {
    this.timeouts.forEach((v) => {
      clearTimeout(v);
    });
  }
  
  onClickRedo = () => {
    console.log("onClickRedo");
    this.setState({
      winNumbers: getWinNumbers(), // 당첨 숫자들
      winBalls: [],
      bonus: null, // 보너스 공
      redo: false,
    });
    this.timeouts = [];
  };

 

두 번째 배열에 들어갈 부분, 즉 위의 예시에서  runtimeouts, 아래의 예시에서는 useEffect가 실행될 두 번째 인자([])

즉, componentDidMount, componentDidUpdate 둘 다 수행하는 조건 = timeouts.current가 빈 배열일 때

useEffect(() => {
    console.log("useEffect");
    for (let i = 0; i < winNumbers.length - 1; i++) {
      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);
      }); //componentWillUnmount
    };
  }, [timeouts.current]);

  const onClickRedo = () => {
    console.log("onClickRedo");
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);
    timeouts.current = [];
  };

 

 

* Hooks 시 문제점

- getWinNumbers 함수에 console.log("getWinNumbers") 추가 시 매번 실행 

= 함수 컴포넌트가 매번 재실행됨

- 다시 실행되지 않고 기억하게 하는 것 = useMemo

 

 

* useRef

일반 값을 기억

 

* useMemo

복잡한 함수 결과값을 기억(리턴값)

▶ 두 번째 인자가 바뀌지 않는 한 getWinNumbers()는 다시 실행되지 않음

▶ 두 번째 요소 배열이 바뀌면  getWinNumbers()도 다시 실행됨

useMemo(() => getWinNumbers(), []);

 

* useCallback

함수 자체를 기억

▶ 두 번째 인자가 바뀌지 않는 한 useCallback은 다시 실행되지 않음

▶ 두 번째 요소 배열이 바뀌면  useCallback도 다시 실행됨

useCallback 안에서 쓰이는 state는 input에다가도 넣어야 함 

 

- 반드시 useCallback을 사용해야 할 때 = 자식 컴포넌트에서 함수를 넘길 때

▶ useCallback이 없으면 매번 새로운 함수가 실행됨(부모가 함수를 바꿀 때마다 자식 컴포넌트가 매번 리렌더링함)
useCallback이 있어야 부모로 받은 함수가 계속 같다고 자식 컴포넌트가 인식함 

 

 

 

* Hooks

import React, {
  useState,
  useRef,
  useEffect,
  useMemo,
  useCallback,
} 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];
}

const Lotto = () => {
  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("useEffect");
    for (let i = 0; i < winNumbers.length - 1; i++) {
      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);
      }); //componentWillUnmount
    };
  }, [timeouts.current]);

  const onClickRedo = useCallback(() => {
    console.log("onClickRedo");
    console.log(winNumbers);
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);
    timeouts.current = [];
  }, [winNumbers]);
  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;

 

 

* [useEffect] ajax를 componentDidMount만 실행하고 싶을 경우

useEffect(() => {
	//ajax
}, []);

 

*  [useEffect] ajax를 componentDidUpdate만 실행하고 componentDidMount에서 실행 X

const mounted = useRef(false);
useEffect(() => {
    if(!mounted.current) {
        mounted.current = true
    } else { 
        //ajax
    }
}, [바뀌는 값]) //componentDidUpdate만 실행하고 componentDidMount에서 실행x