useCallback & useMemo

hyena_lee·2023년 3월 22일
0

React

목록 보기
6/10
post-thumbnail

Function Component와 Class Component

  • Hook은 함수 컴포넌트에서 사용하는 메소드입니다. 함수 컴포넌트 이전에는 클래스(class) 컴포넌트가 있었습니다. 많은 React 개발자들이 이 클래스 컴포넌트를 사용하여 React 앱을 개발해왔습니다.

Class Component

// Counter 클래스를 선언합니다.
class Counter extends Component {
  // props를 받아와서 상위 클래스의 생성자를 호출합니다.
  constructor(props) {
    super(props);
    // state를 초기화합니다.
    this.state = {
      counter: 0,
    };
    // handleIncrease 함수에서 this를 사용하기 위해 bind합니다.
    this.handleIncrease = this.handleIncrease.bind(this);
  }

  // handleIncrease 함수를 선언합니다.
  handleIncrease = () => {
    // state를 업데이트합니다.
    this.setState({
      counter: this.state.counter + 1,
    });
  };

  // 렌더링 함수를 선언합니다.
  render() {
    return (
      <div>
        <p>You clicked {this.state.counter} times</p>
        <button onClick={this.handleIncrease}>Click me</button>
      </div>
    );
  }
}

  • 이런 클래스 컴포넌트는 복잡해질수록 이해하기 어려워졌고, 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다는 단점이 있었습니다.

  • React의 클래스 컴포넌트를 사용하기 위해서는 JavaScript의 this 키워드가 어떤 방식으로 동작하는지 알아야 하는데, 이는 문법을 정확히 알지 못하면 동작 방식 자체를 정확히 이해하기 어렵게 만들곤 했습니다.

  • React는 점진적으로 클래스 컴포넌트에서 함수 컴포넌트로 넘어갔습니다. 다만 이전까지의 함수 컴포넌트는 클래스 컴포넌트와는 다르게 상태 값을 사용하거나 최적화할 수 있는 기능들이 조금 미진했는데, 그 부분들을 보완하기 위해 Hook이라는 개념을 도입하였습니다.

Function Component

// useState 훅을 사용하여 counter 변수와 setCounter 함수를 선언한다.
function Counter() {
  const [counter, setCounter] = useState(0);

  // handleIncrease 함수를 선언한다.
  const handleIncrease = () => {
    // setCounter 함수를 사용하여 counter 값을 1 증가시킨다.
    setCounter(counter + 1);
  };

  // JSX를 반환한다.
  return (
    <div>
      <p>You clicked {counter} times</p>
      <button onClick={handleIncrease}>Click me</button>
    </div>
  );
}
  • 함수형 컴포넌트는 클래스형 컴포넌트에 비해 훨씬 더 직관적이고, 보기 쉽다는 특징이 있습니다. 이 Counter 컴포넌트에서 숫자를 올리기 위해 상태값을 저장하고 사용할 수 있게 해주는 useState() 가 있는데, 여러분도 익히 알고 있는 이 메서드가 바로 Hook입니다.

  • 다시 말하자면, Counter 컴포넌트에서 useState() Hook을 호출해 함수 컴포넌트(function component) 안에 state를 추가한 형태입니다. 이 state는 컴포넌트가 리렌더링 되어도 그대로 유지될 것입니다. 또한 해당 컴포넌트에서 State Hook은 하나만 사용했지만 때에 따라서 여러 개 사용할 수 있습니다.

Hook이란?

  • React의 공식문서를 보면 Hook에 대해 이런 문구가 있습니다.

    Hook은 React 16.8에 새로 추가된 기능입니다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해줍니다.

Hook은 다르게 말하면 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메소드를 의미합니다. Hook은 class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않습니다.


render(){
    /* 클래스 컴포넌트는 render() 안에서 변수를 작성할 수 있습니다. */
    const [counter, setCounter] = useState(0);
...
}
  • 억지로 호출을 해보려고 해도 해당 방식은 React에서 허락하지 않는 호출 방식이기 때문에 위와 같은 에러를 브라우저 전면에 띄웁니다. 해당 에러를 삭제하면 컴포넌트 자체가 렌더링이 되지 않는 것을 볼 수 있습니다.

