1. 렌더링 최적화 - 화면을 빠르게 그리는 기술

메모이제이션(Memoization) - 똑같은 계산은 한 번만

메모이제이션은 한 번 계산한 결과를 메모리에 저장해두고, 똑같은 계산이 필요할 때 저장된 값을 재사용하는 기법입니다. 마치 수학 문제를 풀 때 이미 계산한 답을 옆에 적어두고 다시 쓰는 것과 같습니다.

  1. 실제 구현 방법
    // React에서 useMemo 사용 예시
    const expensiveCalculation = useMemo(() => {
      // 복잡한 계산 로직
      return heavyComputation(data);
    }, [data]); // data가 변경될 때만 재계산
  2. useCallback으로 함수 메모이제이션
    const handleClick = useCallback(() => {
      // 클릭 처리 로직
    }, [dependency]); // dependency가 변경될 때만 함수 재생성
  3. 언제 사용해야 하나요?
    • 복잡한 계산이 반복되는 경우
    • 자식 컴포넌트에 함수를 props로 전달할 때
    • 리스트 렌더링에서 각 아이템이 무거운 연산을 포함할 때

React.memo - 컴포넌트 전체를 기억하기

React.memo는 컴포넌트 자체를 메모이제이션합니다. props가 변경되지 않았다면 컴포넌트를 다시 렌더링하지 않고 이전 결과를 재사용합니다.

  1. 기본 사용법
    const MyComponent = React.memo(function MyComponent({ name, age }) {
      return <div>{name}님의 나이는 {age}살입니다</div>;
    });
  2. 커스텀 비교 함수 활용
    const MyComponent = React.memo(
      function MyComponent(props) {
        // 컴포넌트 로직
      },
      (prevProps, nextProps) => {
        // true를 반환하면 리렌더링 건너뛰기
        return prevProps.id === nextProps.id;
      }
    );
  3. 주의사항
    • 모든 컴포넌트에 무작정 적용하면 오히려 성능 저하
    • props 비교 자체도 비용이 들기 때문에 신중하게 사용
    • 자주 변경되는 props를 가진 컴포넌트에는 비효율적

리렌더링 방지 전략

불필요한 리렌더링은 웹앱 성능의 가장 큰 적입니다. 화면의 작은 부분만 바뀌어도 전체가 다시 그려지는 것을 방지해야 합니다.

  1. 상태 분리하기
    // 나쁜 예 - 전체 폼이 리렌더링
    const Form = () => {
      const [formData, setFormData] = useState({ name: '', email: '', age: '' });
    };
    
    // 좋은 예 - 각 필드가 독립적으로 관리
    const NameField = () => {
      const [name, setName] = useState('');
    };
  2. Context 분할하기
    // UserContext와 ThemeContext를 분리
    const UserProvider = ({ children }) => { /* ... */ };
    const ThemeProvider = ({ children }) => { /* ... */ };
  3. key 속성 최적화
    // 인덱스 대신 고유한 ID 사용
    items.map(item => <Item key={item.id} />)

2. 이벤트 처리 최적화 - 과도한 실행 막기

디바운싱(Debouncing) - 마지막 액션만 처리

디바운싱은 연속적으로 발생하는 이벤트 중 마지막 이벤트만 처리하는 기법입니다. 검색창에 타이핑할 때마다 API를 호출하면 서버에 부담이 되는데, 디바운싱을 사용하면 사용자가 타이핑을 멈춘 후에만 검색을 실행합니다.

  1. 구현 예시
    function debounce(func, delay) {
      let timeoutId;
      return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
      };
    }
    
    // 사용 예시
    const searchAPI = debounce((query) => {
      // API 호출 로직
    }, 500); // 0.5초 대기
  2. React Hook으로 구현
    function useDebounce(value, delay) {
      const [debouncedValue, setDebouncedValue] = useState(value);
    
      useEffect(() => {
        const handler = setTimeout(() => {
          setDebouncedValue(value);
        }, delay);
    
        return () => clearTimeout(handler);
      }, [value, delay]);
    
      return debouncedValue;
    }
  3. 실제 활용 사례
    • 검색 자동완성
    • 폼 유효성 검사
    • 윈도우 리사이즈 이벤트 처리

