React 컴포넌트 최적화 기법

React 컴포넌트 최적화 개요

React 컴포넌트 최적화는 성능을 향상시키고 앱의 반응성을 개선하는데 중요한 역할을 합니다. 최적화를 통해 불필요한 렌더링을 방지하고 자원을 효율적으로 활용할 수 있습니다.

렌더링과 최적화

렌더링은 React 앱에서 UI 갱신을 처리하는 과정입니다. 컴포넌트가 렌더링될 때마다 가상 돔(Virtual DOM)을 비교하여 변경된 부분을 갱신합니다. 하지만 모든 렌더링은 비용이 발생하므로 불필요한 렌더링을 최소화하는 것이 중요합니다.

컴포넌트 최적화 방법

React 컴포넌트를 최적화하는 방법은 여러 가지가 있습니다. 다음은 대표적인 방법들입니다.

1. 불필요한 렌더링 방지하기
2. PureComponent와 memo를 이용한 성능 개선
3. 메모이제이션을 이용한 성능 개선
4. 가상화와 이중 렌더링 방식 이해하기
5. React.memo를 이용한 성능 개선
6. React.lazy와 Suspense를 이용한 지연 로딩 처리
7. 리스트와 키(Key)의 중요성 이해하기
8. 이벤트 핸들러 최적화하기
9. useMemo와 useCallback을 이용한 성능 개선


// 예시 코드
import React, { PureComponent } from 'react';

class MyComponent extends PureComponent {
  render() {
    return (
      

컴포넌트 내용

...

); } }

불필요한 렌더링 방지하기

불필요한 렌더링을 방지하여 성능을 향상시킬 수 있습니다. React에서 불필요한 렌더링이 발생하는 경우는 크게 두 가지로 나눌 수 있습니다.

1. Props나 State가 변경되지 않았을 때의 렌더링

shouldComponentUpdate를 사용하여 Props나 State의 변경 여부를 확인하고, 변경이 없을 경우 렌더링을 하지 않도록 할 수 있습니다.


// 예시 코드
import React, { Component } from 'react';

class MyComponent extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.someProp === nextProps.someProp && this.state.someState === nextState.someState) {
      return false;
    }
    return true;
  }

  render() {
    return (
      

컴포넌트 내용

...

); } }

2. 불필요한 자식 컴포넌트의 렌더링

자식 컴포넌트에서도 불필요한 렌더링이 발생할 수 있습니다. 이 경우는 부모 컴포넌트의 렌더링이 발생하지 않았는데도 자식 컴포넌트가 렌더링되는 경우입니다. 이를 방지하기 위해 React.memo를 사용할 수 있습니다.


// 예시 코드
import React, { memo } from 'react';

const MyComponent = memo(({ prop1, prop2 }) => {
  return (
    

컴포넌트 내용

...

); });

PureComponent와 memo를 이용한 성능 개선

React에서는 PureComponent와 memo를 사용하여 성능을 개선할 수 있습니다. 이들은 얕은(shallow) 비교를 통해 불필요한 렌더링을 방지하는 역할을 합니다.

PureComponent

PureComponent는 React.Component를 상속받은 클래스입니다. PureComponent를 사용하면 shouldComponentUpdate를 구현하지 않아도 Props나 State의 얕은 비교를 자동으로 수행하여 변경된 값이 있는 경우에만 렌더링이 발생합니다.


// 예시 코드
import React, { PureComponent } from 'react';

class MyComponent extends PureComponent {
  render() {
    return (
      

컴포넌트 내용

...

); } }

memo

memo는 함수형 컴포넌트를 감싸는 고차 컴포넌트입니다. memo를 사용하면 이전에 렌더링된 결과를 기억하여 Props가 변경되지 않았을 경우에만 렌더링을 수행합니다.


// 예시 코드
import React, { memo } from 'react';

const MyComponent = memo(({ prop1, prop2 }) => {
  return (
    

컴포넌트 내용

...

); });

메모이제이션을 이용한 성능 개선