Hook 사용 규칙

  1. 리액트 함수의 최상위에서만 호출해야 합니다.
  • 반복문, 조건문, 중첩된 함수 내에서 HOOK을 실행하면 예상한 대로 동작하지 않을 우려가 있다.
  1. 오직 리액트 함수 내에서만 사용되어야 합니다.
  • 이는 리액트 함수형 컴포넌트나 커스텀 Hook이 아닌 다른 일반 JavaScript 함수 안에서 호출해서는 안 된다는 의미입니다.

useMemo

컴포넌트는 기본적으로 상태가 변경되거나 부모 컴포넌트가 렌더링이 될 때마다 리렌더링을 하는 구조로 이루어져 있습니다. 그러나 너무 잦은 리렌더링은 앱에 좋지 않은 성능을 끼칩니다.

  • React Hook은 함수 컴포넌트가 상태를 조작하고 및 최적화 기능을 사용할 수 있게끔 하는 메소드라고 했습니다. 그 중 렌더링 최적화를 위한 Hook도 존재하는데, useCallback과 useMemo가 바로 그 역할을 하는 Hook입니다.

useMemo란?

useMemo은 특정 값(value)를 재사용하고자 할 때 사용하는 Hook입니다.

function Calculator({value}){

	const result = calculate(value);

	return <>
      <div>
					{result}
      </div>
  </>;
}
  • 해당 컴포넌트는 props로 넘어온 value값을 calculate라는 함수에 인자로 넘겨서 result 값을 구한 후,

    엘리먼트로 출력을 하고 있습니다.

  • 만약 여기서 calculate가 내부적으로 복잡한 연산을 해야 하는 함수라 계산된 값을 반환하는 데에 시간이 몇 초 이상 걸린다고 가정해 봅시다. 그렇다면 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속해서 호출할 것이고, 그 때마다 시간이 몇 초 이상 소요가 될 것입니다. 이 몇 초의 지연은 렌더링에도 영향을 미칠 것이고, 사용자는 “앱의 로딩 속도가 느리네?”라는 생각을 하게 될 것입니다.

     /* useMemo를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
    import { useMemo } from "react";
    function Calculator({value}){
    
    	const result = useMemo(() => calculate(value), [value]);
    
    	return <>
         <div>
    					{result}
         </div>
     </>;
    }
    
  • 여기 value 를 인자로 받는 Calculator 컴포넌트가 있습니다.

  • value 는 일종의 값으로서, 이 값이 계속 바뀌는 경우라면 어쩔 수 없겠지만, 렌더링을 할 때마다 이 value값이 계속 바뀌는 게 아니라고 생각해 봅시다. 그럼 이 값을 어딘가에 저장을 해뒀다가 다시 꺼내서 쓸 수만 있다면 굳이 calculate 함수를 호출할 필요도 없을 것입니다. 여기서 useMemo Hook을 사용할 수 있습니다.

  • 이런 식으로 useMemo를 호출하여 calculate를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용할 수 있게 됩니다. 이는 메모이제이션(Memoization) 개념과 긴밀한 관계가 있습니다.

Memoization

  • 메모이제이션(Memoization)은 알고리즘에서 자주 나오는 개념입니다. 기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말합니다. 이 메모이제이션을 적절히 사용한다면 굳이 중복 연산을 할 필요가 없기 때문에 앱의 성능을 최적화할 수 있습니다.

  • useMemo는 바로 이 개념을 이용하여 복잡한 연산의 중복을 피하고 React 앱의 성능을 최적화시킵니다. 직접 메모이제이션 개념을 이용하여 로직을 구현할 수도 있겠으나, useMemo Hook을 호출한다면 이런 로직을 직접 구현하는 것을 대신해주기 때문에 훨씬 간편하다고 할 수 있습니다.

useCallback

  • React Hook은 렌더링 최적화를 위한 Hook도 존재하는데, useCallback과 useMemo가 바로 그 역할을 하는 Hook라고 배웠습니다.

  • useCallback이란?

    useCallback 또한 useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook입니다. useMemo는 값의 재사용을 위해 사용하는 Hook이라면, useCallback은 함수의 재사용을 위해 사용하는 Hook입니다.

function Calculator({x, y}){

	const add = () => x + y;

	return <>
      <div>
					{add()}
      </div>
  </>;
}
  
  • 현재 이 Calculator 컴포넌트 내에는 add라는 함수가 선언이 되어 있는 상태입니다. 이 add 함수는 props로 넘어온 x와 y 값을 더해
    태그에 값을 출력하고 있습니다. 이 함수는 해당 컴포넌트가 렌더링 될 때마다 새롭게 만들어질 것입니다.

useMemo와 마찬가지로, 해당 컴포넌트가 리렌더링 되더라도 그 함수가 의존하고 있는 값인 x와 y가 바뀌지 않는다고 생각해 봅시다. 그렇다면 함수 또한 메모리 어딘가에 저장해 뒀다가 다시 꺼내서 쓸 수 있을 것입니다.

이때 useCallback Hook을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환합니다. 즉 x와 y값이 동일하다면 다음 렌더링 때 이 함수를 다시 사용합니다.

/* useCallback를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import React, { useCallback } from "react";

function Calculator({x, y}){

	const add = useCallback(() => x + y, [x, y]);

	return <>
      <div>
					{add()}
      </div>
  </>;
}
  • 사실 useCallback만 사용해서는 useMemo에 비해 괄목할 만한 최적화를 느낄 수는 없습니다.
  • 왜냐하면 useCallback은 함수를 호출을 하지 않는 Hook이 아니라, 그저 메모리 어딘가에 함수를 꺼내서 호출하는 Hook이기 때문입니다.
  • 따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있습니다. 그러면 언제 사용하는 게 좋을까요? 자식 컴포넌트의 props로 함수를 전달해줄 때 이 useCallback을 사용하기가 좋습니다.

useCallback과 참조 동등성

  • useCallback은 참조 동등성에 의존합니다. React는 JavaScript 언어로 만들어진 오픈소스 라이브러리이기 때문에 기본적으로 JavaScript의 문법을 따라갑니다.
  • JavaScript에서 함수는 객체입니다. 객체는 메모리에 저장할 때 값을 저장하는 게 아니라 값의 주소를 저장하기 때문에, 반환하는 값이 같을 지라도 일치연산자로 비교했을 때 false가 출력됩니다.
function doubleFactory(){
    return (a) => 2 * a;
}
  
const double1 = doubleFactory();
const double2 = doubleFactory();
  
double1(8); // 16
double2(8); // 16
  
double1 === double2;  // false
double1 === double1;  // true

  • double1과 double2는 같은 함수를 할당했음에도 메모리 주소 값이 다르기 때문에 같다고 보지 않습니다.

  • JavaScript에서 함수는 객체입니다. 따라서 두개의 함수는 동일한 코드를 공유하더라도 메모리 주소가 다르기 때문에, 메모리 주소에 의한 참조 비교 시 다른 함수로 봅니다.

  • 이는 React 또한 같습니다. React는 리렌더링 시 함수를 새로이 만들어서 호출을 합니다. 새로이 만들어 호출된 함수는 기존의 함수와 같은 함수가 아닙니다.

  • 그러나 useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용한다는 것과 같다고 볼 수 있습니다.

  • 따라서 React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있습니다.

결론

useCallback과 useMemo는 거의 비슷한 것입니다.
하지만 다른 것은 useCallback 은 메모이제이션된 함수를 반환하며.
useMemo 은 메모이제이션된 값를 반환합니다.

profile
실수를 두려워 말고 계속 도전 하는 개발자의 여정!

0개의 댓글