WEB/React

zeroCho[5/8] Lotto - componentDidUpdate, useEffect, useMemo, useCallback

Harimad 2022. 4. 14. 14:27

목차

더보기

 

강의목차

6-1. 로또 추첨기 컴포넌트
6-2. setTimeout 여러 번 사용하기
6-3. componentDidUpdate
6-4. useEffect로 업데이트 감지하기
6-5. useMemo와 useCallback
6-6. Hooks에 대한 자잘한 팁들

 
 
 

6-1. 로또 추첨기 컴포넌트

더보기

index.html

<html>

<head>
	<meta charset="UTF-8" />
	<title>로또추첨기</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>
	<script src="./dist/app.js"></script>
</body>

</html>​


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,
  }

  onClickRedo = () => {}

  render() {
    const { winBalls, bonus, redo } = this.state
    return (
      <>
        <div>Win Numbers</div>
        <div id="결과창">
          {winBalls.map(v => (
            <Ball key={v} number={v} />
          ))}
        </div>
        <div>Bonus!</div>
        {bonus && <Ball number={bonus} />}
        {redo && <button onClick={this.onClickRedo}>One more</button>}
      </>
    )
  }
}

export default LottoClass​


Ball.jsx

import React, { memo } from 'react'

const Ball = memo(({ number }) => {
  let background
  if (nubmer <= 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

 

6-2. setTimeout 여러 번 사용하기

이번 강의 까지 코드

더보기
//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()
    console.log('로또 숫자를 생성합니다.')
  }

  componentDidUpdate() {}

  componentWillUnmount() {
    this.timeouts.forEach(v => {
      clearTimeout(v)
    })
  }

  onClickRedo = () => {}

  render() {
    const { winBalls, bonus, redo } = this.state
    return (
      <>
        <div>Win Numbers</div>
        <div id="결과창">
          {winBalls.map(v => (
            <Ball key={v} number={v} />
          ))}
        </div>
        <div>Bonus!</div>
        {bonus && <Ball number={bonus} />}
        {redo && <button onClick={this.onClickRedo}>One more</button>}
      </>
    )
  }
}

export default LottoClass
//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

 
 Tip. 자식 컴포넌트는 데이터 역할이 아니라 화면 역할만 하기 때문에
Class 컴포넌트는 PureComponent를 붙이고
함수형 컴포넌트는 React.memo를 붙이는 것이 좋다.
 
 
memo 같은 컴포넌트를 감싸는 것을 HOC(high order component) 고차컴포넌트 라고 부른다.
 
 
 
 

6-3. componentDidUpdate

 

 
실행 화면
console로 찍어가면서 LifeCycle 이해하기 + 자식컴포넌트는 PureComponent나 React.memo 사용해서 랜더링 낭비 막기

6-4. useEffect로 업데이트 감지하기

Comment 에 있는 동작 결과 참고


인프런에 동작 결과에 따른 동작순서 예측을 질문남겨놨음.

제로초님의 대답을 참고하기

 


useEffect 참고할점 

두번째 인자가 빈배열 일 때: componentDidMount 와 동일함 -> 1번 동작

두번쨰 인자가 추적할 요소가 있을 때: componentDidMount + componeneDidUpdate 둘다 수행 -> 2번 동작

useEffect(() => {
    console.log('useEffect1')
    for (let i = 0; i < winNumbers.length - 1; i++) {
      timeouts.current[i] = setTimeout(() => {
        setWinBalls(prevState => [...prevState, winNumbers[i]])
      }, (i + 1) * 1000)
    }
    timeouts.current[6] = setTimeout(() => {
      setBonus(winNumbers[6])
      setRedo(true)
    }, 7000)
    return () => {
      timeouts.current.forEach(v => {
        clearTimeout(v)
      })
    }
  }, [timeouts.current]) // 빈 배열이면 componentDidMount와 동일
  // 배열에 요소가 있으면 componentDidMount랑 componentDidUpdate 둘 다 수행
 
 

6-5. useMemo와 useCallback

깃 링크 : https://github.com/Harimad/zeroReact/commit/4acb8739b4cb56e5e8f3f0731fd55c7d129bafcf

Comment 자세하니까 참고


더보기

useMemo 적용하기 전의 코드

import React, { useEffect, useRef, useState, 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 [winNumbers, setWinNumbers] = useState(getWinNumbers()) //getWinNumbers() 7번 호출
  //const lottoNumbers = useMemo(() => getWinNumbers(), []) // useMemo로 getWinNumbers() 리턴값 캐싱
  //const [winNumbers, setWinNumbers] = useState(lottoNumbers) // 캐싱한 값을 state초기값으로 넣음

  const [winBalls, setWinBalls] = useState([])
  const [bonus, setBonus] = useState(null)
  const [redo, setRedo] = useState(false)
  const timeouts = useRef([])

  useEffect(() => {
    console.log('useEffect1')
    for (let i = 0; i < winNumbers.length - 1; i++) {
      timeouts.current[i] = setTimeout(() => {
        setWinBalls(prevState => [...prevState, winNumbers[i]])
      }, (i + 1) * 300)
    }
    timeouts.current[6] = setTimeout(() => {
      setBonus(winNumbers[6])
      setRedo(true)
    }, 2100)
    return () => {
      timeouts.current.forEach(v => {
        clearTimeout(v)
      })
    }
  }, [timeouts.current]) // 빈 배열이면 componentDidMount와 동일
  // 배열에 요소가 있으면 componentDidMount랑 componentDidUpdate 둘 다 수행

  useEffect(() => {
    console.log('useEffect2 - 로또 숫자를 생성합니다.')
  }, [winNumbers])

  const onClickRedo = useCallback(() => {
    console.log('onClickRedo')
    console.log(winNumbers)
    setWinNumbers(getWinNumbers())
    setWinBalls([])
    setBonus(null)
    setRedo(false)
    timeouts.current = []
  }, [winNumbers])

  return (
    <>
      <div>Win Numbers</div>
      <div id="결과창">
        {winBalls.map(v => (
          <Ball key={v} number={v} />
        ))}
      </div>
      <div>Bonus!</div>
      {bonus && <Ball number={bonus} />}
      {redo && <button onClick={onClickRedo}>One more</button>}
    </>
  )
}

export default Lotto
 Q. 위의 코드에서 getWinNumbers가 여러번 찍히는 이유? 🎁

-> Hooks 의 특성 때문이다.
state값이 바뀌면 컴포넌트가 전체가 다시 실행된다. (컴포넌트 밖의 코드는 재실행X)
여기서는 첫번째 useEffect속에 state값이 7번 바뀌기 때문에 Lotto 컴포넌트가 7번 재실행됨
그러면 const [winNumbers, setWinNumbers] = useState(getWinNumbers()) 가 매번(총7번) 다시 실행된다.

getWinNumbers는 로또 번호 7자리를 return 하는 함수이다.
useMemo를 사용하면 getWinNumbers가 return 한 값을 캐싱(기억) 할 수 있다. 
그러면 Hooks가 getWinNumbers의 값을 기억하고 있는 것이다.

- useMemo syntax
const lottoNumbers = useMemo( () => getWinNumbers(), [ 💥 ] );​

여기서 두번째 인자(💥)가 바뀌지 않는한 useMemo안의 코드는 재실행 되지 않는다.
반대로 두번째 인자(💥)속의 값이 바뀌면 useMemo안의 코드가 재실행 된다.

- useMemo 사용설명
useMemo로 캐싱한 변수를 다시 useState안에 넣어줘야한다.
const [winNumbers, setWinNumbers] = useState(lottoNumbers);​

 

이제는 getWinNumbers함수의 리턴값을 캐싱하고 있기 때문에 
getWinNumbers가 한번만 호출되고 더이상 호출되지 않는다.

🎁 useMemo: 복잡한 함수 결과값을 기억
🎁 useRef : 일반 값을 기억

useMemo를 안쓰면 getWinNumbers가 state값이 바뀌기 때문에 또 다시 호출하게 된다.



Q. useCallback vs useMemo 🎁
useMemo는 함수의 리턴값!!을 기억한다.
useCallback은 함수!! 자체를 기억한다.

Q. useCallback은 어떻게 쓰는가? 🎁
함수 컴포넌트 안에 만들어 놓은 함수 앞에 useCallback을 달아놓는다.

Q. useCallback은 언제 쓰나? 장점은? 🎁
함수 컴포넌트는 전체가 재실행 -> 이제 함수 자체를 기억해둬서 함수 컴포넌트가 재실행 되어도 useCallback() 으로 감싼 함수는 재생성되지 않는다.
재생성 자체가 오래걸리거나 비용이 클때 useCallback을 쓴다.

Q. useCallback 쓸때, 어떨때 문제가 발생하는가? 🎁
onClickRedo를 useCallback()으로 감쌈으로써 onClickRedo 함수를 캐싱(기억)하게 된다.
그럼 onClickRedo안에 winNumbers를 찍으면 처음에 저장한 winNumbers값을 계속 출력한다.
  const onClickRedo = useCallback(() => {
    console.log('onClickRedo')
    console.log(winNumbers) //처음 저장한 값 저장
    setWinNumbers(getWinNumbers())
    setWinBalls([])
    setBonus(null)
    setRedo(false)
    timeouts.current = []
  }, [])

 

새로 클릭해도 같은 값이 콘솔에 찍힌다. 왜냐하면 함수컴포넌트가 state값이 바뀌어서 전체가 랜더링 되더라도  onClickRedo함수는 재생성되지 않기 때문이다.
사용자는 새로운 winNumber가 찍히는줄 알았는데 아니다.

Q. 그럼 winNumbers값을 계속 최신화 시키려면 어떻게 해야할까? 🎁
useCallback의 두번째 배열 인자 안에 최신화 시킬 값을 넣어준다.
// useCallback 콜백함수 재실행 하는방법, 두 번째 배열인자에 추적할 값을 넣기
useCallback( () => {} , [ 💥 ] )
useCallback( ( ) => { }, [ winNumbers ] )

 

두번째 인자가 바뀌면 useCallback안의 코드가 재실행된다.
이건 useEffect에 두번째 인자를 넣는것과 동일하다.


Q. useCallback을 필수로 적용해야 되는 때는 언제인가? 🎁
자식컴포넌트(<Ball/>)에 props로 함수를 넘길떄는 useCallback을 꼭 줘야한다.

const Lotto = () => {
  return (
    <> {bonus && <Ball number={bonus} onClick={onClickRedo} />} </>
  )
}​

 

왜? useCallback을 하지않고 props를 넘기면 onClickRedo함수가 매번 재생성된다.
그리고 재생성된 함수가 props로 자식컴포넌트에 전달된다.
그러면 자식컴포넌트 입장에선 부모로 부터 새로운 props(함수)가 넘어왔으니 props(함수)가 변경되었다고 인식한다.
그럼 매번 자식이 리랜더링됨 => 엄청난 낭비!!
그러므로 useCallback으로 함수를 깜싸주고 props를 자식컴포넌트에 넘겨줘야한다.


Q. useCallback , useMemo,  useEffect 두 번째 배열 인자에 공통으로적으로 값을 넣으면? 🎁
세 Hooks의 두 번째 배열인자에 특정 state값을 넣고, 
그 state값이 바뀌면 세 Hooks 안에있는 코드가 재실행 된다.
그러므로 세 Hooks를 잘활용하기 위해서는 두 번째 배열인자를 잘넣어서 코드를 컨트롤 해야한다.


6-6. Hooks에 대한 자잘한 팁들

정리

useEffect는 두 번째 인자값이 바뀌기 전까지 callback 함수가 유지된다.useMemo는 두 번째 인자값이 바뀌기 전까지 callback 함수가 유지된다.

useCallback는 두 번째 인자값이 바뀌기 전까지 callback 함수가 유지된다.

 

공통정리

Hooks는 순서가 중요해서 순서가 마음대로 바뀌면 안된다.

조건문 안에 Hooks를 절대 넣으면 안되고 
함수나 반복문 안에도 웬만하면 Hooks를 넣지 말아야 한다.
다른 Hooks안에도 Hooks를 넣으면 안된다. ex) useEffect 안에 useState쓰는 방식



useEffect 사용방법

componentDidMount 쓸것 따로 useEffect 쓰기  

useEffect( () => {} , [ ] )

 

componentDidUpdate 쓸것 따로 useEffect 쓰기

useEffect( () => {} , [ 추적할인자 ] )
// componentDidUpdate 쓰면 componentDidMount도 같이 동작한다.


Q. componentDidMount 에서만 ajax 쓰고싶다?

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


Q. componentDidMount는 쓰고 싶지 않고, componentDidUpdate에서만 쓰고싶다?

-> 꼼수 써야함 (패턴이니까 알아둬야함)

const mounted = useRef(false)
useEffect( () => {
  if (!mounted.current) {	// componentDidMount가 실행은 되지만 아무것도 하지않는다.
    mounted.current = true;
  } else {		// 실행된 후에는 [ 바뀌는값 ] 에 따라 else 이후에 코드가 실행될 뿐이다.
    // ajax
 }
}, [ 바뀌는값] );