TIL 22-04-14

thisisyjin·2022년 4월 14일
0

TIL 📝

목록 보기
23/113

React.js

Today I Learned ... react.js

🙋‍♂️ React.js Lecture

🙋‍ My Dev Blog


React Lecture CH 6

1 - 로또 추첨기 컴포넌트
2 - setTimeout 중복 사용

3 - componentDidUpdate
4 - useEffect - 업데이트 감지
5 - useMemo, useCallback
6 - Hooks Tips

componentDidUpdate

Class Component

  • 이전 코드에서 일부 수정
    -이전에는 onClickRedo에서 state들을 다 초기화 해준 후, this.runTimeouts()를 해서 다시한번 setTimeout 해줬었다.
// 수정 전 
onClickRedo = () => {
    this.setState({
      winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
      winBalls: [],
      bonus: null,
      redo: false,
    });
    this.runTimeouts();
  };

수정 사항

  1. this.timeouts 배열도 빈 배열로 초기화해줌.
    (setTimeout 함수들이 담긴 배열)

  2. componentDidUpdate를 이용함.
    = 컴포넌트가 업데이트 될때마다 발생함.
    -> 조건문을 걸어줘야 버튼을 클릭했을 때만 실행됨.

componentDidUpdate(prevProps, prevState) {
    if (this.state.winBalls.length === 0) {
      this.runTimeouts();
    }
  }
  • 버튼 클릭시 -> onClickRedo 실행 -> state 전부 초기화.

  • 초기화 된 state중, winBalls 배열이 빈배열[]이 되어 this.state.winBalls.length === 0인 상태이므로, 이를 조건식으로 이용한다.

❗️ 주의

  • componentDidUpdate는 컴포넌트의 state, props 등이 바뀌어 render()가 다시 실행되는 순간마다 매번 발생한다.
  • 따라서, 위와 같이 특정 조건에만 실행하고 싶다면?
    -> 조건문을 걸어주자.

Hooks로 변경

  • 내가 변경해본 코드.
import React, { useState, useRef, useEffect } from 'react';
import Ball from './Ball';

const getWinNumbers = () => {
  const candidate = Array(45)
    .fill()
    .map((_, i) => i + 1);
  let 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());
  const [winBalls, setWinBalls] = useState([]);
  const [bonus, setBonus] = useState(null);
  const [redo, setRedo] = useState(false);

  const timeouts = useRef([]);

  // 🔻 이부분을 모르겠다.
  useEffect(() => {
    runTimeout();
    return timeouts.current.forEach((v) => clearTimeout(v));
  });

  const runTimeout = () => {
    for (let i = 0; i < winNumbers.length - 1; i++) {
      timeouts.current[i] = setTimeout(() => {
        setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
      }, (i + 1) * 1000);
    }
    timeouts.current[6] = setTimeout(() => {
      setBonus(winNumbers[6]);
      setRedo(true);
    }, 7000);
  };

  const onClickRedo = () => {
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);

    timeouts.current = [];

    runTimeout();
  };

  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;
  • 클래스 컴포넌트에서의 componentDidMount, componentDidUpdate, componentWillUnmount를 Hooks로 나타내려면?
    -> useEffect를 사용한다!

* * *

componentDidMount

useEffect()의 첫번째 인자로 콜백이 전달되고,
두번째 인자로는 dependencies가 배열로 전달된다.
-> 리액트가 변화를 지켜봐야하는 state.

✅ 만약 deps를 빈 배열[] 로 한다면?

-> 최초 1회만 실행된다. (첫 렌더링시)
= componentDidMount와 같다!

useEffect(() => {
    for (let i = 0; i < winNumbers.length - 1; i++) {
      timeouts.current[i] = setTimeout(() => {
        setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
      }, (i + 1) * 1000);
    }
    timeouts.current[6] = setTimeout(() => {
      setBonus(winNumbers[6]);
      setRedo(true);
    }, 7000);
  }, []); // deps가 빈배열이면 - componentDidMount 역할

componentWillUnmount

  • useEffect의 콜백 안에 return문으로 작성해준다.