쓰로틀링(Throttling) - 일정 간격으로만 실행

쓰로틀링은 특정 시간 간격으로만 함수가 실행되도록 제한합니다. 스크롤 이벤트처럼 매우 빈번하게 발생하는 이벤트를 처리할 때 유용합니다.

  1. 구현 방법
    function throttle(func, limit) {
      let inThrottle;
      return function(...args) {
        if (!inThrottle) {
          func.apply(this, args);
          inThrottle = true;
          setTimeout(() => inThrottle = false, limit);
        }
      };
    }
  2. 스크롤 이벤트 최적화
    const handleScroll = throttle(() => {
      // 스크롤 위치 계산 및 처리
    }, 100); // 100ms마다 실행
    
    window.addEventListener('scroll', handleScroll);
  3. 디바운싱 vs 쓰로틀링 선택 기준
    • 디바운싱: 사용자 입력이 끝난 후 처리 (검색, 저장)
    • 쓰로틀링: 지속적인 업데이트 필요 (스크롤, 드래그)

페이지네이션 - 데이터를 나누어 보여주기

대량의 데이터를 한 번에 모두 로드하면 초기 로딩이 느려지고 메모리를 많이 사용합니다. 페이지네이션으로 필요한 만큼만 보여주면 성능이 크게 향상됩니다.

  1. 기본 페이지네이션
    const itemsPerPage = 20;
    const currentItems = data.slice(
      (currentPage - 1) * itemsPerPage,
      currentPage * itemsPerPage
    );
  2. 무한 스크롤 구현
    const InfiniteScroll = () => {
      const [items, setItems] = useState([]);
      const [page, setPage] = useState(1);
    
      const loadMore = useCallback(() => {
        // 다음 페이지 데이터 로드
        fetchData(page + 1).then(newItems => {
          setItems(prev => [...prev, ...newItems]);
          setPage(prev => prev + 1);
        });
      }, [page]);
    
      // Intersection Observer로 스크롤 감지
    };
  3. 가상 스크롤링
    • 화면에 보이는 항목만 렌더링
    • react-window, react-virtualized 라이브러리 활용
    • 수천 개의 항목도 부드럽게 스크롤 가능

3. 메모리 관리 - 누수 방지와 효율적 사용

메모리 누수 방지

메모리 누수는 더 이상 필요 없는 데이터가 메모리에서 해제되지 않는 현상입니다. 시간이 지날수록 앱이 느려지고 결국 멈출 수도 있습니다.

  1. 이벤트 리스너 정리
    useEffect(() => {
      const handleResize = () => { /* ... */ };
      window.addEventListener('resize', handleResize);
    
      // cleanup 함수에서 반드시 제거
      return () => {
        window.removeEventListener('resize', handleResize);
      };
    }, []);
  2. 타이머 정리
    useEffect(() => {
      const timer = setInterval(() => {
        // 주기적 작업
      }, 1000);
    
      return () => clearInterval(timer);
    }, []);
  3. 구독 해제
    useEffect(() => {
      const subscription = dataSource.subscribe(handleChange);
    
      return () => {
        subscription.unsubscribe();
      };
    }, []);

가비지 컬렉션 최적화

자바스크립트는 자동으로 메모리를 관리하지만, 개발자가 도울 수 있는 부분이 있습니다.

  1. 순환 참조 방지
    // 나쁜 예 - 순환 참조
    const obj1 = {};
    const obj2 = {};
    obj1.ref = obj2;
    obj2.ref = obj1;
    
    // 좋은 예 - WeakMap 사용
    const weakMap = new WeakMap();
    weakMap.set(obj1, obj2);
  2. 전역 변수 최소화
    // 나쁜 예
    window.myGlobalData = hugeArray;
    
    // 좋은 예 - 모듈 스코프 사용
    const moduleData = hugeArray;
    export { moduleData };
  3. 대용량 데이터 처리 후 정리
    let bigData = processLargeDataset();
    // 사용 완료 후
    bigData = null; // 명시적으로 참조 해제

4. 번들 최적화 - 다운로드 크기 줄이기

코드 스플리팅

