로또 추첨기 정리(useEffect, useMemo, useCallback)

jaemin·2020년 12월 22일
0

리액트

목록 보기
9/16
post-thumbnail

로또 추첨기 정리(useEffect, useMemo, useCallback)

로또 추첨기를 클래스 컴포넌트와 함수 컴포넌트 두 가지로 만들어보면서 클래스 컴포넌트에서의 라이프 사이클과 함수 컴포넌트의 useEffect가 어떻게 다른지, useMemo와 useCallback을 어떻게 사용하는지 알아보도록 하자.

클래스 컴포넌트로 만들기

// LottoPractice.jsx
import React, { Component } from 'react';

const getWinNumbers = () => {
  console.log('getWinNumbers');
  
  const candidate = Array(45).fill().map((v, i) => i + 1);
  const winNumbers = Array(7).fill().map(v => v = candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);

  return winNumbers;
};

class LottoPractice extends Component {
  state = {
    winNumbers: getWinNumbers(),
    winBalls: [],
    bonus: null,
    redo: false,
  }

  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} />}
        <div>
          {redo && <button onClick={this.onClickRedo}>한 번 더!</button>}
        </div>
      </>
    );
  }
}

export default LottoPractice;

먼저, 당첨 숫자를 미리 뽑아두고 winNumbers에 담아둔다. 렌더링을 기준으로 state를 결정하므로 당첨 숫자를 담는 배열, 보너스 숫자, 다시하기 버튼을 상태로 결정했다.

getWinNumbers 안에 콘솔 로그가 들어가 있는데, 처음 리액트를 하는 경우 항상 함수 안에 콘솔을 넣어 꼭 실행돼야 하는 곳에서만 실행되는지 확인해야 한다. 연산량이 많은 함수인데, 필요하지 않은 곳에서도 실행된다면 성능 문제가 생긴다.

render 메서드 안에서 당첨 숫자를 보여주는 map에서 컴포넌트를 분리했는데, 보통 반복문을 기준으로 컴포넌트를 많이 분리한다.

// Ball.jsx
import React from 'react';

class Ball extends PureComponent {
  render() {

    const { number } = this.props;

    let background;

    if (number <= 10) background: 'pletviotred';
    else if (number <= 20) background: 'lightskyblue';
    else if (number <= 30) background: 'lightgreen';
    else if (number <= 40) background: 'lightyello';
    else background: 'lightblue';

    return (
      <div className="ball" style={{ background }}>
        {number}
      </div>
    );
  }
}

export default Ball;

state를 사용하지 않는 제일 작은 자식 컴포넌트는 PureComponent로 만드는 것이 좋다. 사실, PureComponent가 아니라 함수 컴포넌트를 사용하는 것이 좋다. hooks와 함수 컴포넌트를 혼동하지 말자. hooks는 useState, useEffect 등등을 사용하는 것을 말한다.

함수컴포넌트로 Ball.jsx만들기

// Ball.jsx
import React, { memo } from 'react';

const Ball = memo(({ number }) => {
  let background;

  if (number <= 10) background = 'palevioletred';
  else if (number <= 20) background = 'lightskyblue';
  else if (number <= 30) background = 'lightgreen';
  else if (number <= 40) background = 'lightyellow';
  else background: 'lightblue';

  return (
    <div className="ball" style={{ background }}>
      {number}
    </div>
  );
});

export default Ball;

함수 컴포넌트에서는 memo가 클래스 컴포넌트에서 pureComponent와 같은 역할을 한다. 변화된 부분만 렌더링 되도록 성능 최적화를 도와준다.

다시 LottoPractice로 돌아와서, 화면이 렌더링되고 그 다음에 번호가 차례대로 나와야 한다. 이때 사용할 수 있는게 componentDidMount이다.

우선, setTimeout으로 당첨 숫자가 차례대로 보여주게 한다.

// LottoPractice.jsx
class LottoPractice extends Component {

timeouts = [];

  runTimeouts = () => {
    const { winNumbers } = this.state;
    winNumbers.forEach((v, i) => {
      this.timeouts[i] = setTimeout(() => {
        this.setState(prevState => ({ winBalls: [...prevState.winBalls, winNumbers[i]] }))
      }, 1000 * (i + 1));
    });

    this.timeouts[6] = setTimeout(() => {
      this.setState({
        bonus: winNumbers[6],
        redo: true,
      });
    }, 7000);
  };

}

