35일차 React 최적화

정상희·2025년 4월 24일

코딩공부

목록 보기
43/60

현재 오즈코딩스쿨 강의를 통해 프론트엔드를 학습하고 있습니다.
본 포스트는 해당 강의에 대한 내용 정리를 목적으로 합니다.

1. lazy와 Suspense

1) lazy

컴포넌트를 바로 불러오지 않고, 실제로 그 컴포넌트가 필요할 때 불러옴
React.lazy는 컴포넌트를 동적으로 import하여 코드 스플리팅을 구현할 수 있게 해주는 함수이다. 즉, 해당 컴포넌트를 실제로 필요할 때까지 로딩하지 않고, 나중에 필요한 시점에서 가져오도록 할 수 있다.

const MyComponent = React.lazy(() => import('./MyComponent'));

이렇게 하면 MyComponent는 실제로 렌더링되기 전까지 로드되지 않으며, 이를 통해 초기 번들 크기를 줄일 수 있다.


2) Suspense

컴포넌트를 불러오는 동안 사용자에게 보여줄 임시 화면 설정.
Suspense는 React.lazy로 로드되는 컴포넌트가 비동기적으로 로드되는 동안 보여줄 UI를 정의하는 컴포넌트이다. 일반적으로 로딩 상태를 보여주는 데 사용된다.

import React, { Suspense } from 'react';
const MyComponent = React.lazy(() => import('./MyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MyComponent />
    </Suspense>
  );
}

위 코드에서 MyComponent가 로드되는 동안에는 "Loading..."이라는 메시지가 보여지게 된다. 이 방식은 사용자에게 더 나은 경험을 제공할 수 있게 해준다.


3) 장단점

a. 장점

  • 코드 스플리팅을 통한 성능 향상
    초기 번들 크기를 줄여서 첫 로딩 속도를 개선할 수 있다. 필요한 컴포넌트만 로드되기 때문에 사용자 경험이 향상된다.

  • 간단한 문법
    React.lazySuspense만으로 쉽게 코드 스플리팅을 적용할 수 있어 설정이 간편하다.

  • 지연 로딩(Deferred loading)
    사용자가 실제로 해당 컴포넌트를 보기 전까지는 로딩하지 않기 때문에 리소스 낭비를 줄일 수 있다.

  • 로딩 UI 설정 가능
    Suspensefallback 속성을 통해 사용자에게 자연스러운 로딩 상태를 보여줄 수 있다.


b. 단점

  • 서버사이드 렌더링(SSR) 미지원
    React.lazy는 기본적으로 클라이언트 사이드에서만 동작하므로, Next.js 등 SSR 환경에서는 문제가 발생할 수 있다.

  • 에러 핸들링 한계
    로드 중에 에러가 발생해도 직접적인 오류 처리가 어려우며, ErrorBoundary를 함께 사용해야 한다.

  • 초기 로딩이 아닌 중간 로딩이 생길 수 있음
    라우팅을 통해 페이지를 전환하거나 특정 기능을 사용할 때 로딩이 생겨 사용자 흐름이 끊길 수 있다.

  • Suspense의 제한적 활용
    React 18 이전까지는 Suspense가 데이터 로딩 등 비동기 처리에 제한적이었고, 아직 모든 비동기 상황에 완벽하게 대응하지는 못한다.



2. 번들링과 코드스플리팅

1) 번들링이란?

번들링이란 여러 개의 자바스크립트 파일과 모듈들을 하나의 파일로 묶어주는 작업이다. 이를 통해 웹 페이지가 서버로부터 로드해야 할 파일 수를 줄이고, 초기 로딩 속도를 향상시킬 수 있다.

a. 번들링의 주요 효과

  • 파일 수 감소
    여러 개의 JS, CSS, 이미지 파일 등을 하나로 묶음으로써 브라우저가 서버에 요청해야 하는 파일 수를 줄일 수 있다. 이로 인해 HTTP 요청 횟수가 감소하여 네트워크 성능이 향상된다.

  • 로딩 속도 개선
    번들된 파일은 압축(예: gzip, Brotli)되어 전달되므로, 전송 크기 감소를 통해 페이지 로딩 속도를 높일 수 있다.

  • 의존성 관리 용이
    번들링 도구(Webpack, Vite 등)는 모듈 간 의존성을 분석해 필요한 코드만 포함시켜 주므로, 불필요한 코드 제거(Tree shaking)와 의존성 자동 정리에 효과적이다.

  • 캐싱 효율화
    번들 파일은 보통 해시값을 파일명에 포함하므로, 코드 변경이 없으면 브라우저가 캐시를 재사용할 수 있어 성능이 향상된다.

  • 코드 일관성 유지
    개발 환경에서 모듈을 나눠 작성하더라도, 번들링을 통해 일관된 하나의 실행 파일로 만들어지므로 배포와 실행 환경이 안정적이 된다.