애플리케이션을 여러 조각으로 나누어 필요한 시점에만 로드하는 기법입니다. 초기 로딩 속도가 크게 향상됩니다.

  1. 라우트 기반 분할
    const Home = lazy(() => import('./pages/Home'));
    const About = lazy(() => import('./pages/About'));
    
    function App() {
      return (
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </Suspense>
      );
    }
  2. 컴포넌트 레벨 분할
    const HeavyComponent = lazy(() => import('./HeavyComponent'));
    
    function MyComponent() {
      const [showHeavy, setShowHeavy] = useState(false);
    
      return (
        <>
          <button onClick={() => setShowHeavy(true)}>
            무거운 컴포넌트 보기
          </button>
          {showHeavy && (
            <Suspense fallback={<div>로딩중...</div>}>
              <HeavyComponent />
            </Suspense>
          )}
        </>
      );
    }
  3. 동적 import 활용
    button.addEventListener('click', async () => {
      const module = await import('./heavyModule.js');
      module.doSomething();
    });

레이지 로딩

리소스를 필요한 시점에 로드하여 초기 로딩 부담을 줄입니다.

  1. 이미지 레이지 로딩
    const LazyImage = ({ src, alt }) => {
      const [imageSrc, setImageSrc] = useState(null);
      const imgRef = useRef();
    
      useEffect(() => {
        const observer = new IntersectionObserver(
          entries => {
            entries.forEach(entry => {
              if (entry.isIntersecting) {
                setImageSrc(src);
                observer.unobserve(entry.target);
              }
            });
          }
        );
    
        if (imgRef.current) {
          observer.observe(imgRef.current);
        }
    
        return () => observer.disconnect();
      }, [src]);
    
      return <img ref={imgRef} src={imageSrc} alt={alt} />;
    };
  2. 컴포넌트 레이지 로딩
    const LazySection = () => {
      const [isVisible, setIsVisible] = useState(false);
      const sectionRef = useRef();
    
      useEffect(() => {
        const observer = new IntersectionObserver(
          ([entry]) => {
            if (entry.isIntersecting) {
              setIsVisible(true);
            }
          },
          { threshold: 0.1 }
        );
    
        if (sectionRef.current) {
          observer.observe(sectionRef.current);
        }
      }, []);
    
      return (
        <div ref={sectionRef}>
          {isVisible && <ExpensiveComponent />}
        </div>
      );
    };

트리 쉐이킹

사용하지 않는 코드를 번들에서 제거하여 파일 크기를 줄입니다.

  1. ES6 모듈 사용
    // 나쁜 예 - 전체 라이브러리 import
    import _ from 'lodash';
    
    // 좋은 예 - 필요한 함수만 import
    import debounce from 'lodash/debounce';
  2. 사이드 이펙트 표시
    // package.json
    {
      "sideEffects": false, // 또는 특정 파일만 지정
      "sideEffects": ["*.css", "*.scss"]
    }
  3. Production 빌드 설정
    // webpack.config.js
    module.exports = {
      mode: 'production',
      optimization: {
        usedExports: true,
        minimize: true,
        sideEffects: false
      }
    };

5. 캐싱 전략 - 한 번 받은 데이터 재활용

브라우저 캐싱

브라우저의 캐싱 기능을 활용하면 반복적인 리소스 다운로드를 줄일 수 있습니다.

  1. HTTP 캐시 헤더 설정
    // 서버 응답 헤더
    res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1년
    res.setHeader('ETag', '"123456"'); // 버전 관리
  2. Service Worker 캐싱
    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open('v1').then(cache => {
          return cache.addAll([
            '/',
            '/styles/main.css',
            '/scripts/app.js'
          ]);
        })
      );
    });
    
    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request).then(response => {
          return response || fetch(event.request);
        })
      );
    });
  3. localStorage 활용
    // 데이터 캐싱
    const getCachedData = (key) => {
      const cached = localStorage.getItem(key);
      if (cached) {
        const { data, timestamp } = JSON.parse(cached);
        const isExpired = Date.now() - timestamp > 3600000; // 1시간
        if (!isExpired) return data;
      }
      return null;
    };
    
    const setCachedData = (key, data) => {
      localStorage.setItem(key, JSON.stringify({
        data,
        timestamp: Date.now()
      }));
    };

CDN 활용

