Javascript

React 컴파일러를 이용한 성능 최적화: 불필요한 렌더링은 이제 그만!

드리프트2 2024. 8. 4. 12:20

 

 

React는 현대 웹 개발에서 가장 인기 있는 JavaScript 라이브러리 중 하나로, 사용자 인터페이스를 구축하는 데 있어 강력하고 유연한 도구입니다.

 

하지만 컴포넌트의 복잡성이 증가함에 따라 성능 문제가 발생할 수 있는데, 특히 불필요한 렌더링은 애플리케이션의 속도를 저하시키는 주요 원인 중 하나입니다.

 

이러한 문제를 해결하기 위해 React는 컴파일러를 통해 혁신적인 성능 최적화 기능을 제공합니다.

 

본 글에서는 React 컴파일러의 기능과 장점을 실제 코드 예제를 통해 자세히 살펴보고, 기존의 useMemo와 같은 수동 최적화 방식과 비교하여 어떻게 더 효율적인 메모이제이션을 제공하는지 알아보겠습니다.

 

1. 기존 React 컴포넌트의 렌더링 문제점

 

React 컴포넌트는 상태(State) 또는 속성(Props)이 변경될 때마다 다시 렌더링됩니다.

 

이는 단순한 애플리케이션에서는 큰 문제가 되지 않지만, 컴포넌트 트리가 복잡해지고 데이터가 많아질수록 불필요한 렌더링으로 인해 성능 저하가 발생할 수 있습니다.

 

특히, 자식 컴포넌트가 부모 컴포넌트의 상태 변경에 영향을 받지 않음에도 불구하고 재렌더링되는 경우가 많습니다.

 

아래 예제를 통해 기존 React 컴포넌트의 렌더링 문제점을 살펴보겠습니다.

 

TodoList 컴포넌트는 여러 개의 TodoListItem 컴포넌트를 렌더링하는 간단한 할 일 목록입니다.

import React, { useState } from 'react';

const TodoListItem = ({ todo }) => {
  console.log('TodoListItem rendering:', todo.id);
  return <li>{todo.text}</li>;
};