메모이제이션을 이용하여 함수의 결과를 저장하고 재사용함으로써 성능을 개선할 수 있습니다. React에서는 useCallback과 useMemo를 사용하여 메모이제이션을 구현할 수 있습니다.

useCallback

useCallback은 함수를 기억하고 재사용할 수 있게 해줍니다. 의존성 배열을 인자로 받아 변경되지 않으면 이전에 생성된 함수를 반환합니다.


// 예시 코드
import React, { useCallback } from 'react';

const MyComponent = () => {
  const handleClick = useCallback(() => {
    // 버튼 클릭 시 동작
  }, []);

  return (
    
  );
};

useMemo

useMemo는 계산된 값을 기억하고 재사용할 수 있게 해줍니다. 의존성 배열을 인자로 받아 변경되지 않으면 이전에 생성된 값을 반환합니다.


// 예시 코드
import React, { useMemo } from 'react';

const MyComponent = ({ prop1, prop2 }) => {
  const calculatedValue = useMemo(() => {
    // 계산된 값
    return prop1 + prop2;
  }, [prop1, prop2]);

  return (
    

{calculatedValue}

); };

가상화와 이중 렌더링 방식 이해하기

가상화(Virtualization)는 대규모 데이터 목록을 렌더링할 때 성능을 개선하기 위한 방법입니다. 이중 렌더링(Double Rendering)은 가상화를 구현하기 위한 방식 중 하나입니다.

가상화

가상화는 큰 데이터 목록을 작은 단위로 나누어 화면에 보여주는 방식입니다. 화면에 보이는 부분만 렌더링하고 나머지는 가상화된 상태로 유지하여 불필요한 렌더링을 최소화합니다.

예시로 React에서는 react-virtualized나 react-window와 같은 라이브러리를 사용하여 가상화를 구현할 수 있습니다. 이러한 라이브러리는 스크롤 가능한 리스트나 테이블과 같은 컴포넌트들을 최적화된 방식으로 렌더링합니다.

이중 렌더링

이중 렌더링은 효율적인 가상화 구현을 위해 사용되는 방식 중 하나입니다. 이 방식은 두 개의 컨테이너를 사용하여 화면에 표시할 컨텐츠와 실제로 렌더링되는 컨텐츠를 분리합니다.

첫 번째 컨테이너인 가상화 컨테이너는 화면에 보이는 내용을 가상화하여 빠르게 스크롤 및 검색 기능을 제공합니다. 이 컨테이너는 실제 DOM 요소를 가지지 않고 가상화된 상태로 유지됩니다.

두 번째 컨테이너인 렌더링 컨테이너는 화면에 표시되어야 할 실제 컨텐츠를 렌더링합니다. 이 컨테이너는 화면에 보이는 영역에 해당하는 데이터만을 불러와서 렌더링하므로 효율적인 렌더링이 가능합니다.

이중 렌더링은 사용자가 스크롤하여 가상화 컨테이너에서 보고자 하는 컨텐츠를 선택하면 해당 컨텐츠가 렌더링 컨테이너로 이동하여 화면에 표시됩니다. 이 과정에서 가상화 컨테이너와 렌더링 컨테이너의 데이터가 동기화되어 화면에 빠르게 업데이트됩니다.

이중 렌더링은 react-window 라이브러리와 같은 가상화 라이브러리에서 사용되는 방식입니다.


React.memo를 이용한 성능 개선

React.memo는 함수형 컴포넌트의 성능을 개선하기 위해 사용되는 React의 기능입니다. 이를 사용하면 컴포넌트의 프로퍼티(props)가 변경되지 않는 한 이전에 렌더링된 결과를 재사용할 수 있습니다.

React.memo는 컴포넌트의 출력 결과를 메모이제이션하여 동일한 프로퍼티가 주어졌을 때 이전에 렌더링한 결과를 재사용합니다. 이를 통해 불필요한 렌더링을 피하고 성능을 향상시킬 수 있습니다.