CDN을 사용하면 사용자와 가까운 서버에서 콘텐츠를 제공받아 로딩 속도가 향상됩니다.

  1. 정적 자산 CDN 배포
    <!-- 라이브러리 CDN 사용 -->
    <script src="<https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js>"></script>
    
    <!-- 이미지 CDN -->
    <img src="<https://cdn.example.com/images/hero.jpg>" />
  2. CDN 캐시 무효화
    // 버전 관리로 캐시 갱신
    const assetUrl = `https://cdn.example.com/app.js?v=${VERSION}`;

6. 렌더링 성능 극대화

가상 DOM 최적화

React나 Vue 같은 프레임워크는 가상 DOM을 사용하여 실제 DOM 조작을 최소화합니다.

  1. 효율적인 리스트 렌더링
    // key 속성으로 요소 추적
    {items.map(item => (
      <ListItem
        key={item.id} // 안정적인 고유 키 사용
        data={item}
      />
    ))}
  2. 조건부 렌더링 최적화
    // 나쁜 예 - 매번 새로운 컴포넌트 생성
    {isVisible ? <Component /> : null}
    
    // 좋은 예 - display 속성 활용
    <Component style={{ display: isVisible ? 'block' : 'none' }} />

리플로우/리페인트 최소화

DOM 변경 시 브라우저가 레이아웃을 다시 계산(리플로우)하고 화면을 다시 그리는(리페인트) 과정을 최소화해야 합니다.

  1. DOM 조작 배치 처리
    // 나쁜 예 - 여러 번 리플로우 발생
    element.style.left = '10px';
    element.style.top = '10px';
    element.style.width = '100px';
    
    // 좋은 예 - 한 번에 처리
    element.style.cssText = 'left: 10px; top: 10px; width: 100px;';
  2. GPU 가속 활용
    /* transform과 opacity는 GPU에서 처리 */
    .animated {
      transform: translateX(100px);
      opacity: 0.8;
      will-change: transform, opacity;
    }
  3. 레이아웃 스래싱 방지
    // 나쁜 예 - 읽기와 쓰기 반복
    elements.forEach(el => {
      el.style.height = el.offsetHeight + 10 + 'px';
    });
    
    // 좋은 예 - 읽기 후 쓰기
    const heights = elements.map(el => el.offsetHeight);
    elements.forEach((el, i) => {
      el.style.height = heights[i] + 10 + 'px';
    });

Critical Rendering Path 최적화

브라우저가 HTML을 받아서 화면에 픽셀을 그리기까지의 과정을 최적화합니다.

  1. 리소스 로딩 순서 최적화
    <!DOCTYPE html>
    <html>
    <head>
      <!-- 중요한 CSS는 인라인으로 -->
      <style>
        .critical { /* 초기 렌더링에 필요한 스타일 */ }
      </style>
    
      <!-- 나머지 CSS는 비동기 로드 -->
      <link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
    
      <!-- 중요하지 않은 JS는 defer -->
      <script defer src="app.js"></script>
    </head>
    <body>
      <!-- 콘텐츠 -->
    
      <!-- 분석 스크립트는 최하단 -->
      <script async src="analytics.js"></script>
    </body>
    </html>
  2. 리소스 힌트 활용
    <!-- DNS 미리 연결 -->
    <link rel="dns-prefetch" href="//api.example.com">
    
    <!-- 연결 미리 설정 -->
    <link rel="preconnect" href="//api.example.com">
    
    <!-- 리소스 미리 가져오기 -->
    <link rel="prefetch" href="next-page.js">
    
    <!-- 중요 리소스 미리 로드 -->
    <link rel="preload" href="font.woff2" as="font" crossorigin>