return () => { 
  // 이부분 
}
useEffect(() => {
    if (winBalls.length === 0) {
      for (let i = 0; i < winNumbers.length - 1; i++) {
        timeouts.current[i] = setTimeout(() => {
          setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
        }, (i + 1) * 1000);
      }
      timeouts.current[6] = setTimeout(() => {
        setBonus(winNumbers[6]);
        setRedo(true);
      }, 7000);
      
      return () => { 
        // componentWillUnmount 역할
        timeouts.current.forEach((v) => {
          clearTimeout(v);
        });
      };
    }
  }, []);

componentDidUpdate

❗️❗️ 주의
deps가 빈 배열이면 -> componentDidMount 역할.
그렇다면, deps에 state가 존재하면? (빈 배열이 X)
-> componentDidUpdate + componentDidMount 수행.
(즉 componentDidUpdate만 수행하는것이 아님.)

내가 처음에 작성했던 코드

  • useEffect를 두개 작성했음. (..?)
useEffect(() => {
    for (let i = 0; i < winNumbers.length - 1; i++) {
      timeouts.current[i] = setTimeout(() => {
        setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
      }, (i + 1) * 1000);
    }
    timeouts.current[6] = setTimeout(() => {
      setBonus(winNumbers[6]);
      setRedo(true);
    }, 7000);
  }, []); // deps가 빈배열이면 - componentDidMount 역할


// 🔻 componentDidUpdate를 예상했음.
  useEffect(() => {
    if (winBalls.length === 0) {
      for (let i = 0; i < winNumbers.length - 1; i++) {
        timeouts.current[i] = setTimeout(() => {
          setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
        }, (i + 1) * 1000);
      }
      timeouts.current[6] = setTimeout(() => {
        setBonus(winNumbers[6]);
        setRedo(true);
      }, 7000);
    }
  }, [winBalls]);
  • 분명 componentDidUpdate의 기능을 예상하고 작성했지만, 공이 두번씩 렌더링되었다.

  • 즉, deps는 빈 배열이 아니고 state가 들어있지만,
    componentDidMount를 수행한 후에 componentDidUpdate를 수행한 것.
    (render가 두번 일어남)


즉, useEffect에서는 componentDidMount와 componentDidUpdate를 하나의 useEffect 함수로 구현 가능하다.
-> 어차피 실행하는 코드가 같기 때문에!

그런데, 위 조건대로 코드를 작성하면 오류가 발생한다.
따라서, deps를 수정해주어야 한다.

참고

  • deps에는 state만 올 수 있다?
    No!
  • 변경을 관찰할 수 있는 대상이라면 뭐든지 OK.
  • 아래 코드에서는 ref로 사용된 timeoutsdeps로 사용한다.
    -> timeouts는 버튼 클릭시 onClickRedo에 의해 빈 배열로 변경됨.

❗️주의 - timeouts.current[i] = setTimeout(...) 과 같이 배열의 '요소'가 바뀌는 것은 바뀌는것으로 인지하지 않는다.

deps 수정

  useEffect(() => {
    if (winBalls.length === 0) {
      for (let i = 0; i < winNumbers.length - 1; i++) {
        timeouts.current[i] = setTimeout(() => {
          setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
        }, (i + 1) * 1000);
      }
      timeouts.current[6] = setTimeout(() => {
        setBonus(winNumbers[6]);
        setRedo(true);
      }, 7000);
      return () => { 
        // componentWillUnmount 역할
        timeouts.current.forEach((v) => {
          clearTimeout(v);
        });
      };
    }
  }, [timeouts.current]); // ComponentDidMount + ComponentDidUpdate 동시에 수행

🙋‍♂️ 왜 deps가 바뀐건지?

  • 맨 처음에 설정해줬던 조건은 winBalls.length === 0이였다.
  • 따라서, deps 배열 안에 [winBalls.length === 0]을 작성해주면?
    -> 두번씩 렌더링된다.
  • 🙋‍♀️ 왜 두번씩 렌더링 되는지?
    -> winBalls의 초기값이 [] 이므로 맨 처음에도 length === 0 을 만족하게 됨. - 첫 공이 두개 렌더링됨.

