코드스테이츠-부트캠프 [React-심화]-useMemo

김희목·2024년 3월 31일
0

코드스테이츠

목록 보기
45/56

React Hooks

React 16.8 버전부터 추가된 기능으로, 클래스 컴포넌트와 생명주기 메서드를 이용하여 작업을 하던 기존 방식에서 벗어나 함수형 컴포넌트에서도 더 직관적인 함수를 이용하여 작업할 수 있게 만든 기능입니다.

뛰어난 재사용성과 직관성을 갖는 함수형 컴포넌트와 훅은 사용률이 매우 높은 편입니다.


Function Component와 Class Component

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


Class Component

여기 컴포넌트를 클래스로 작성한 간단한 코드를 보겠습니다.

class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            counter: 0
        }
        this.handleIncrease = this.handleIncrease.bind(this);
    }

    handleIncrease = () => {
        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

이번에는 컴포넌트를 함수형 컴포넌트로 작성해 보겠습니다.

function Counter () {
    const [counter, setCounter] = useState(0);

    const handleIncrease = () => {
        setCounter(counter + 1)
    }

    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 사용 규칙

Hook을 사용할 때는 두 가지 규칙을 준수해야만 합니다. 여러분이 Hook을 직접 만들거나, 혹은 작성하면서 자주 볼 수 있는 에러들과 함께 규칙들을 살펴보도록 하겠습니다.

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

반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한 대로 동작하지 않을 우려가 있습니다.

...
if(counter) {
    const [sample, setSample] = useState(0);
}
...

예를 들어 count가 있을 때 sample이라는 state를 사용하고 싶어서 조건문 안에 useState() hook을 불러왔다고 가정해 봅시다.(이런 식의 가정은 애초부터 틀린 가정입니다. 예시를 위해 이러한 가정을 한 것이라 이해해 주세요.) 이런 식으로 호출을 하게 되면 React의 동작 방식에 거스르기 때문에 React는 에러를 출력합니다.

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

이는 리액트 함수형 컴포넌트나 커스텀 Hook이 아닌 다른 일반 JavaScript 함수 안에서 호출해서는 안 된다는 의미입니다.

...
window.onload = function () {
    useEffect(() => {
        // do something...
    }, [counter]);
}
...

예를 들어 window의 요소가 모두 준비가 되면 useEffect()가 호출되었으면 좋겠다고 생각해서 함수를 작성했다고 가정해 봅시다.(이런 식의 가정은 애초부터 틀린 가정입니다. 예시를 위해 이러한 가정을 한 것이라 이해해 주세요.) 이 또한 React의 동작 방식에 위배되기 때문에 React는 에러를 출력합니다.

애초에 Hook은 React의 함수 컴포넌트 내에서 사용되도록 만들어진 메서드이기 때문에 근본적으로 일반 JavaScript 함수 내에서는 정상적으로 돌아가지 않습니다. 따라서 이 규칙 또한 반드시 준수해야 하는 규칙입니다.


useMemo란?

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


너무 많은 렌더링은 앱에 안 좋은 성능을 미치는 극단적인 예

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

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

아래 코드를 보면서 useMemo에 대해 좀 더 알아봅시다.

function Calculator({value}){

	const result = calculate(value);

	return <>
      <div>
					{result}
      </div>
  </>;
}

해당 컴포넌트는 props로 넘어온 value값을 calculate라는 함수에 인자로 넘겨서 result 값을 구한 후, div 엘리먼트로 출력을 하고 있습니다.

만약 여기서 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을 호출한다면 이런 로직을 직접 구현하는 것을 대신해 주기 때문에 훨씬 간편하다고 할 수 있습니다.


useMemo를 이용하여 앱 최적화하기

import React, { useState } from "react";
import "./styles.css";
import { add } from "./add";

export default function App() {
  const [name, setName] = useState("");
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  const answer = add(val1, val2);

  return (
    <div>
      <input
        className="name-input"
        placeholder="이름을 입력해주세요"
        value={name}
        type="text"
        onChange={(e) => setName(e.target.value)}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val1}
        type="number"
        onChange={(e) => setVal1(Number(e.target.value))}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val2}
        type="number"
        onChange={(e) => setVal2(Number(e.target.value))}
      />
      <div>{answer}</div>
    </div>
  );
}

해당 컴포넌트는 아직 최적화되지 않은 컴포넌트입니다. 위의 컴포넌트에서 실제로 연산 로직에 영향을 주는 값은 val1과 val2입니다. 현재는 이름 상태가 변화하면 add 함수가 계속 같은 결과값을 리턴함에도 불구하고 불필요하게 계속 호출되고 있기 때문에, useMemo를 이용하여 add함수의 호출을 최소화해야만 합니다. 즉 여러분이 이름을 입력할 때는 add 함수가 호출되지 않아야 최적화가 된 컴포넌트라고 볼 수 있습니다.

import React, { useState, useMemo } from "react";
import "./styles.css";
import { add } from "./add";

export default function App() {
  const [name, setName] = useState("");
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  
  // useMemo를 사용하여 add 함수의 결과 값을 최적화
  const answer = useMemo(() => add(val1, val2), [val1, val2]);

  return (
    <div>
      <input
        className="name-input"
        placeholder="이름을 입력해주세요"
        value={name}
        type="text"
        onChange={(e) => setName(e.target.value)}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val1}
        type="number"
        onChange={(e) => setVal1(Number(e.target.value))}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val2}
        type="number"
        onChange={(e) => setVal2(Number(e.target.value))}
      />
      <div>{answer}</div>
    </div>
  );
}

0개의 댓글