7. 최적화를 위한 용어 - 이걸 알아야 AI 에게 지시 가능

  • 렌더링 최적화

    메모이제이션, React.memo, useMemo, useCallback, 리렌더링, shouldComponentUpdate, PureComponent, 가상DOM, 리플로우, 리페인트, 레이아웃스래싱, CriticalRenderingPath, FOUC, FOIT, 프레임드롭, requestAnimationFrame, will-change, GPU가속, 컴포지팅, 레이어생성

  • 번들 및 로딩 최적화

    코드스플리팅, 레이지로딩, 트리쉐이킹, 번들링, 미니파이, 압축, gzip, brotli, 청크, 동적임포트, prefetch, preload, preconnect, dns-prefetch, async, defer, 모듈페더레이션, 웹팩최적화, 롤업, 파셀, Vite, esbuild

  • 이벤트 처리 최적화

    디바운싱, 쓰로틀링, 이벤트위임, 이벤트버블링, 이벤트캡처링, 패시브리스너, 이벤트풀링, 배치업데이트, RAF쓰로틀링

  • 데이터 처리 최적화

    페이지네이션, 무한스크롤, 가상스크롤, 윈도잉, 커서페이지네이션, 오프셋페이지네이션, 데이터정규화, 데이터캐싱, 옵티미스틱업데이트, 낙관적UI

  • 메모리 관리

    메모리누수, 가비지컬렉션, 메모리프로파일링, 힙스냅샷, WeakMap, WeakSet, 순환참조, 클로저누수, 이벤트리스너정리, 타이머정리, 옵저버해제, 메모리풀링, 객체풀링

  • 캐싱 전략

    브라우저캐싱, HTTP캐싱, 서비스워커, 캐시무효화, ETag, Last-Modified, Cache-Control, localStorage, sessionStorage, IndexedDB, 메모리캐시, 디스크캐시, CDN캐싱, 엣지캐싱, 캐시워밍, 캐시히트율

  • 네트워크 최적화

    CDN, 엣지서버, HTTP2, HTTP3, QUIC, 멀티플렉싱, 서버푸시, 헤더압축, 커넥션풀링, Keep-Alive, 도메인샤딩, 리소스힌트, 프리페칭, 배치요청, GraphQL, 데이터로더

  • 이미지 최적화

    이미지압축, WebP, AVIF, 반응형이미지, srcset, picture요소, 이미지스프라이트, Base64인코딩, 블러업, 프로그레시브JPEG, 이미지CDN, 이미지최적화API, 아트디렉션

  • 폰트 최적화

    폰트서브세팅, WOFF2, 폰트디스플레이, 폰트프리로드, 가변폰트, 폰트페이스옵저버, 로컬폰트, 시스템폰트스택, 폰트로딩전략

  • 애니메이션 최적화

    CSS애니메이션, 트랜지션, 트랜스폼, FLIP기법, 모션블러, 60FPS, 하드웨어가속, 컴포짓레이어, 페인트최적화, 레이아웃경계

  • 상태 관리 최적화

    상태정규화, 상태분리, 컨텍스트분할, 리듀서최적화, 셀렉터메모이제이션, 불변성, Immer, 구조적공유, 상태구독, 원자적상태

  • 빌드 최적화

    프로덕션빌드, 소스맵, 환경변수, 빌드캐싱, 증분빌드, 병렬빌드, 빌드타임최적화, 정적사이트생성, 서버사이드렌더링, 정적최적화

  • 성능 측정

    Lighthouse, WebVitals, LCP, FID, CLS, TTFB, FCP, TTI, SpeedIndex, 성능프로파일링, 크롬데브툴스, 성능모니터링, RUM, 합성모니터링

  • API 최적화

    RESTful, GraphQL, gRPC, WebSocket, SSE, 롱폴링, 폴링최적화, API게이트웨이, 레이트리미팅, API캐싱, 배치API, 페이지네이션API

  • 모바일 최적화

    터치최적화, 뷰포트설정, 모바일퍼스트, AMP, PWA, 오프라인지원, 앱셸모델, PRPL패턴, 적응형디자인, 터치제스처

  • 보안 관련 최적화

    CSP, CORS, XSS방지, CSRF보호, 서브리소스무결성, HTTPS, SSL최적화, 보안헤더, 난독화, 코드서명

  • 서버 최적화

    서버사이드캐싱, 리버스프록시, 로드밸런싱, 오토스케일링, 서버리스, 엣지컴퓨팅, 마이크로서비스, 컨테이너화, 클러스터링

  • 데이터베이스 최적화

    쿼리최적화, 인덱싱, 커넥션풀링, 읽기전용복제본, 샤딩, 파티셔닝, 캐싱레이어, NoSQL, 비정규화, 쿼리배칭