React 심화 (2) - useMemo, useCallback

선정·2022년 7월 28일
0

Today I Learned

  • React Hooks
  • useMemo
  • useCallback

React Hooks

React Hooks는 React 16.8 버전부터 추가된 기능이다. Hook을 이용하면 클래스 컴포넌트(Class Component)와 생명주기 메서드를 이용하여 작업을 하던 기존 방식에서 벗어나 함수형 컴포넌트(Function Component)에서도 상태 값과 여러 React의 기능을 사용할 수 있다.

Hook 사용 규칙

1. 리액트 함수의 최상위에서만 호출해야 한다.

⭕️ 리액트 컴포넌트 최상위에서 호출

import React, { useState } from "react";

const Login = () => {
  const [value, setValue] = useState("");
  
  return <div></div>
}

위와 같이 리액트 컴포넌트의 최상위가 아닌 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한 대로 동작하지 않을 수 있다. 그렇기 때문에 React는 에러를 반환한다.

❌ 중첩된 함수 내부에서 호출

import React, { useState } from "react";

const Login = () => {
  
  function callHook = () => {
      const [value, setValue] = useState("");
  }
  
  return <div></div>
}


리액트 컴포넌트 내부에 선언된 함수에서 useState를 호출하면 위와 같은 에러가 발생한다. 리액트 함수형 컴포넌트도, 커스텀 훅도 아닌 위치에서 useState를 호출했다는 에러메시지를 출력하고 있다.

2. 오직 리액트 함수 내에서만 사용되어야 한다.

위의 에러 메시지를 토대로 리액트 함수형 컴포넌트나 커스텀 훅이 아닌 곳에서는 Hook을 호출할 수 없다는 것을 알 수 있다. 즉, 다른 일반 JavaScript 함수 안에서 호출해서는 안 된다는 것이다.


useMemo

useMemo은 특정 값를 재사용하고자 할 때 사용하는 Hook으로, 메모이제이션된 값을 반환한다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

“생성(create)” 함수와 그것의 의존성 값의 배열을 전달하면 의존성이 변경되었을 때에만 메모이제이션된 값을 다시 계산한다. 이런 방식을 통해 모든 렌더링 시의 고비용 계산을 방지할 수 있다.

src/Calculator.js

import { useState, useMemo } from "react";
import { easyCalculate, hardCalculate } from "./util/calculate"

function Calculator() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);
  
  // 시간이 오래 걸리는 고비용 계산
  const hardSum = useMemo(() => hardCalculate(hardNumber), [hardNumber]);
  const easySum = easyCalculate(easyNumber);

  return (
    <div>
      <h3>hardSum 계산하기</h3>
      <input 
        type="value"
        value={hardNumber}
        onChange={(e) => setHardNumber(parseInt(e.target.value))
        />
      <span>{hardSum}</span>
          
      <h3>easySum 계산하기</h3>
      <input 
        type="value"
        value={easyNumber}
        onChange={(e) => setEasyNumber(parseInt(e.target.value))
        />
      <span>{easySum}</span>
    </div>
  )
}

useMemo를 적용한 샘플 코드를 알아보자. 위의 예시에서는 hardCalculate, easyCalculate의 2개의 값 계산하는 함수를 사용하고 있다.
그 중 hardCalculate는 연산하는 시간이 오래 걸리는 함수이기 때문에 필요 시에만(hardNumber 값이 변경 됐을 때만) 새로 계산할 수 있도록useMemo를 사용해주었다.

이렇게 처리하면 easyNumber와 같은 다른 상태값이나 props가 변경돼 컴포넌트가 리렌더링 되더라도 hardCalculate 함수를 다시 실행하는 대신, hardSum에 이전에 계산했던 값을 꺼내와 재사용한다.

useMemo를 사용하면 메모리에 공간을 확보해 해당 값을 저장하는데, 이 비용과 렌더링 시 다시 계산하는 비용를 비교해 생각해봐야 한다. useMemo를 남발해서는 안 된다.

메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 동적 계획법의 핵심이 되는 기술이다.

참고 영상


useCallback

useCallback 또한 useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook으로, 메모이제이션된 콜백을 반환한다.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

인라인 콜백과 이에 대한 의존성 값을 배열에 전달하면 메모이제이션된 콜백은 의존성이 변경되었을 때에만 변경된다.

useCallback은 함수를 새로 호출해 값을 리턴하는 것과 관련된 useMemo와 달리, 함수를 새로 생성(할당)하는 것과 관련 돼있다. 때문에 하나의 컴포넌트 내부에서만 활용되는 경우에는 의미가 없거나 오히려 손해일 수 있다. 그럼 useCallback은 어떻게 활용해야 할까?

useCallback은 자식 컴포넌트의 props로 함수를 전달할 때, 사용하기 좋다.
➡️ 자식 컴포넌트의 불필요한 리렌더링 방지할 수 있다❗️

useMemo와 같은 이유로 useCallback 또한 남발하지 않는 것이 좋다.

useCallback(fn, deps)useMemo(() => fn, deps)와 같다.


useCallback과 참조 동등성

useCallback은 참조 동등성에 의존한다. 함수는 객체이며, 객체는 메모리에 저장할 때 값을 저장하는 게 아니라 값의 주소를 저장하기 때문에, 반환하는 값이 같을 지라도 일치연산자로 비교했을 때 false가 출력된다.

const testFn = () => {
  return () => console.log("test");
};

const test1 = testFn();
const test2 = testFn();

test1(); // "test"
test2(); // "test"

console.log(test1 === test2); // false

React는 렌더링 될 때마다 함수를 새로 만들어서 호출한다. 새로 만들어 호출된 함수는 기존의 함수와 같은 함수가 아니다. 그러나 useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용한다는 것과 같다고 볼 수 있다. 따라서 React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있다.

profile
starter

0개의 댓글