컴포넌트 성능 최적화

2023. 7. 17. 07:50UX Engineer 이야기
taeyong.kim

들어가며

최근 오픈한 서비스들에 대해 성능이슈가 꾸준하게 제기되고 있는 상태입니다.
물론 서비스 구축을 할 때에 이런 부분들을 신경 써서 진행을 하였다면 문제가 되지 않겠으나 대한민국 프로젝트 특성상 이런 부분까지 챙겨가며 서비스 구축을 하는 건 쉽지 않습니다.

더불어 성능적인 부분은 개발영역만이 아닌 UI와 GUI에서도 신경써주고, 관심을 가져가며 함께해야 하는 부분이지만 촉박한 일정과 계속해서 살아 숨 쉬는 프로젝트의 상황상 쉽지 않은 게 사실입니다.

그렇다고 우리마저 이 부분을 제쳐둔다면 성능 관련 이슈적인 수준이 아닌 서비스 자체에 문제가 될 수 있으니 작업 진행을 하면서 최대한 신경써주면 좋지 않을까 싶습니다.

그래서 본론으로 들어가면 React와 Next.js는 성능과 사용자 경험을 개선하기 위한 다양한 최적화 기능을 제공합니다.

컴포넌트 최적화는 애플리케이션의 성능 향상과 메모리 사용량 감소에 중요한 역할을 하는데, 이 글에서는 React와 Next.js에서 컴포넌트 최적화 방법을 살펴보고자 합니다.

React.memo, 데이터 Fetching, Dynamic Import, 그리고 useMemouseCallback에 대해 알아보려고 합니다.

이러한 최적화 기법을 사용하면 애플리케이션의 성능을 향상시키고, 사용자 경험을 최적화할 수 있습니다.

React.memo 사용하기


React.memo는 컴포넌트의 불필요한 리렌더링을 방지하여 성능을 향상시키는 데 도움을 주는 React의 기능입니다.
React.memo를 사용하면 컴포넌트의 속성(prop)이 변경되지 않으면 이전에 렌더링 된 결과를 재사용하게 됩니다.
이를 통해 불필요한 리렌더링을 방지하여 컴포넌트의 성능을 최적화할 수 있습니다.

그러므로 불필요한 props 변경으로 인해 리렌더링되는 컴포넌트가 있다면, memo를 사용하여 해당 컴포넌트를 감싸주어 이런 불필요한 props 변경으로 인한 리렌더링을 방지할 수 있습니다.

import React from 'react';

const MyComponent = React.memo(({ propA, propB }) => {
  // 컴포넌트 로직 및 렌더링
});

export default MyComponent;

데이터 Fetching 최적화하기


Next.js는 데이터 Fetching에 매우 강력한 기능을 제공합니다.
하지만 데이터를 Fetch할 때 최적화를 고려해야 하는데, getStaticPropsgetServerSideProps 등을 사용하여 필요한 데이터를 미리 가져오고, useMemouseCallback 등을 사용하여 불필요한 리렌더링을 피할 수 있습니다.
그런데 데이터 Fetching이 필요하다고 해서 무조건적인 getStaticPropsgetServerSideProps의 사용은 오히려 좋지 않을 수 있습니다.
데이터의 사용 목적이나 화면상에 필요한 데이터가 실시간으로 업데이트가 되어야 하는지에 따라 SSR이 아닌 CSR방식의 Fetching이 필요할 수 있기 때문입니다.

데이터 Fetching에 대한 내용과 최적화 부분은 이전에 작성한 글을 참고하면 좋습니다.

Dynamic Import 사용하기


Dynamic Import는 Next.js에서 제공하는 기능으로, 페이지나 컴포넌트를 필요한 시점에 동적으로 로드하는 방법입니다.
이를 통해 초기 번들 크기를 줄이고, 필요한 컴포넌트만 로드하여 애플리케이션의 성능을 향상시킬 수 있습니다.

import dynamic from 'next/dynamic';
import { useState } from 'react';

// MyComponent를 동적으로 불러오는 DynamicComponent 생성
const DynamicComponent = dynamic(() => import('../components/MyComponent'), {
  loading: () => <div>Loading...</div>,
});

const HomePage = () => {
  // 컴포넌트의 렌더링 여부를 관리하는 상태 변수
  const [showComponent, setShowComponent] = useState(false);

  // 버튼 클릭 시 컴포넌트 보여주기
  const handleClick = () => {
    setShowComponent(true);
  };

  return (
    <div>
      <h1>Home Page</h1>
      {/* 버튼을 클릭하면 handleClick 함수 실행 */}
      <button onClick={handleClick}>동적 컴포넌트 보기</button>
      {/* showComponent 값이 true일 때만 DynamicComponent 렌더링 */}
      {showComponent && <DynamicComponent />}
    </div>
  );
};

export default HomePage;

위처럼 초기 화면에 사용하지 않는 컴포넌트를 필요한 시점에 로드하게끔 해주어 초기 로딩 속도 향상에 도움이 됩니다.
여기서 주의해야 할 부분은 lazy와는 다르다는 점으로 Dynamic Import를 사용하여 컴포넌트를 필요한 시점에 로드하려면, 컴포넌트를 특정 이벤트에 연결하거나 조건부로 렌더링해야 합니다.

