React

[React/리액트] React 성능 최적화: Code Splitting과 Lazy Loading 적용기 / 번들 최적화하기

solfa 2025. 1. 8. 07:40

프로젝트의 규모가 커지면서 초기 로딩 속도가 느려지는 문제가 발생했다. 특히 홈페이지 접속 시 불필요한 코드까지 모두 다운로드되는 현상을 발견하고, 이를 개선하기 위한 최적화 작업을 진행했다. 말로만 들어보던 lazy loading을 처음으로 적용해봤다! 각 이론에 대한 간단한 설명과 함께 내가 적용했던 코드와 성능 분석 방법들을 말해보고자 한다.

 

Code Splitting이란?

Code Splitting은 번들링된 코드를 여러 개의 작은 청크(chunk)로 분할하는 기술이다.

초기 로딩 시 필요한 코드만 다운로드 -> 나머지 코드는 필요할 때 로드 -> 전체 페이지 로드 시간 감소

 

이런 효과를 얻을 수 있고 React에서는 React.lazy()와 Suspense를 통해 쉽게 구현할 수 있다!

 

문제 상황

불필요한 코드 로드
홈페이지 접속 시 모든 페이지의 코드가 한꺼번에 다운로드되는 현상이 있었다.


분명 홈 화면인데 마이페이지, 채팅 페이지, 저장한 장소 페이지 코드 등등... 이는 초기 번들 사이즈를 증가시켜 첫 페이지 로딩 시간이 길어지는 원인이 되었다.

개선 방안: Code Splitting과 Lazy Loading

1. Route 기반 Code Splitting 적용

import { lazy } from 'react';

// 각 페이지를 lazy loading으로 변경
const Home = lazy(() => 
  import('./pages/home/home').then((module) => ({ 
    default: module.Home 
  }))
);
const Mypage = lazy(() => 
  import('./pages/mypage/mypage').then((module) => ({ 
    default: module.Mypage 
  }))
);


이렇게 lazy로 import하면 해당 컴포넌트는 처음 렌더링될 때만 로드된다. 

예를 들어, 사용자가 마이페이지에 접근할 때만 관련 코드를 다운로드하는 식이다.


2. Suspense로 로딩 상태 처리
`Suspense`는 코드가 로드되는 동안 대체 UI를 보여주는 React 컴포넌트다. lazy를 사용하기 위해서는 이걸로 감싸주는 작업이 필요하다.

function App() {
  return (
    <Suspense>
      <AppWrapper $hasNavbar={showNavbar}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/mypage" element={<Mypage />} />
          <Route path="/chatpage" element={<ChatPage />} />
          {/* ... 기타 라우트 ... */}
        </Routes>
        {showNavbar && <BottomNav />}
      </AppWrapper>
    </Suspense>
  );
}


Suspense로 감싸진 컴포넌트들은 로드되는 동안 fallback UI를 보여준다. fallback UI를 하나로 통일해주는 UI 일관성을 맡아주기도 한다.

 

개선 화면 - lazy loading 적용 후

 

lazy loading 적용 후를 보면 홈에서는 홈 관련 스크립트만 다운 받는 것을 볼 수 있다.

 

 

결과적으로 LCP도 2점이나 올랐다!

 

이렇게 코드 분할과 지연 로딩을 적용했지만, 여전히 각 번들의 크기가 최적화되지 않은 상태였다. 빠른 로딩을 위해서는 각 번들의 크기도 최적화할 필요가 있었다. 실제로 번들의 크기가 클수록 다운로드와 파싱, 실행하는 데 더 많은 시간이 걸리기 때문이다.

 

번들 최적화 전략

1. 청크 분할 (Chunking)

번들을 여러 개의 작은 파일로 나누어 효율적으로 관리하는 방법이다.


