Frontend

React 성능 최적화 완전 가이드

React 애플리케이션의 성능을 최적화하는 다양한 기법들을 실전 예제와 함께 소개합니다. 메모이제이션, 코드 스플리팅, 가상화 등 핵심 최적화 전략을 다룹니다.

React 성능 최적화 완전 가이드

소개

React 앱이 커질수록 “클릭했는데 반응이 늦다”는 피드백이 나오기 쉽습니다. 우리도 대시보드·이벤트 페이지를 만들면서 리렌더링·번들 크기 이슈를 겪었고, 그때 적용한 메모이제이션·코드 스플리팅·가상화 같은 기법들을 실전 예제 중심으로 정리했습니다.

React.memo를 활용한 컴포넌트 메모이제이션

React.memo는 props가 변경되지 않았을 때 컴포넌트의 리렌더링을 방지합니다.

기본 사용법

typescript
1import { memo } from 'react';
2
3interface UserCardProps {
4  name: string;
5  email: string;
6}
7
8const UserCard = memo(({ name, email }: UserCardProps) => {
9  console.log('UserCard rendered');
10  return (
11    <div>
12      <h3>{name}</h3>
13      <p>{email}</p>
14    </div>
15  );
16});
17
18UserCard.displayName = 'UserCard';

커스텀 비교 함수

typescript
1const UserCard = memo(
2  ({ name, email }: UserCardProps) => {
3    return (
4      <div>
5        <h3>{name}</h3>
6        <p>{email}</p>
7      </div>
8    );
9  },
10  (prevProps, nextProps) => {
11    // true를 반환하면 리렌더링을 건너뜀
12    return prevProps.name === nextProps.name && 
13           prevProps.email === nextProps.email;
14  }
15);

useMemo와 useCallback

useMemo로 값 메모이제이션

typescript
1import { useMemo } from 'react';
2
3function ExpensiveComponent({ items }: { items: Item[] }) {
4  const sortedItems = useMemo(() => {
5    console.log('Sorting items...');
6    return items.sort((a, b) => a.price - b.price);
7  }, [items]);
8
9  return (
10    <ul>
11      {sortedItems.map(item => (
12        <li key={item.id}>{item.name}</li>
13      ))}
14    </ul>
15  );
16}

useCallback으로 함수 메모이제이션

typescript
1import { useCallback, useState } from 'react';
2
3function ParentComponent() {
4  const [count, setCount] = useState(0);
5  const [items, setItems] = useState<string[]>([]);
6
7  const handleAddItem = useCallback((item: string) => {
8    setItems(prev => [...prev, item]);
9  }, []); // 의존성 배열이 비어있으므로 함수는 한 번만 생성됨
10
11  return (
12    <div>
13      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
14      <ChildComponent onAddItem={handleAddItem} />
15    </div>
16  );
17}
18
19const ChildComponent = memo(({ onAddItem }: { onAddItem: (item: string) => void }) => {
20  // handleAddItem이 메모이제이션되어 있으므로 불필요한 리렌더링 방지
21  return <button onClick={() => onAddItem('New Item')}>Add Item</button>;
22});

코드 스플리팅 (Code Splitting)

React.lazy와 Suspense

typescript
1import { lazy, Suspense } from 'react';
2
3const LazyComponent = lazy(() => import('./LazyComponent'));
4
5function App() {
6  return (
7    <Suspense fallback={<div>Loading...</div>}>
8      <LazyComponent />
9    </Suspense>
10  );
11}

라우트 기반 코드 스플리팅

typescript
1import { lazy, Suspense } from 'react';
2import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
4const Home = lazy(() => import('./pages/Home'));
5const About = lazy(() => import('./pages/About'));
6const Contact = lazy(() => import('./pages/Contact'));
7
8function App() {
9  return (
10    <BrowserRouter>
11      <Suspense fallback={<div>Loading...</div>}>
12        <Routes>
13          <Route path="/" element={<Home />} />
14          <Route path="/about" element={<About />} />
15          <Route path="/contact" element={<Contact />} />
16        </Routes>
17      </Suspense>
18    </BrowserRouter>
19  );
20}

가상화 (Virtualization)

대량의 리스트를 렌더링할 때는 가상화를 사용하여 성능을 크게 개선할 수 있습니다.

react-window 사용 예제

typescript
1import { FixedSizeList } from 'react-window';
2
3const Row = ({ index, style, data }: any) => (
4  <div style={style}>
5    {data[index].name}
6  </div>
7);
8
9function VirtualizedList({ items }: { items: Item[] }) {
10  return (
11    <FixedSizeList
12      height={600}
13      itemCount={items.length}
14      itemSize={50}
15      itemData={items}
16      width="100%"
17    >
18      {Row}
19    </FixedSizeList>
20  );
21}

불필요한 리렌더링 방지

Context 최적화