❗️ 중요

  • Hooks의 useEffect와
    클래스 컴포넌트의 componentDidUpdate는 완벽하게 일치할 수 X.
  • componentDidUpdate 에서는 if()의 조건식으로
    winBalls.length === 0을 적어줬지만,
  • useEffect 에서는 deps로 winBalls.length === 0를 적어주면 오류가 발생한다.
    -> 따라서 값이 달라질 때만 실행되도록 deps를 잘 작성해줘야 한다.

useMemo와 useCallback

  • For. 성능 최적화
    -> 각 함수에 console.log를 찍어보면?

  • 맨 처음 렌더링 되자마자 useEffect가 실행되고,
    버튼 클릭시 useEffect가 실행됨.
  • 그런데, getWinNumbers 함수가 계속해서 실행됨.
  • Hooks는 상태가 바뀔 때 마다 (Lotto) 함수 전체가 다시 실행된다. (render만 실행되는 클래스 컴포넌트와 달리)
const Lotto = () => {
  const [winNumbers, setWinNumbers] = useState(getWinNumbers());
  ...
}
  • 위 부분도 매번 다시 실행되므로, getWinNumbers()가 계속해서 실행되는 것임.
  • 지금은 큰 문제는 없지만, 만약 반복되는 함수가 10초가 걸리는 함수라면 성능 문제가 발생할 것.

⭐️ 이럴 땐, useMemo를 이용하자!

-> getWinNumbers가 다시 실행되지 않고, 기억할 수 있게 함.

useMemo

  • syntax
useMemo(() => {}, [두번째 인자]);
  • useMemo 적용하기!
const Lotto = () => {
  const lottoNumbers = useMemo(() => getWinNumbers(), []);
  // useEffect, useMemo, useCallback은 모두 두번째 인자 []가 존재한다.
  const [winNumbers, setWinNumbers] = useState(lottoNumbers);
  ...
}

이제 콘솔을 살펴보면,
getWinNumbers 함수가 맨 처음 한번만 실행된다.

useMemo는 두번째 인자가 변경되기 전까지는 다시 실행되지 않는다.

참고 - 메모이제이션

  • 프로그래밍을 할 때 반복되는 결과를 저장해두고 다음에 같은 결과가 나올 때 빨리 실행함.
  • 참고 링크

✅ useMemo vs useRef

useMemouseRef
복잡한 함수 값 기억일반 값을 기억.
  • 함수 안에는 다 console.log를 넣어두고, 성능최적화가 되어있는지 습관적으로 체크해야함.
* * *

useCallback

  • Syntax
useCallback( () => {}, [use]);
  • useMemo와 비슷하지만, 기억하는 값이 다르다!
useMemouseCallback
함수의 리턴값을 기억함수 자체를 기억

- useCallback 예시
const onClickRedo = useCallback(() => {
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);

    timeouts.current = [];
  }, []);
  • onClickRedo를 useCallback으로 감쌈.
    -> 함수 자체를 기억하여 여러번 실행해도 함수를 새로 생성되지 않게.

Q. 그렇다면, 모든 함수에 useCallback을 하는것이 이득인가?

  • No. 그렇지만은 않다.
  • onClickRedo 함수 안에서 consoel.log(winNumbers)를 해보자.

  • 몇번을 다시 뽑아도 winNumbers는 그대로이다.
    (BUT, 화면에 렌더링되는 숫자들은 바뀐다.)
    -> 기억을 너무 잘 한 나머지 첫번째 winNumbers를 그대로 기억하는 것임.

JSX에서 자식 컴포넌트로 함수를 넘길 때,
그 함수에는 반드시 useCallback을 해줘야한다.

const onClickRedo = useCallback(() => {}, []);
// JSX (자식 컴포넌트 Button)
<Button onClick={onClickRedo}>
  • useCallback이 없으면 매번 새로운 함수를 생성하고, 자식 컴포넌트로 계속 새로운 함수를 전달하게 됨.
    -> 자식 컴포넌트 입장에서는 계속 새로운 함수가 들어오므로, 매번 렌더링이 된다.

useCallback 주의사항

  • ❗️ useCallback 안에서 state를 쓸 때는 항상 e두번째 인자인 'inputs' 배열을 적어준다.
  • 까먹을 필요가 있는 경우 (inputs 배열, 즉 두번째 인자에 적어준 값이 바뀌었을때)를 지정해줌.
