main-logo

React 앱이 느리다고요?

react-scan으로 성능 문제 해결하기

profile
doworld
2025년 03월 02일 · 0 분 소요

들어가기

안녕하세요.

React 애플리케이션의 성능을 개선하는 방법을 쉽게 이해할 수 있는 예제를 통해 알아보겠습니다.

특히 react-scan이라는 도구를 사용해서 어떤 부분이 성능을 저하시키는지 쉽게 찾아볼 수 있습니다.

 

react-scan 소개

react-scan은 React 애플리케이션의 성능을 실시간으로 모니터링하고 최적화할 수 있게 도와주는 개발자 도구입니다.
이 도구를 통해 우리는
  • 불필요한 렌더링 발생 지점 식별
  • 컴포넌트 렌더링 횟수 모니터링
  • 성능 병목 현상 시각화
  • 최적화 포인트 발견

등을 할 수 있습니다.

 

예제 소개

사용자 목록을 보여주는 간단한 애플리케이션을 만들어볼 건데요, 조금 억지스럽지만..

같은 기능을 구현하는 두 가지 다른 방법을 비교해 볼 거예요

  • 일부러 성능이 안 좋게 만든 버전 (나쁜 예시)
  • 성능을 개선한 버전 (좋은 예시)

 

성능이 안 좋은 버전은 어떻게 만들었나요?

먼저 성능이 안 좋은 버전을 보여드릴게요. 이 버전에서는 일부러 이런 문제들을 만들었습니다

// 비효율적인 구현 예시
// 각 문자를 개별 컴포넌트로 만들어서 렌더링하는 안 좋은 방식
const SlowCharacter: React.FC<{ char: string }> = ({ char }) => {
  useEffect(() => {
    // 일부러 성능 저하를 시뮬레이션하기 위한 인위적인 지연
    // 실제 프로젝트에서는 절대 이렇게 하면 안 됩니다!
    const start = performance.now();
    while (performance.now() - start < 1) {}
  }, [char]);

  return <span>{char}</span>;
};

// 이름을 한 글자씩 쪼개서 렌더링하는 비효율적인 컴포넌트
// 예: "John Doe" => <span>J</span><span>o</span><span>h</span><span>n</span>...
const FullName: React.FC<{ firstName: string; lastName: string }> = ({
  firstName,
  lastName,
}) => {
  const fullName = `${firstName} ${lastName}`;

  return (
    <div className="full-name">
      {/* 
        문자열을 배열로 쪼개고 각 문자마다 새로운 컴포넌트를 생성
        이는 매우 비효율적인 방식입니다:
        1. 불필요한 컴포넌트 생성
        2. 과도한 메모리 사용
        3. 성능 저하
      */}
      {fullName.split("").map((char, index) => (
        <SlowCharacter key={index} char={char} />
      ))}
    </div>
  );
};

이게 왜 안 좋을까요?

  • 이름의 각 글자마다 새로운 컴포넌트를 만듭니다 (너무 과하죠?)
  • 각 글자마다 일부러 지연 시간을 넣었어요
  • 불필요하게 여러 번 다시 그려집니다

 

개선된 버전은 어떻게 다른가요?

이제 같은 기능을 하지만, 훨씬 더 효율적으로 만든 버전을 보여드릴게요:

// React.memo를 사용하여 불필요한 리렌더링 방지
// props가 변경되지 않으면 다시 렌더링하지 않음
const FullName = memo(
  ({ firstName, lastName }: { firstName: string; lastName: string }) => (
    <div className="full-name">
      {/* 이름을 쪼개지 않고 한 번에 렌더링 */}
      {firstName} {lastName}
    </div>
  )
);

// 사용자 카드 컴포넌트도 memo로 최적화
// 각 사용자의 정보가 변경될 때만 리렌더링
const UserCard = memo(
  ({
    firstName,
    lastName,
    email,
  }: {
    firstName: string;
    lastName: string;
    email: string;
  }) => (
    <div className="user-card">
      {/* 컴포넌트를 논리적인 단위로 분리 */}
      <FullName firstName={firstName} lastName={lastName} />
      <div className="email">{email}</div>
    </div>
  )
);

이 버전이 더 좋은 이유는

  • 이름을 쪼개지 않고 한 번에 보여줍니다
  • React.memo를 사용해서 필요할 때만 다시 그립니다
  • 불필요한 작업이 없어서 훨씬 빠릅니다

 

react-scan으로 차이 확인하기

이제 react-scan을 사용해서 두 버전의 차이를 실제로 확인해 볼 수 있습니다

import { scan } from "react-scan";

function App() {
  scan(); // 함수로 호출

  return (
    <div className="app">
      <h1>React Scan 성능 테스트</h1>

      <div className="comparison">
        <section className="inefficient">
          <h2>비효율적인 구현</h2>
          <InefficientList />
        </section>

        <section className="efficient">
          <h2>최적화된 구현</h2>
          <OptimizedUserList />
        </section>
      </div>
    </div>
  );
}

react-scan을 사용하면:

  • 어떤 컴포넌트가 자주 다시 그려지는지
  • 어떤 부분이 느린지
  • 어떻게 하면 더 빠르게 만들 수 있는지

쉽게 확인할 수 있습니다.

 

에서 확인하실 수 있습니다.

 

마치며

간단한 예제이지만 우리는 세 가지의 이점을 누릴 수 있는 것 같아요.
 
첫째, 명확한 성능 진단을 할 수 있습니다. 더 이상 성능 문제를 추측하지 않아도 됩니다. 실시간으로 어떤 컴포넌트가 불필요하게 다시 렌더링 되는지, 어떤 부분이 병목 현상을 일으키는지 정확히 보여줍니다.
 
둘째, 데이터 기반의 의사결정이 가능해집니다. "이 부분이 느린 것 같아요"라는 주관적인 판단이 아닌, 실제 측정된 데이터를 기반으로 최적화 결정을 내릴 수 있습니다. 이는 팀 내 커뮤니케이션을 더욱 효과적으로 만들어줍니다.
 
셋째, 선제적인 문제 해결이 가능합니다. 성능 문제가 사용자에게 영향을 미치기 전에 미리 발견하고 해결할 수 있어, 일관된 사용자 경험을 제공할 수 있습니다.
 
프런트엔드 개발자로서 우리의 궁극적인 목표는 사용자에게 더 나은 경험을 제공하는 것입니다. 
성능 최적화는 단순히 기술적인 과제가 아닌, 사용자 경험을 향상시키는 중요한 요소입니다. 이러한 목표를 달성하는데 react-scan과 같은 도구들이 유용하게 사용될 수 있는 것 같습니다.
감사합니다.