DidMount, DidUpdate, WillUnmount

  • componentDidMount

componentDidMount는 화면이 처음 렌더링된 후에 상황을 컨트롤 할 수 있다.(첫 렌더링에 딱 한 번 실행)

componentDidMount 안에서 보통 api 요청 같은 비동기 처리를 한다. 이 안에서 setState를 하는 것은 바람직하지 않다. componentDidMount가 실행된 시점은 이제 막 렌더링을 마친 시점인데 state가 변경된다면 또 다시 렌더링이 일어나기 때문이다. 이 안에서 setState를 사용하는 경우는 비동기적으로 값을 가져온 후 state를 변경하는 경우이다. 렌더링 이전에 결정되는 state는 밖에서 지정해준다.

runTimeouts 함수는 화면이 렌더링된 후에 타이머가 시작돼야 한다. 따라서 이 함수는 componentDidMount 안에서 호출돼야 한다.

componentDidMount() {
	this.runTimeouts();
}
  • componentWillUnmount

componentWillUnmount는 컴포넌트 자기 자신이 삭제되기 직전의 상황을 컨트롤 할 수 있다. componentDidMount에서 비동기 처리를 하고 나서, 컴포넌트가 삭제된다면 이 비동기 처리는 삭제되지 않고 계속 실행된다.

만약, componentDidMount에서 setInterval로 1초마다 어떤 일을 수행하도록 설정했다고 생각해보자. 그런데, 이 컴포넌트가 삭제된다면 setInterval은 멈추지 않고 여전히 계속 실행되고 있을 것이다. 컴포넌트가 사라진다면 비동기 처리도 같이 멈춰줘야 한다. 이때 componentWillUnmount에서 비동기 처리를 멈출 수 있다.

  • componentDidUpdate

componentDidUpdate는 리렌더링을 마친 후의 상황을 컨트롤 할 수 있다. 특정 state가 변했을 때 어떤 일을 수행하도록 할 때 사용할 수 있다. componentDidUpdate는 state가 변하면 계속 실행되기 때문에 특정 조건에서만 수행하도록 조건을 잘 써주는 것이 중요하다.

함수 컴포넌트로 만들기

memo

클래스 컴포넌트에서 Ball.jsx를 pureComponent로 만들었는데 함수 컴포넌트에서 이와 같은 역할을 하는 것은 memo이다.

// Ball.jsx
import React, { memo } from 'react';

const Ball = memo(({number}) => {...});

단순히 memo로 감싸주면 된다. memo를 사용하면, React는 먼저 컴퍼넌트를 렌더링(rendering) 한 뒤, 이전 렌더된 결과와 비교하여 DOM 업데이트를 결정한다. 만약 렌더 결과가 이전과 다르다면, React는 DOM을 업데이트한다.

성능 최적화를 해준다면 모든 컴포넌트를 memo로 감싸는게 좋다고 생각할 수 있다. 그러나 경험적으로, 성능적인 이점을 얻지 못한다면 메모이제이션을 사용하지 않는것이 좋다.

성능 관련 변경이 잘못 적용 된다면 성능이 오히려 악화될 수 있다. React.memo()를 현명하게 사용하라.

useRef

클래스 컴포넌트에서 멤버 변수로 선언했던 것이 두 가지 있다. 바로 state와 setTimeout 함수들을 보관한 timeouts가 있다. 클래스 컴포넌트에서는 그냥 class 안에 변수를 선언하면 됐지만 함수 컴포넌트에서는 다르게 관리한다.

state팁 : 조건문 안엔 절대 금지, 반복문에도 권장 X

조건에 따라 state가 생기기도 하고 안생기기도 하면 실행 순서가 보장되지 않기 때문에 항상 최상위 순서에서 선언돼야 한다!

useRef는 두 가지 용도로 사용된다.

  1. 돔에 직접 접근할 때 사용한다.

    react는 가상 돔을 만들어 렌더링하기 때문에 돔에 접근할 필요가 없다고 생각할 수 있지만 input창의 focus를 주고 싶은 경우는 돔에 접근해야 한다.

  2. 변수 관리할 때 사용한다.

    클래스 컴포넌트에서의 멤버 변수를 함수 컴포넌트로 변경할 때 useRef를 사용한다. useRef는 데이터가 변경되어도 다시 렌더링하고 싶지 않은 경우에 사용한다.