const TodoList = ({ todos }) => {
  console.log('TodoList rendering');
  return (
    <ul>
      {todos.map((todo) => (
        <TodoListItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
};

const App = () => {
  const [todos, setTodos] = useState([
    { id: 1, text: 'React 공부하기' },
    { id: 2, text: 'Redux 배우기' },
  ]);
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>할 일 목록</h1>
      <TodoList todos={todos} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
};

export default App;

 

위 코드에서 App 컴포넌트의 count 상태가 변경될 때마다 TodoList와 모든 TodoListItem 컴포넌트가 다시 렌더링됩니다.

 

count 상태는 TodoListTodoListItem 컴포넌트에 직접적인 영향을 미치지 않지만, React는 기본적으로 모든 자식 컴포넌트를 재렌더링하기 때문에 불필요한 연산이 발생합니다.

 

2. useMemo를 이용한 수동 최적화

 

이러한 문제를 해결하기 위해 React는 useMemo 훅을 제공합니다. useMemo는 특정 값을 메모이제이션하여 불필요한 재계산을 방지합니다.

 

아래 예제는 useMemo를 사용하여 todos 배열을 메모이제이션하는 방법을 보여줍니다.

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

// ... (TodoListItem, TodoList 컴포넌트는 동일)

const App = () => {
  const [todos, setTodos] = useState([
    { id: 1, text: 'React 공부하기' },
    { id: 2, text: 'Redux 배우기' },
  ]);
  const [count, setCount] = useState(0);

  const memoizedTodos = useMemo(() => todos, [todos]);

  return (
    <div>
      <h1>할 일 목록</h1>
      <TodoList todos={memoizedTodos} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
};

export default App;

 

useMemo를 사용하면 todos 배열이 변경될 때만 TodoList 컴포넌트가 재렌더링되고, count 상태가 변경될 때는 재렌더링되지 않습니다.

 

하지만 useMemo를 사용하는 것은 코드의 복잡성을 증가시키고, 개발자가 직접 최적화해야 할 부분을 추가하는 단점이 있습니다.

 

3. React 컴파일러를 이용한 자동 최적화

 

React 컴파일러는 이러한 수동 최적화 과정을 자동화하여 개발자가 더욱 간편하게 성능을 향상시킬 수 있도록 도와줍니다.

 

컴파일러는 코드를 분석하여 불필요한 렌더링을 감지하고, 자동으로 메모이제이션을 적용합니다.

 

React 컴파일러를 사용하면 useMemo 없이도 동일한 최적화 효과를 얻을 수 있습니다.

 

컴파일러는 todos 배열이 변경되지 않았다는 것을 인지하고, TodoList 컴포넌트를 재렌더링하지 않습니다.

 

이를 통해 개발자는 복잡한 최적화 로직을 작성하지 않고도 성능을 향상시킬 수 있습니다.

 

React 컴파일러는 코드를 분석하여 TodoList 컴포넌트가 todos 배열의 변경에만 의존한다는 것을 파악하고, count 상태 변경 시에는 재렌더링하지 않도록 자동으로 최적화합니다.

 

React 컴파일러가 내놓는 소스 코드는 실제로는 매우 복잡하고, 일반적인 JavaScript 코드와는 다릅니다.

 

컴파일러는 React 코드를 분석하여 최적화된 JavaScript 코드로 변환하는데, 이 과정에서 다양한 기법을 사용하며, 그 결과물은 사람이 직접 읽고 이해하기 어려운 형태를 띄게 됩니다.

 

하지만, 컴파일러가 어떤 방식으로 코드를 변환하고 최적화하는지 이해하는 데 도움이 되도록, 간단한 예제를 통해 컴파일된 코드의 개념적인 변환 과정을 살펴보겠습니다.

 

원본 코드 (App.js):

import React, { useState } from 'react';

const TodoListItem = ({ todo }) => {
  console.log('TodoListItem rendering:', todo.id);
  return <li>{todo.text}</li>;
};

const TodoList = ({ todos }) => {
  console.log('TodoList rendering');
  return (
    <ul>
      {todos.map((todo) => (
        <TodoListItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
};

const App = () => {
  const [todos, setTodos] = useState([
    { id: 1, text: 'React 공부하기' },
    { id: 2, text: 'Redux 배우기' },
  ]);
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>할 일 목록</h1>
      <TodoList todos={todos} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
};

export default App;

 

개념적인 컴파일된 코드 (개략적인 예시):

// ... (React 라이브러리 코드)

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'React 공부하기' },
    { id: 2, text: 'Redux 배우기' },
  ]);
  const [count, setCount] = useState(0);

  // 컴파일러가 생성한 TodoList 컴포넌트의 최적화된 버전
  const _memoizedTodoList = React.memo(TodoList);

  return (
    React.createElement("div", null,
      React.createElement("h1", null, "할 일 목록"),
      // 최적화된 TodoList 컴포넌트를 사용
      React.createElement(_memoizedTodoList, { todos: todos }),
      React.createElement("button", { onClick: () => setCount(count + 1) }, "Count: ", count)
    )
  );
}

// ... (나머지 컴포넌트 및 로직)
  • React.memo(): 컴파일러는 TodoList 컴포넌트를 React.memo()로 감싸서 불필요한 재렌더링을 방지합니다. React.memo()는 컴포넌트의 props가 변경되지 않았을 경우 재렌더링을 건너뛰는 기능을 제공합니다.

 

4. 성능 비교 및 결론

 

React 컴파일러를 사용하면 기존의 useMemo 방식보다 더욱 효율적이고 간편하게 성능을 최적화할 수 있습니다.

 

컴파일러는 자동으로 불필요한 렌더링을 감지하고 메모이제이션을 적용하여 개발자가 직접 최적화 코드를 작성해야 하는 부담을 줄여줍니다.

 

React 컴파일러는 복잡한 애플리케이션에서 더욱 빛을 발합니다.컴포넌트 트리가 복잡하고 데이터가 많을수록 컴파일러의 자동 최적화 기능은 더 큰 성능 향상을 가져다 줍니다.

 

결론적으로, React 컴파일러는 개발자가 더욱 효율적이고 성능이 뛰어난 React 애플리케이션을 구축할 수 있도록 도와주는 강력한 도구입니다.

 

컴파일러의 자동 최적화 기능을 활용하여 불필요한 렌더링을 줄이고, 사용자에게 더욱 빠르고 부드러운 경험을 제공할 수 있습니다.

 

React 컴파일러는 미래 React 개발의 핵심적인 역할을 할 것으로 기대됩니다.

 

앞으로 더욱 발전된 컴파일러 기술을 통해 더욱 효율적이고 성능이 뛰어난 React 애플리케이션을 개발할 수 있을 것입니다.