main-logo

컴포넌트 성능 최적화

Next.js에서 성능을 최적화 하는 법

profile
TY
2023년 06월 04일 · 0 분 소요

들어가며

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

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

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

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

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

React.memo, 데이터 Fetching, Dynamic Import, 그리고 useMemo와 useCallback에 대해 알아보자.

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

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할 때 최적화를 고려해야 하는데, getStaticProps나 getServerSideProps 등을 사용하여 필요한 데이터를 미리 가져오고, useMemo나 useCallback 등을 사용하여 불필요한 리렌더링을 피할 수 있다.
그런데 데이터 Fetching이 필요하다고 해서 무조건적인 getStaticProps나 getServerSideProps의 사용은 오히려 좋지 않을 수 있다.
데이터의 사용 목적이나 화면상에 필요한 데이터가 실시간으로 업데이트가 되어야 하는지에 따라 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 사용하기


useCallback은 useMemo와 비슷한데, 일반적으로 콜백 함수를 메모이제이션(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.memo나 useMemouseCallback등 메모이제이션 기능으로 최적화를 한다고 가정할 때, 오로지 성능 최적화를 위해서만 사용을 해야 한다는 점이다.

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

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

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

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

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

마치며

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

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

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

참고 문서