// LottoFunction.jsx
import React, { useState, useRef } from 'react';


const LottoPractice = () => {
	// 상태들 입력
  
  // useRef로 관리
  const timeouts = useRef([]);
  
};

timeouts를 사용할 땐 반드시 timeouts.current로 사용해야 한다.

useMemo

클래스 컴포넌트는 state가 변경되면 render 메서드가 다시 호출된다. 하지만 함수 컴포넌트는 함수 몸체 전체가 다시 실행된다. 따라서 아까는 별 문제 없었던, getWinNumbers 함수가 계속 다시 호출되는 문제가 발생한다. 이때 useMemo를 사용할 수 있다.

// LottoFunction.jsx
import React, { useState, useRef, useMemo } from 'react';

// getWinNumbers 함수는 그대로 가져온다.
const getWinNumbers = () => {
  const candidate = Array(45).fill().map((v, i) => i + 1);
  const winNumbers = Array(7).fill().map(v => v = candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);

  return winNumbers;
};

const LottoFunction = () => {
	const lottoNumbers = useMemo(() => getWinNumbers(), []);
};

useMemo의 두번째 인자에는 getWinNumbers를 언제 다시 실행할건지에 대한 조건을 넣어줄 수 있다. 이 조건이 맞지 않는다면 다시 실행하지 않는다.

이 문제 때문에 함수가 있으면 내부에 콘솔로그 하나씩 넣어둔 후 적절한 때 호출되는지 확인해주는 것이 좋다.

useMemo가 하는 일은 복잡한 함수의 결과값을 기억한다. 함수 자체가 아니라 함수의 리턴값만 기억!!

useCallback

useMemo가 함수의 결과값을 기억한다면 useCallback은 함수 자체를 기억한다. 따라서 함수 몸체가 다시 실행돼도 함수를 새로 생성하지 않는다.

로또 추첨기로 돌아와 이해해보도록 하자. 로또 추첨기에서는 한 번 더! 버튼을 누르면 초기화되면서 다시 새로운 번호로 추첨을 해야 한다.

const onClickRedo = useCallback(() => {
  	console.log(winNumbers);  
  
  	setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);

    timeouts.current = [];
}, []);

이렇게 상태값을 초기해주는 함수가 있다. 이 함수는 렌더링 될때마다 새로 만들 필요가 없으므로 useCallback을 사용했다. 그런데, 문제가 있다.

onClickRedo 함수 내부에 winNumbers를 콘솔에 찍어보면 처음 뽑은 숫자 그대로 나온다. useCallback 안에서 state를 사용할 때는 인수로 state를 넣어줘야 한다. (두번째 인자 : [winNumbers])

자식 컴포넌트 안에 함수를 넘겨줄때는 useCallback을 꼭 사용해야 한다. 그렇지 않으면, 자식에서 매번 새로운 props를 넘겨준다고 판단해서 새로 랜더링한다.

useEffect

useEffect는 클래스 컴포넌트의 라이프 사이클을 모방할 수 있다.

useEffect의 두번째 인자가 빈 배열이라면 첫 렌더링한 후에 실행되는 componentDidMount와 같다. 빈 배열이 아니라 요소가 있다면 componentDidMount와 componentDidUpdate 둘 다 수행된다.

클래스 컴포넌트에 익숙하다면 어려울 수 있다.

처음엔 두번째 인자를 빈 배열로 한 뒤, didMount만 생각한 다음, 뭐가 변하면 다시 렌더링할 것인지(willUpdate) 생각하자.

  • componentDidMount에서만 실행하고 싶은 경우

예를 들어, componentDidMount에서만 ajax 호출을 하고 싶다면, 다음과 같이 한다.

useEffect(() => ajax호출, []);
  • componentDidUpdate에서만 실행하고 싶은 경우

componentDidUpdate에서만 ajax 호출을 하고 싶다면 다음과 같이 한다.

const mounted = useRef(false);

useEffect(() => {
  if (!mounted.current) mounted.current = true;
 // ajax 요청
}, [바뀌는 값]);

componentDidMount가 실행은 되지만 아무것도 하지 않는다. 일종의 꼼수다. : )

Reference

profile
프론트엔드 개발자가 되기 위해 공부 중입니다.

0개의 댓글