typescript
1import { createContext, useContext, useState, useMemo } from 'react';
2
3interface AppContextType {
4  user: User;
5  theme: string;
6}
7
8const AppContext = createContext<AppContextType | null>(null);
9
10function AppProvider({ children }: { children: React.ReactNode }) {
11  const [user, setUser] = useState<User>({ name: 'John', id: 1 });
12  const [theme, setTheme] = useState('dark');
13
14  const value = useMemo(
15    () => ({ user, theme }),
16    [user, theme]
17  );
18
19  return (
20    <AppContext.Provider value={value}>
21      {children}
22    </AppContext.Provider>
23  );
24}
25
26// Context를 여러 개로 분리
27const UserContext = createContext<User | null>(null);
28const ThemeContext = createContext<string>('dark');
29
30// 이렇게 하면 theme만 변경되어도 user를 사용하는 컴포넌트는 리렌더링되지 않음

이미지 최적화

Next.js Image 컴포넌트 활용

typescript
1import Image from 'next/image';
2
3function OptimizedImage() {
4  return (
5    <Image
6      src="/large-image.jpg"
7      alt="Description"
8      width={800}
9      height={600}
10      priority // 중요한 이미지에 사용
11      placeholder="blur" // 블러 플레이스홀더
12      blurDataURL="data:image/jpeg;base64,..."
13    />
14  );
15}

지연 로딩 (Lazy Loading)

typescript
1import { useState, useEffect, useRef } from 'react';
2
3function LazyImage({ src, alt }: { src: string; alt: string }) {
4  const [isLoaded, setIsLoaded] = useState(false);
5  const imgRef = useRef<HTMLImageElement>(null);
6
7  useEffect(() => {
8    const observer = new IntersectionObserver(
9      ([entry]) => {
10        if (entry.isIntersecting) {
11          setIsLoaded(true);
12          observer.disconnect();
13        }
14      },
15      { threshold: 0.1 }
16    );
17
18    if (imgRef.current) {
19      observer.observe(imgRef.current);
20    }
21
22    return () => observer.disconnect();
23  }, []);
24
25  return (
26    <div ref={imgRef}>
27      {isLoaded ? (
28        <img src={src} alt={alt} />
29      ) : (
30        <div className="placeholder">Loading...</div>
31      )}
32    </div>
33  );
34}

번들 크기 최적화

Tree Shaking

typescript
1// 나쁜 예: 전체 라이브러리 import
2import _ from 'lodash';
3
4// 좋은 예: 필요한 함수만 import
5import debounce from 'lodash/debounce';

동적 import

typescript
1async function loadHeavyLibrary() {
2  const { heavyFunction } = await import('./heavy-library');
3  return heavyFunction;
4}
5
6// 필요할 때만 로드
7button.onclick = async () => {
8  const func = await loadHeavyLibrary();
9  func();
10};

성능 측정 및 프로파일링

React DevTools Profiler

typescript
1import { Profiler } from 'react';
2
3function onRenderCallback(
4  id: string,
5  phase: 'mount' | 'update',
6  actualDuration: number,
7  baseDuration: number,
8  startTime: number,
9  commitTime: number
10) {
11  console.log('Component:', id);
12  console.log('Phase:', phase);
13  console.log('Actual duration:', actualDuration);
14  console.log('Base duration:', baseDuration);
15}
16
17function App() {
18  return (
19    <Profiler id="App" onRender={onRenderCallback}>
20      <YourComponent />
21    </Profiler>
22  );
23}

Web Vitals 측정

typescript
1import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
2
3function sendToAnalytics(metric: any) {
4  // 성능 메트릭 전송
5  console.log(metric);
6}
7
8getCLS(sendToAnalytics);
9getFID(sendToAnalytics);
10getFCP(sendToAnalytics);
11getLCP(sendToAnalytics);
12getTTFB(sendToAnalytics);

실전 최적화 체크리스트

  1. 컴포넌트 최적화

    • React.memo로 불필요한 리렌더링 방지
    • useMemo와 useCallback 적절히 활용
    • 큰 컴포넌트를 작은 컴포넌트로 분리
  2. 코드 스플리팅

    • 라우트별 코드 스플리팅
    • 큰 라이브러리 동적 import
    • 조건부 렌더링 컴포넌트 lazy loading
  3. 리스트 최적화

    • 가상화 적용 (100개 이상 아이템)
    • 키 값 최적화
    • 인라인 함수/객체 생성 최소화
  4. 이미지 최적화

    • 적절한 이미지 포맷 사용 (WebP, AVIF)
    • 이미지 지연 로딩
    • 반응형 이미지 사용
  5. 번들 최적화

    • Tree shaking 확인
    • 중복 의존성 제거
    • 번들 크기 분석

결론

React 성능 최적화는 다음 원칙을 따릅니다:

  1. 측정 후 최적화: 실제 병목 지점을 파악
  2. 점진적 개선: 한 번에 모든 것을 최적화하지 말고 단계적으로
  3. 사용자 경험 우선: 성능 개선이 UX를 해치지 않도록 주의
  4. 유지보수성 고려: 과도한 최적화는 코드 복잡도를 증가시킬 수 있음

이러한 최적화 기법들을 적절히 활용하면 사용자에게 더 빠르고 부드러운 경험을 제공할 수 있습니다.

참고 자료

공유하기

관련 포스트