useMemo 사용하기


useMemo를 사용하면 계산 비용이 큰 연산이나 함수 호출의 결과를 기억하고, 의존성이 변경되지 않으면 이전 결과를 재사용할 수 있어 이를 통해 불필요한 연산을 방지하고 성능을 향상시킬 수 있습니다.

useMemo를 사용하여 값을 기억할 때, 함수의 결과 또는 연산된 값을 기억하게 됩니다.
이전에 계산된 값이 필요한 상황에서는 이전 결과를 재사용하여 다시 계산하지 않아 특히 렌더링 동안 반복적으로 호출되는 연산이나 함수에 유용합니다.

import React, { useMemo } from 'react';

const MyComponent = ({ data }) => {
  // data가 변경되지 않는 한 computedData 값을 기억.
  const computedData = useMemo(() => {
    // 계산 비용이 큰 연산을 수행하거나 함수를 호출.
    return someExpensiveComputation(data);
  }, [data]); // data가 변경될 때에만 재계산.

  return (
    <div>
      <p>Computed Data: {computedData}</p>
      {/* ... */}
    </div>
  );
};

export default MyComponent;

주의해야 할 점은 useMemo의 의존성 배열을 제대로 설정해야 한다는 것이죠.
의존성 배열에 포함된 값이 변경될 때에만 useMemo 내의 콜백 함수가 실행되고 값이 재계산되므로 의존성 배열을 제대로 관리하지 않으면 원하는 대로 값이 갱신되지 않을 수 있습니다.

useCallback 사용하기


useCallbackuseMemo와 비슷한데, 일반적으로 콜백 함수를 메모이제이션(memoization)하여 불필요한 함수 생성을 방지하고 성능을 향상시킬 수 있도록 도와줍니다.

import React, { useCallback } from 'react';

const MyComponent = ({ onClick }) => {
  // onClick 함수를 메모이제이션하여 변경되지 않는 한 재사용.
  const handleClick = useCallback(() => {
    // 클릭 이벤트 처리 로직
    // ...
    onClick(); // 부모 컴포넌트로부터 전달받은 onClick 함수 호출
  }, [onClick]); // onClick이 변경될 때에만 함수를 새로 생성.

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      {/* ... */}
    </div>
  );
};

export default MyComponent;

주의해야 할 점은 useMemo와 마찬가지로 useCallback의 의존성 배열을 제대로 설정해야 합니다.
의존성 배열에 포함된 값이 변경될 때에만 useCallback 내의 콜백 함수가 생성되며, 의존성 배열을 제대로 관리하지 않으면 원하는 대로 함수가 생성되지 않거나, 의도하지 않은 동작이 발생할 수 있습니다.

주의해야 할 점


위에서 언급한 내용중 React.memouseMemo, useCallback등 메모이제이션 기능으로 최적화를 한다고 가정할 때, 오로지 성능 최적화를 위해서만 사용을 해야 한다는 점입니다.

이유는 이러한 최적화 메커니즘은 추가적인 계산이나 메모리 사용을 필요로 하므로 불필요한 부분에도 적용되면 오히려 성능 저하의 원인이 됩니다.

게다가 이런 방법을 사용하면 코드가 더 복잡해질 수 있어 추가적인 추적이나 관리가 필요하며, 잘못 사용하면 버그를 유발할 수도 있습니다.

또, 작은 규모의 간단한 컴포넌트에서는 이러한 최적화 기법을 사용할 필요가 없는데, 이유는 React 자체적으로 효율적인 렌더링을 수행하므로, 불필요한 최적화는 오히려 코드를 복잡하게 만들 수 있기 때문입니다.

따라서, React.memo, useMemo, useCallback을 사용하기 전에 실제로 최적화가 필요한지 판단해야 합니다.
컴포넌트가 성능 이슈를 겪거나, 계산 비용이 높은 작업이 있는 경우에 사용하는 것이 좋습니다.

항상 사용하기보다는 필요한 경우에만 사용하고, 테스트와 성능 측정을 통해 실제로 성능 향상을 확인하는 것이 좋습니다.

마치며

Next.js는 성능과 사용자 경험을 개선하기 위한 다양한 컴포넌트 최적화 방법을 제공하고 있습니다.

React.memo를 사용하여 컴포넌트의 리렌더링을 방지하고, 데이터 Fetching을 최적화하여 효율적으로 데이터를 로드하고, Dynamic Import를 활용하여 필요한 컴포넌트만 로드하며, useMemouseCallback을 사용하여 불필요한 연산을 방지할 수 있습니다.

이러한 최적화 기법을 적절하게 활용하여 Next.js 애플리케이션의 성능을 향상시켜보면 앞으로 성능이 느리다는 말을 조금이라도 덜 들을 수 있지 않을까 싶습니다.

 

이 글은 pxd XE Group Blog에서도 보실 수 있습니다.

참고 문서