예시 코드


// 예시 함수형 컴포넌트
import React from 'react';

const MyComponent = React.memo(({ prop1, prop2 }) => {
  // 컴포넌트의 렌더링 코드
  return (
    

{prop1}

{prop2}

); });

위의 예시 코드에서 MyComponent는 React.memo로 감싸져 있습니다. 이로 인해 MyComponent의 prop1과 prop2가 이전에 렌더링된 결과와 동일하다면 이전 결과를 재사용하고, 동일하지 않다면 컴포넌트를 다시 렌더링합니다.

React.memo를 사용하여 컴포넌트를 메모이제이션하면, 상위 컴포넌트가 리렌더링되어도 MyComponent는 프로퍼티가 변경되지 않을 경우에만 이전 결과를 재사용하여 성능을 개선합니다.


React.lazy와 Suspense를 이용한 지연 로딩 처리

React.lazy와 Suspense는 React 16.6 이후에 도입된 기능으로, 지연 로딩 처리를 쉽게 구현할 수 있게 해줍니다. 이를 사용하면 필요한 컴포넌트가 실제로 필요할 때까지 불러오지 않고, 필요한 시점에서 비동기적으로 로딩할 수 있습니다.

예시 코드


// 예시로 사용할 다이나믹 임포트 함수
const OtherComponent = React.lazy(() => import('./OtherComponent'));

// 예시 컴포넌트
function MyComponent() {
  return (
    
Loading...
}>
); }

위의 예시 코드에서는 React.lazy를 사용하여 OtherComponent를 다이나믹하게 임포트하고 있습니다. 이로 인해 OtherComponent는 필요한 시점에서 비동기적으로 로딩됩니다.

MyComponent 컴포넌트 내에서 Suspense 컴포넌트를 사용하여 로딩 중에 보여줄 미리 지정된 fallback 컴포넌트를 설정할 수 있습니다. fallback 컴포넌트는 OtherComponent가 로딩되는 동안 사용자에게 보여집니다.

React.lazy와 Suspense는 동적인 컴포넌트를 사용하는 코드에서 특히 유용합니다. 필요한 컴포넌트가 초기 렌더링 시점에 즉시 필요하지 않은 경우에는 React.lazy와 Suspense를 사용하여 필요한 컴포넌트를 지연해서 로딩할 수 있습니다.


리스트와 키(Key)의 중요성 이해하기

리액트에서 리스트를 렌더링할 때는 각 리스트 아이템에 특정한 키(Key)를 할당해야 합니다. 이는 리액트의 재조정 알고리즘이 동작하는 데 중요한 역할을 합니다.

리스트의 각 아이템에 고유한 키(Key)를 할당하는 것은 리액트가 리렌더링 시에 각 아이템을 식별하는 데 도움을 줍니다. 키는 고유해야 하며, 리스트의 순서를 변경하거나 아이템을 추가, 제거할 때 리액트가 올바르게 업데이트할 수 있도록 도와줍니다.

예시 코드


// 예시 리스트 컴포넌트
function MyList() {
  const items = [1, 2, 3, 4, 5];

  return (
    
    {items.map((item) => (
  • {item}
  • ))}
); }

위의 예시 코드에서는 MyList 컴포넌트가 리스트 아이템을 렌더링하고 있습니다. 각 아이템에는 item 값이 키(Key)로 할당되어 있습니다.

키(Key)를 할당함으로써 리액트는 각 아이템을 고유하게 식별하고, 리스트의 순서 변경이나 아이템 추가, 제거 시에 올바른 변경 사항을 적용할 수 있습니다. 이를 통해 효율적인 리렌더링과 업데이트를 수행할 수 있습니다.

리스트의 아이템에 키(Key)를 제공하지 않으면 리액트는 경고 메시지를 발생시키고 기본적인 재조정 알고리즘을 사용하여 리렌더링합니다. 하지만 키(Key)를 제대로 설정하지 않으면 예기치 않은 결과가 발생할 수 있으므로 항상 키(Key)를 할당하는 것이 중요합니다.