2. Bundle 최적화 (vite.config.ts)
Vite의 rollupOptions를 사용해 번들을 더 세밀하게 제어할 수 있다.

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // 자주 변경되지 않는 라이브러리들은 별도 청크로 분리
          vendor: ['react', 'react-dom', 'react-router-dom'],
          // 애니메이션 관련 코드 분리
          animations: ['framer-motion'],
          // 스타일 관련 코드 분리
          styling: ['styled-components'],
          // 상태 관리 라이브러리 분리
          queries: ['@tanstack/react-query'],
        },
      },
    },
  },
});


각 라이브러리를 별도의 청크로 분리하면 캐시 효율성이 증가하고 변경이 잦은 코드와 잦지 않은 코드를 분리할 수 있고 각 코드를 병렬 다운로드 가능하게 할 수 있다. 위에서는 스크립트를 분할했다면 여기에서는 라이브러리를 분할하는 느낌?


번들 분석과 최적화

1. 번들 사이즈 분석
최적화에 앞서 현재 상태를 정확히 파악하기 위해 `vite-bundle-visualizer`를 사용했다.

npx vite-bundle-visualizer

 

프로젝트를 열고 터미널에 이 명령어를 실행해주면

이런 식으로 번들과 번들이 차지하는 용량을 체크할 수 있다.

일부 청크가 500kB를 초과하고 있었고 SVG 파일이 거대한 크기를 차지(20MB+)하고 있었고

이미지 압축을 위해 라이브러리를 다운받았는데 그 용량이 가장 큰 것을 확인할 수 있었다. 

 

이렇게 차지하는 용량을 시각화된 자료로 얻을 수 있다.

분석 결과, React, framer-motion, styled-components와 같은 외부 라이브러리들이 하나의 큰 번들로 묶여 있는 것을 발견했다.

 

vite-bundle-visualizer가 제안하는 개선사항은 다음과 같다.
- 동적 import를 사용한 코드 분할
- `build.rollupOptions.output.manualChunks`로 청킹 개선
- 청크 크기 경고 한도 조정

 

이걸 바탕으로 청크 분리를 해보자!

라이브러리 청크 분리

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // 핵심 React 라이브러리들
          vendor: ['react', 'react-dom', 'react-router-dom'],
          
          // 애니메이션 관련
          animations: ['framer-motion'],
          
          // 스타일링 관련
          styling: ['styled-components'],
          
          // 상태 관리
          queries: ['@tanstack/react-query'],
        },
      },
    },
  },
});

 

각 라이브러리를 특성과 변경 빈도에 따라 별도의 청크로 분리했다.

 

  • 변경 빈도에 따른 분리
    • vendor: React 관련 라이브러리는 거의 변경되지 않음
    • animations: 애니메이션 로직도 안정적
    • styling: 스타일 관련 코드는 중간 정도의 변경 주기
    • queries: 상태 관리는 비교적 자주 업데이트
  • 캐싱 전략
    • 자주 변경되지 않는 vendor, animations는 장기 캐시
    • styling, queries는 중기 캐시
    • 애플리케이션 코드는 단기 캐시

이런 기준으로 분리를 했고 이후 다시 시각화를 한 결과

 

처음보다는 번들이 많이 쪼개진 것을 볼 수 있었당 굿쟙 실제 빌드 용량도 크진 않지만 감소했다.



마치며

이번 성능 최적화를 진행하면서 실제로 처음 해보는 것들이 많았다. 특히 Code Splitting이나 Lazy Loading 같은 개념들은 글로만 보다가 처음 적용해봤는데, 생각보다 적용 방법이 어렵지 않았고 효과가 좋았다. vite-bundle-visualizer 같은 도구를 써보면서 프로젝트의 번들 구조를 시각적으로 파악할 수 있었던 게 재미있었다. 다음에는 Tree Shaking을 더 적극적으로 활용한다거나, 이미지 최적화를 진행한다면 더 나은 성능을 얻을 수 있을 것 같다. 앞으로도 새로운 기술을 적용하고 개선하는 과정을 꾸준히 기록하고 수치로 남기자!!! 

 

728x90