b. 번들링 도구의 종류

  • Rollup : 라이브러리 번들링에 적합한 경량 번들러이다.

    • 장점

      • ES 모듈 기반으로 트리 셰이킹 성능이 우수
      • 출력 번들이 작고 최적화에 유리
      • 라이브러리 제작에 자주 사용됨
    • 단점

      • 애플리케이션용으로는 설정이 불편할 수 있음
      • 개발 서버 기능은 약한 편
  • Webpack : 가장 널리 사용되는 JavaScript 번들러이다.

    • 장점

      • 플러그인과 로더 기반의 유연한 설정 가능
      • 다양한 파일 타입 지원(JS, CSS, 이미지 등)
      • 코드 스플리팅, 트리 셰이킹 지원
      • 커뮤니티와 문서가 풍부
    • 단점

      • 설정이 복잡하고 러닝커브가 있음
      • 빌드 속도가 느릴 수 있음

2) 코드스플리팅이란?

코드 스플리팅이란 애플리케이션의 코드를 여러 개의 청크(chunk)로 나누어 필요한 시점에 필요한 코드만 로드할 수 있도록 하는 기술이다. 이를 통해 초기 로딩 속도를 더욱 개선하고, 사용자 경험을 향상시킬 수 있다.

a. 코드스플리팅의 주요 효과

  • 초기 로딩 속도 개선

    • 전체 앱을 한 번에 로드하지 않고, 필요한 코드만 먼저 불러오기 때문에 첫 페이지 로딩 시간이 단축된다.
    • 사용자가 보지 않을 컴포넌트까지 모두 로드할 필요가 없기 때문에 리소스를 절약할 수 있다.
  • 사용자 경험 향상

    • 로딩 중인 화면에 대해 Suspense를 이용한 로딩 UI를 제공할 수 있어, 사용자 입장에서 끊김 없는 인터페이스를 제공할 수 있다.
    • 페이지 전환이나 특정 기능 사용 시 필요한 컴포넌트만 불러오기 때문에 더 부드러운 상호작용이 가능하다.
  • 트래픽 절감
    한 번에 모든 리소스를 다운로드하지 않기 때문에, 모바일이나 저속 네트워크 환경에서도 데이터 낭비를 줄일 수 있다.

  • Lazy Loading과의 연계 가능
    코드 스플리팅은 React의 React.lazy와 함께 사용되며, 라우터와 연계하면 페이지 단위, 컴포넌트 단위로 분할이 가능하다. 이를 통해 라우팅 기반 분할(예: 페이지별 코드 스플리팅)도 자연스럽게 구현할 수 있다.

  • 기능 단위 최적화
    대형 애플리케이션에서 기능별로 코드 분리를 하면, 유지보수성이 향상되고, 버그 발생 범위도 좁힐 수 있다.


b. lazy와 Suspense를 이용한 코드 스플리팅

  • 컴포넌트를 동적으로 불러오기 (React.lazy)
    React.lazy()는 import를 지연시켜 해당 컴포넌트가 실제로 렌더링될 때 로드되도록 한다.
// About.js 라는 컴포넌트를 동적으로 불러온다
const About = React.lazy(() => import('./About'));
  • Suspense로 로딩 UI 지정하기
    React.lazy로 불러오는 컴포넌트는 비동기 처리이므로, 로딩 중에 보여줄 화면을 Suspense로 감싸고 fallback 속성으로 지정한다.
import React, { Suspense } from 'react';
const About = React.lazy(() => import('./About'));

function App() {
  return (
    <div>
      <h1>Welcome</h1>
      <Suspense fallback={<div>로딩 중...</div>}>
        <About />
      </Suspense>
    </div>
  );
}
  • 라우팅과 함께 사용하는 예 (React Router v6 기준)
    페이지 단위 코드 스플리팅에 자주 사용하는 방식이다.
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Suspense } from 'react';