const onClickRedo = useCallback(() => {
    console.log(winNumbers);
    setWinNumbers(getWinNumbers());
    setWinBalls([]);
    setBonus(null);
    setRedo(false);

    timeouts.current = [];
  }, [winNumbers]);
  • 이제, 계속해서 같은 winNumbers를 기억하는 문제가 해결되었다.

  • 참고로, onClickRedo는 공이 뽑히고 난 후 클릭하므로, 이전 당첨숫자들이 콘솔에 출력된다.

+) useMemo도 마찬가지이다.
useMemo는 함수의 리턴값을 계속 기억하므로,

const Lotto = () => {
  const [winBalls, setWinBalls] = useState([]);
  const lottoNumbers = useMemo(() => getWinNumbers(), [winBalls]);
  const [winNumbers, setWinNumbers] = useState(lottoNumbers);
  
  ...
  
}
  • useMemo의 두번째 인자에 winBall를 주었을 때,
    코드 하단에 setWinBalls(..)에 의해서
    winBalls가 바뀔때마다 useMemo로 저장해둔 리턴값을 잊어버린다.

-> 따라서, getWinNumbers 함수가 이전처럼 계속 실행된다. (winBalls가 바뀔때마다)


Hooks의 TIP!

  1. Hooks는 순서가 매우 중요하므로, 순서가 바뀌어선 안된다!
    -> 조건문 안에 Hooks 절대 사용 금지!
    -> 함수 or 반복문 안에도 사용 자제하기.

  1. useEffect, useMemo, useCallback 의 두번째 인자
useEffectuseMemouseCallback
최초 + 두번째 인자가 바뀌면 실행두번째 인자가 바뀌기 전까지 리턴값 기억두번째 인자가 바뀌기 전까지 함수 기억

  1. useEffect는 여러번 사용해도 된다.
  • 만약, state 별로 실행하는 코드를 다르게 하고 싶다면 useEffect를 여러개 사용하면 된다.
    (예> timeouts.current가 바뀔 때 실행할 코드와 / winNumbers가 바뀔 때 실행할 코드가 다르면)

cf. 클래스 컴포넌트에서는?

  • componentDidUpdate에서 한번에 가능
    -> if문의 조건을 나눠서 코드를 작성해줌.
  • 클래스컴포넌트에서는 여러개의 state를 한꺼번에 사용 가능하지만, Hooks에서는 하나의 state당 하나의 useEffect를 쓰되, didMount+didUpdate를 한꺼번에 가능.

예 -

componentDidUpdate(prevProps, prevState) {
  if(this.state.winBalls.length === 0) {
    // code 1
  }
  if (prevState.winNumbers !== this.state.winNumbers) {
    // code 2
  }

useEffect 에서 didUpdate만 실행하고 싶을 때

  • useEffect에서 두번째 인자를 빈 배열로 두면
    componentDidMount의 역할을 하고,

  • 빈 배열이 아닐 때는 componentDidMount + componentDidUpdate의 역할을 한다.

  • 그렇다면, componentDidUpdate의 역할만 하려면?
    -> didMount때 아무것도 안하게 하면 된다.
    (일종의 꼼수 사용)

// componentDidUpdate에서만 사용 가능하게

 const mounted = useRef(false);

 useEffect(() => {
    if (!mounted.current) {
      mounted.current = true;
    } else {
      // componentDidUpdate에서 실행할 코드
    }
  }, [바뀌는 값]) 
  1. useRef로 임의의 mount 여부를 나타내는 값을 설정.
  2. 최초 실행시, 즉 mounted가 false 일때는 mounted에 true를 대입하고 끝.
  3. mounted가 true가 되고, 두번째 인자에 적어준 값이 바뀌면 다시 useEffect가 실행된다.
  4. 조건을 불충족하므로(mounted가 true니까)
    else문으로 빠져서 코드가 실행된다.

✅ 정리

  • useEffect가 mount에도 실행되는건 어쩔 수 없는 것이지만, update시에만 실행하려면 위와 같이 mount시 아무것도 안하게 하면 된다!
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글