이벤트 핸들러 최적화하기

리액트에서 이벤트 핸들러를 최적화하는 것은 성능 향상에 중요한 요소입니다. 이벤트 핸들러의 최적화는 불필요한 리렌더링을 방지하여 애플리케이션의 반응성을 개선할 수 있습니다.

예시 코드


function MyComponent() {
  const handleClick = React.useCallback(() => {
    // 이벤트 핸들러의 로직
    console.log("Button clicked!");
  }, []);

  return (
    
  );
}

위의 예시 코드에서는 MyComponent 컴포넌트 내에 handleClick이라는 이벤트 핸들러를 사용하고 있습니다. 이벤트 핸들러의 최적화를 위해 React의 useCallback 훅을 사용하여 핸들러 함수를 메모이제이션합니다.

useCallback은 종속성 배열을 입력받아 그 배열의 값이 변경되지 않는 한 핸들러 함수를 재생성하지 않습니다. 이렇게 함으로써 리렌더링 시에도 핸들러 함수의 새 인스턴스를 생성하지 않고 기존의 함수를 재사용할 수 있습니다.

이벤트 핸들러의 최적화는 특히 이벤트 핸들러를 자식 컴포넌트로 전달할 때 유용합니다. 이를 통해 자식 컴포넌트가 의도치 않게 재랜더링되는 것을 방지하고 전체 애플리케이션의 성능을 향상시킬 수 있습니다.

하지만 모든 이벤트 핸들러에 useCallback을 사용할 필요는 없으며, 핸들러 함수가 자주 변경되는 경우에만 사용하는 것이 좋습니다. 핸들러 함수의 종속성을 명시적으로 지정하여 필요한 경우에만 최적화를 수행하는 것이 좋습니다.


useMemo와 useCallback을 이용한 성능 개선

리액트에서는 useMemo와 useCallback 훅을 사용하여 성능을 개선할 수 있습니다. 이들 훅은 작업의 결과를 메모이제이션하고 종속성이 변경되지 않을 때 다시 계산하는 방식으로 동작합니다.

useMemo

useMemo는 계산 비용이 높은 작업의 결과를 캐싱하여 재계산을 방지합니다. 종속성 배열에서 변동이 없는 한 이전에 계산된 값을 재사용합니다.

예를 들어, 배열의 합을 계산하는 작업을 useMemo를 사용하여 최적화할 수 있습니다.


const sum = useMemo(() => {
  let result = 0;
  for (let i = 0; i < array.length; i++) {
    result += array[i];
  }
  return result;
}, [array]);

위의 예시 코드에서는 배열의 합을 계산하는 작업을 useMemo로 감싸고 있습니다. 배열이 변경되지 않는 한, sum 변수는 이전에 계산된 값을 재사용합니다.

useCallback

useCallback은 핸들러 함수 등을 메모이제이션하여 동일한 함수 인스턴스를 재사용합니다. 종속성 배열이 변경되지 않는 한 이전에 생성된 함수를 재사용합니다.

예를 들어, 클릭 이벤트 핸들러를 useCallback을 사용하여 최적화할 수 있습니다.


const handleClick = useCallback(() => {
  // 클릭 이벤트 핸들러의 로직
  console.log("Button clicked!");
}, []);

위의 예시 코드에서는 클릭 이벤트 핸들러 handleClick을 useCallback으로 감싸고 있습니다. 종속성 배열이 비어있기 때문에 핸들러 함수는 변하지 않고 이전의 함수 인스턴스를 재사용합니다.

useMemo와 useCallback은 비용이 높은 작업이나 자주 바뀌지 않는 핸들러 함수를 최적화하는 데 유용합니다. 그러나 모든 상황에서 사용하는 것은 필요하지 않으며, 결과 값이나 함수 인스턴스를 재사용하기가 실제로 성능을 향상시킬 수 있는 경우에만 사용하는 것이 좋습니다.


Leave a Comment