const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>페이지 불러오는 중...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
  • 에러 핸들링: 컴포넌트 로딩 실패 시를 대비해 ErrorBoundary를 함께 사용하면 좋다.

  • 분리 기준: 페이지 단위 외에도 특정 기능이나 무거운 UI 컴포넌트 단위로 분리할 수 있다.



3. debounce와 throttle

1) debounce의 개념과 사용법

a. 개념

debounce는 너무 자주 실행되는 함수 호출을 제어하여, 일정 시간 동안 입력이 없을 때 한 번만 실행되도록 만드는 기술이다.

  • 주로 검색창 자동완성, 스크롤 이벤트, resize 이벤트 등에서 성능 최적화를 위해 사용된다.

  • 입력이 계속 들어오면 실행을 미루다가, 마지막 입력 이후 일정 시간이 지나야 실제로 실행된다.


b. 사용법

  • debounce 기본 사용법 (lodash)
    먼저 lodash 라이브러리를 설치한다: npm install lodash

    import React, { useState, useMemo } from 'react';
    import debounce from 'lodash/debounce';
    
    function SearchBox() {
      const [value, setValue] = useState('');
    
      // 디바운스 함수는 useMemo로 한번만 생성
      const debouncedSearch = useMemo(() =>
        debounce((input) => {
          console.log('API 요청:', input); // API 호출 위치
        }, 300)
      , []);
    
      const handleChange = (e) => {
        const input = e.target.value;
        setValue(input);
        debouncedSearch(input); // debounce된 함수 호출
      };
    
      return (
        <input
          type="text"
          placeholder="검색어 입력"
          value={value}
          onChange={handleChange}
        />
      );
    }
    • 300은 300ms 지연 시간
    • useMemo로 debounce 함수는 재생성되지 않도록 처리
    • API 호출이 너무 자주 발생하지 않도록 제어

  • debounce 직접 구현해서 사용하기

    function debounce(func, delay) {
      let timeout;
      return function (...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), delay);
      };
    }

    const handleResize = debounce(() => {
      console.log('창 크기 변경됨');
    }, 500);
    
    window.addEventListener('resize', handleResize);

2) throttle의 개념과 사용법

a. 개념

throttle은 특정 시간 간격마다 함수가 한 번만 실행되도록 제한하는 기술이다.
→ 주기적으로 반복되는 이벤트(스크롤, resize 등)의 실행 횟수를 강제 제한해서 성능을 높인다.


b. 사용법

  • throttle 사용법 (lodash)
    먼저 lodash 라이브러리를 설치한다: npm install lodash
import React, { useEffect } from 'react';
import throttle from 'lodash/throttle';

function ScrollTracker() {
  useEffect(() => {
    const handleScroll = throttle(() => {
      console.log('스크롤 위치:', window.scrollY);
    }, 500); // 0.5초에 한 번만 실행됨

    window.addEventListener('scroll', handleScroll);

    // cleanup
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return <div style={{ height: '2000px' }}>스크롤 해보세요</div>;
}

여기선 스크롤 이벤트가 자주 발생하더라도 0.5초에 한 번만 로그가 찍힌다.


  • throttle 직접 함수 구현해서 사용하기

    function throttle(func, delay) {
      let lastCall = 0;
      return function (...args) {
        const now = Date.now();
        if (now - lastCall >= delay) {
          lastCall = now;
          func.apply(this, args);
        }
      };
    }

    const throttledResize = throttle(() => {
      console.log('윈도우 리사이즈!');
    }, 1000);
    
    window.addEventListener('resize', throttledResize);

3) debounce와 throttle의 차이점

구분debounce (디바운스)throttle (스로틀)
정의마지막 이벤트 발생 후 일정 시간 지나면 실행일정 시간마다 한 번만 실행
주요 목적잦은 입력 중 마지막 것만 반영잦은 입력 중 주기적으로 반영
실행 타이밍입력 멈춘 뒤 delay만큼 기다렸다가 실행입력 중이라도 delay마다 한 번씩 실행
대표 사용 예검색 자동완성, 폼 입력값 검증, API 요청스크롤 추적, 윈도우 리사이즈, 마우스 이동 추적
실행 횟수 제어무조건 1회만 (입력 멈출 때)여러 번 실행 가능하지만 간격 조절됨
profile
UI/UX디자이너의 코딩 공부

0개의 댓글