..

Search

28) Performance Hooks

Performance Hooks


Performance Hooks

컴포넌트를 리렌더링할 때 성능을 최적화하는 가장 일반적인 방법은 바로 불필요한 작업을 건너뛰는 것입니다. 예를 들어, 앞서 렌더링한 이후 데이터가 변경되지 않았다면 이전에 캐시한 데이터를 재사용하거나 리렌더링을 건너뛰도록 설정하면 성능을 향상 시킬 수 있습니다.

 

React에서 데이터를 캐시하려면 다음 Hook 중 하나를 사용하면 됩니다.

1. useMemo를 사용하면 이전에 수행한 계산 결과를 캐시하여 재사용할 수 있습니다.

2. useCallback을 사용하면 이미 만들어 놓은 함수를 캐시하여 재사용할 수 있습니다.


useMemo Hook

useMemo를 사용하여 이전에 수행한 계산 결과를 캐시해 놓으면, 컴포넌트 내부에서 발생하는 연산 작업을 최적화할 수 있습니다.

useMemo는 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고, 해당 값이 바뀌지 않았다면 캐시해 놓은 이전 계산 결과를 그대로 재사용하는 방식으로 최적화를 수행합니다.

useMemo 문법
const cachedValue = useMemo(calculateValue, dependencies)

 

useMemo의 첫 번째 인수는 해당 값을 계산하는 함수이며, 두 번째 인수는 배열을 전달 받습니다. 두 번째 인수로 전달 받은 배열에 포함된 값이 변경되면 첫 번째 인수로 전달된 함수를 호출하여 해당 값을 계산하고, 만약 값이 변경되지 않았다면 이전에 연산한 값을 그대로 재사용하게 됩니다.

 

다음 예제는 useMemo를 사용하여 todos와 tab이 변경될 때에만 filterTodos() 함수를 호출하여 재연산하는 예제입니다.

예제(App.js)
import { useState, useMemo } from "react";

// Todo를 30개를 랜덤으로 생성함
const randomTodos = () => {
  const todos = [];
  for (let i = 0; i < 30; i++) {
    todos.push({
      id: i,
      text: `Todo ${i + 1}`,
      completed: Math.random() > 0.5
    });
  }
  return todos;
};

const todos = randomTodos();

// 필터 기능 구현
const filterTodos = (todos, tab) => {
  let startTime = performance.now();
  while (performance.now() - startTime < 2) {
    // while (performance.now() - startTime < 300) {
    // 굉장히 많은 연산으로 느려지는 코드를 시뮬레이션 하기 위해서 300ms를 아무것도 하지 않는 코드
    // 하지만 codesandbox 정책 상 10,000번 이상의 반복문이 되어 에러를 발생시킴.
    // 따라서 이 코드를 로컬에서 실행시킬 때는 startTime < 500으로 수정하여 테스트할 것.
  }
  return todos.filter((todo) => {
    if (tab === "completed") {
      return todo.completed;
    } else if (tab === "incompleted") {
      return !todo.completed;
    }
    return true;
  });
};

const TodoList = ({ todos, tab }) => {
  // todos와 tab이 변경될 때만 filterTodos() 함수를 호출하여 값을 재연산함
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  return (
    <ul>
      {visibleTodos.map((todo) => (
        <li key={todo.id}>{todo.completed ? <s>{todo.text}</s> : todo.text}</li>
      ))}
    </ul>
  );
};

const App = () => {
  const [tab, setTab] = useState("all");
  return (
    <>
      Filter :<button onClick={() => setTab("all")}>모두</button>
      <button onClick={() => setTab("incompleted")}>미완료</button>
      <button onClick={() => setTab("completed")}>완료</button>
      <TodoList todos={todos} tab={tab} />
    </>
  );
};

export default App;

 

위의 예제에서 filterTodos() 함수는 굉장히 많은 연산으로 느려지는 코드를 시뮬레이션 하기 위해서 300ms 동안 아무것도 하지 않는 코드를 작성하였습니다.

 

하지만 우리가 온라인 예제를 실행시키는 CodeSandbox에서는 정책 상 10,000번 이상의 반복문을 실행할 수 없기 때문에 우선 2ms 정도만 느려지도록 코드를 바꾸었습니다. 하지만 이렇게 변경된 코드로 여러분이 많은 연산에 의해 느려진 코드를 실제로 체감하기는 힘들기 때문에, 만약 여러분의 로컬 PC에 React가 설치되어 있다면 예제 코드를 300으로 수정하여 실행시켜 보길 바랍니다.

 

일반적으로 애플리케이션의 성능 최적화에는 장점과 단점이 동시에 존재합니다. useMemo도 자주 사용하게 되면 컴포넌트 간의 상호 의존도가 복잡해지고, 코드의 가독성도 안 좋아집니다. 따라서 성능을 최적화 할 때 얻을 수 있는 이점과 최적화 작업에 소비되는 대가를 비교하여 최적화 여부와 방법에 대해 고민해야 할 것 입니다.


useCallback Hook

useCallback을 사용하면 이전에 정의해 놓은 함수를 캐시해 놓음으로써, 렌더링 성능을 최적화할 수 있습니다. useCallback은 리렌더링 간 함수의 정의를 캐시하여 필요한 때 해당 함수를 재생성하는 방식으로 최적화를 수행합니다.

useCallback 문법
const cachedFn = useCallback(fn, dependencies)

 

useCallback의 첫 번째 인수는 생성하고 싶은 함수의 정의이며, 두 번째 인수는 배열을 전달 받습니다. 이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시합니다.

 

useCallback은 두 번째 인수로 전달 받은 배열에 포함된 값이 변경되면, 첫 번째 인수로 전달된 함수의 정의를 사용하여 해당 함수를 생성합니다.

만약 빈 배열을 전달하면 컴포넌트가 렌더링될 때 단 한 번만 함수가 생성되며, 배열에 숫자나 리스트를 포함시키면 해당 값이 변경되거나 리스트에 새로운 요소가 추가될 때마다 함수가 생성됩니다. 특히 생성하고자 하는 함수 내부에서 state 값을 사용해야 한다면, 두 번째 인수에 해당 state도 같이 포함시켜야 합니다.

 

다음 예제는 앞서 state hooks 수업에서 살펴본 버튼을 클릭하면 화면의 숫자가 증가시키는 카운터 예제에 리셋 기능을 추가한 예제입니다.

Reset.js
import { memo } from "react";

const Reset = ({ handleClick }) => {
  return <button onClick={handleClick}>리셋</button>;
};

export default memo(Reset);
예제(Counter.js)
import { useState } from "react";
import Reset from "./Reset";

const Counter = () => {
  const [state, setState] = useState(0);

  const handleReset = () => {
    setState(0);
  };

  return (
    <>
      <h1>State 값 : {state}</h1>
      <button onClick={() => setState(state + 1)}>1씩 증가</button>
      <Reset handleClick={handleReset} />
    </>
  );
};

export default Counter;

 

위의 예제에서 Reset 컴포넌트는 메모이제이션 메소드인 memo() 덕분에 state에 의존하지 않는 코드라고 생각되기 쉽지만, 실제로는 state가 변경될 때마다 리렌더링 됩니다. 이때 아래 코드처럼 useCallback을 사용하면 state가 변경될 때마다 실행되는 불필요한 리렌더링을 방지할 수 있게 됩니다.

 
const handleReset = useCallback(() => {
  setState(0);
}, []);

연습문제