React.js

21.04.02 useMemo, useCallback, useRef

슈팅스타제제 2021. 4. 10. 15:40

평균값을 구해보자.

📌Average.js 

import React, {useCallback, useMemo, useRef, useState} from 'react';

const getAverage = (numbers)=>{
	console.log('평균값 계산 중');
    if(numbers.length === 0) return 0;
    const sum = numbers.reduce((a, b)=>a+b);
    return sum/numbers.length;
};

getAverage 함수에 numbers 배열 인자를 받으면 reduce 함수에 의해 numbers의 모든 원소 값을 합산하고 이 반환값을 변수 sum에 할당한다. 

getAverage 함수의 반환값은 sum/numbers.length이 된다. 

따라서 모든 원소의 합/배열의 원소 갯수 이므로 평균을 계산하게 된다. 

 

📌useMemo

memorization 을 적용하여 함수형 컴포넌트 내부에서 발생하는 연산을 최적화한다. 

렌더링하는 과정에서 특정값이 바뀌었을 때만 연산을 실행시킨다. 

(이렇게 해주면 기존 함수는 두번째 prop이 달라졌을 때만 호출이 되고 동일할 때는 최초 호출 결과가 계속해서 재사용된다. )

입력 필드에 글자를 입력할 때마다 지연이 발생하지 않는 장점이 있지만 남용하면 복잡도가 올라가고 메모리를 더 쓰게 된다는 단점도 있다. 

const Average = () => {
  const [list, setList] = useState([]); //list 배열의 초기값은 빈 배열
  const [number, setNumber] = useState(''); //number의 초기값은 빈 값

  //이벤트 객체에서 입력한 부분의 value를 가져온다.
  const onChange = (e) => {
    setNumber(e.target.value);
  };

  const onInsert = (e) => {
    //number에 입력받은 값이 문자열이기 때문에 parseInt 함수를 통해 숫자로 바꿔준 후
    //concat 함수를 통해 list 에 number 값을 추가하고 이를 새로운 배열인 nextList에 할당한다.
    const nextList = list.concat(parseInt(number));
    //이 새로운 배열로 list를 업데이트한다.
    setList(nextList);
    //number값은 초기화를 시킨다.
    setNumber('');
  };
  const avg = useMemo(() => getAverage(list), [list]);

첫번째 인자는 결과값을 생성해주는 팩토리함수이고 두번째는 기존 결과값 재활용 여부의 기준이 되는 입력값 배열이다. 기존 함수를 useMemo 함수의 첫번째 인자로 넘기고 두번째 인자로 prop이 든 배열을 넘긴다. 

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>클릭</button>
      <ul>
        {list.map((value, index) => {
          <li key={index}>{value}</li>;
        })}
      </ul>
      <div>
        <b>평균값 : </b>
        {avg}
      </div>
    </div>
  );
};

📌useCallback

렌더링 성능을 최적화해야하는 상황에서 만들어놓은 함수를 재사용할 수 있다. 

첫번째 파라미터에는 생성하고 싶은 함수를 넣고 두번째 파라미터에는 배열을 넣는다. 

배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시해야 한다. 

함수 내부에서 상태 값에 의존해야 하는 경우 그 값을 반드시 두번째 파라미터 안에 포함시켜야 한다. 

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');

  const onChange = useCallback((e) => {
    setNumber(e.target.value);
    //기존의 값을 조회하지 않고 바로 설정만 하기 때문에 배열이 비어있어도 된다.
  }, []); //컴포넌트가 처음 렌더링될 때만 함수 생성

  const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    //기존의 값을 조회해서 nextList를 생성하기 때문에 배열 안에 number, list를 넣어줘야 한다.
  }, [number, list]);//number 혹은 list가 바뀌었을 때만 함수를 생성한다.

아래 부분은 useMemo 예제와 동일하다.

  const avg = useMemo(() => getAverage(list), [list]);
  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>클릭</button>
      <ul>
        {list.map((value, index) => {
          <li key={index}>{value}</li>;
        })}
      </ul>
      <div>
        <b>평균값 : </b>
        {avg}
      </div>
    </div>
  );
};

📌그러면 useMemo와 useCallback의 차이점이 뭐지?

useCallback은 useMemo를 기반으로 만들어지긴 했다. useMemo는 특정 결과값을 재사용할 때 사용하는 반면, useCallback은 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.

 

이 함수들은 컴포넌트가 리렌더링될 때마다 새로 만들어진다. 한번 만든 함수를 필요할 때만 새로 만들고 재사용하는 것이 필요하다. 그 이유는 우리가 나중에 컴포넌트에서 props가 바뀌지 않았으면 virtual DOM에 새로 렌더링하는 것조차 하지 않고 컴포넌트의 결과물을 재사용하는 최적화 작업을 할 것인데 이 작업을 하기 위해서는 함수를 재사용하는 것이 필수이기 때문이다. 

 

📌useRef

함수형 컴포넌트에서 ref를 쉽게 사용할 수 있게 해준다. 

useRef를 사용하여 ref를 설정하면 useRef를 통해 만든 객체 안의 current 값이 실제 엘리먼트를 가리킨다. 

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');
  const inputEl = useRef(null); //ref를 설정해주기 위해

  const onChange = useCallback((e) => {
    setNumber(e.target.value);
  }, []);
  const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    //ref를 사용해야 하는 focus
    inputEl.current.focus();
  }, [number, list]);

  const avg = useMemo(() => getAverage(list), [list]);
  //ref={}에 참조할 엘리먼트를 넣어준다.
  return (
    <div>
      <input value={number} onChange={onChange} ref={inputEl} />
      <button onClick={onInsert}>클릭</button>
      <ul>
        {list.map((value, index) => {
          <li key={index}>{value}</li>;
        })}
      </ul>
      <div>
        <b>평균값 : </b>
        {avg}
      </div>
    </div>
  );
};

export default Average;

위 코드에서는 컴포넌트를 렌더링한 이후에도 커서의 포커스가 입력 창 엘리먼트 안에 남아있게 설정한 것이다.