React 환경에서 성능을 개선하는 방법은 다양하게 있다.
그 중 번들 크기를 감소시켜 초기 로딩 속도를 최적화 하는 방법 몇 가지를 자세하게 알아보자.
1. Tree Shaking
사용되지 않는(dead) 코드를 제거하여 최종 번들 크기를 줄이는 기법이다.
불필요한 코드를 제거해 번들 크기를 축소하고, 로딩 시간을 단축시키기 위해 사용한다.
1.1 사용 예시
import _ from 'lodash'; // lodash 전체를 가져옴
function MyComponent() {
const arr = [1, 2, 3];
const newArr = _.shuffle(arr); // 실제로는 shuffle 함수만 필요
return <div>{newArr.join(',')}</div>;
}
export default MyComponent;
위의 코드는 약 70KB+인 lodash 라이브러리 전체를 불러옴(import)으로써, 사용하지 않는 코드(대부분의 lodash 함수)도 번들에 포함된다.
import { shuffle } from 'lodash'; // 특정 함수만 임포트
function MyComponent() {
const arr = [1, 2, 3];
const newArr = shuffle(arr);
return <div>{newArr.join(',')}</div>;
}
export default MyComponent;
이렇게 수정하면 번들러(Webpack, Rollup 등)가 사용되지 않는 다른 lodash 함수들을 자동으로 제거(Tree Shaking)해 번들 크기가 크게 감소한다.
단, export default가 많은 라이브러리는 Tree Shaking이 어려울 수 있어
가급적 Named Export를 사용하는 라이브러리를 활용하는 것이 좋다.
2. Code Splitting (코드 분할)
전체 애플리케이션 번들을 한 번에 불러오는 대신, 애플리케이션 코드(번들)를 여러 개의 파일로 분할하여 필요한 시점에 필요한 코드만 불러오는 기법이다.
코드 분할을 하는 이유는 초기 로딩 시 무거운 전체 번들을 한 번에 다운받지 않도록 하여, 초기 번들 크기를 감소시켜 초기 로딩 시간을 단축해 첫 페이지 로딩 속도를 향상시켜 결과적으로 사용자 경험을 개선하기 위해서 사용한다.
2.1 사용 예시
import React from 'react';
import BigComponent from './BigComponent';
import AnotherBigComponent from './AnotherBigComponent';
function App() {
return (
<div>
<BigComponent />
<AnotherBigComponent />
</div>
);
}
export default App;
위의 코드 같은 경우, 모든 컴포넌트를 한 번에 임포트하는 것을 볼 수 있다.
이러면 최종 번들이 매우 커질 수 있으며 사용자가 당장 보지 않는 컴포넌트도 전부 다운로드하여 초기 로딩 속도가 느려진다.
import React, { Suspense } from 'react';
// React.lazy를 이용해 동적 import로 컴포넌트를 분할
const BigComponent = React.lazy(() => import('./BigComponent'));
const AnotherBigComponent = React.lazy(() => import('./AnotherBigComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<BigComponent />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<AnotherBigComponent />
</Suspense>
</div>
);
}
export default App;
이전의 코드를 위와 같이 React.lazy와 Suspense를 사용하면, 컴포넌트를 개별 번들로 분리하여 동적으로 불러오게 된다.
초기에는 fallback UI만 보였다가, 실제 컴포넌트가 로드된 뒤 화면에 표시된다.
여기서 중요한 것은 Suspense는 SSR이 아닌 CSR 방식이라는 점이다!
React.lazy()는 동적 import를 사용해서 클라이언트 측에서만 컴포넌트를 로드하고,
Suspense는 클라이언트에서 로딩 상태 UI를 보여주기 위해 만들어진 컴포넌트이다.
때문에 서버에서는 JavaScript가 실행되기 전이라 동적으로 로딩하거나 fallback을 보여줄 수 없다.
Next.js 같은 SSR 프레임워크에서는 React.lazy를 직접적으로 쓰면 SSR 단계에서 오류가 나거나 컴포넌트가 렌더링되지 않을 수 있다는 말이다.
2.2 그렇다면 Next.js에서는 어떻게 코드 스플리팅을 적용시키나?
import dynamic from 'next/dynamic';
const LazyComponent = dynamic(() => import('../components/LazyComponent'), {
ssr: false, // 이 옵션이 핵심! 서버에선 렌더링하지 않음
loading: () => <p>로딩 중입니다...</p>, // 클라이언트에서 보여줄 fallback
});
export default function Page() {
return <LazyComponent />;
}
Next.js는 SSR과 CSR 모두를 지원하기 위해, 위와 같이 React.lazy 대신 next/dynamic을 사용한다.
이때 ssr은 false를 설정하면 해당 컴포넌트는 서버 사이드에서는 렌더링되지 않고 오직 클라이언트에서만 로드되며,
loading은 Suspense의 fallback 역할과 유사하다.
정리하자면 Code Splitting 하는 방법은 React.lazy 방법과 dynamic import로 두 가지로 나뉘며,
렌더링 방식에 따라 다르게 사용한다.
3. Lazy-loading (지연 로딩)
필요한 시점이 되었을 때 그때서야 리소스(이미지, 컴포넌트 등)를 불러오는 기법이다.
미리 불필요한 리소스를 모두 로드하지 않고, 사용자가 실제로 해당 콘텐츠를 볼 때(또는 필요할 때) 로드함으로써 초기 로딩 시간을 단축시키기 위해 사용한다.
3.1 사용 예시
<body>
<!-- 아직 보이지 않는 이미지도 전부 즉시 로딩 -->
<img src="large1.jpg" alt="이미지1" />
<img src="large2.jpg" alt="이미지2" />
<img src="large3.jpg" alt="이미지3" />
</body>
페이지 진입 시 모든 이미지를 한꺼번에 로딩하기 때문에 큰 이미지가 많을수록 초기 로딩이 오래 걸린다.
<body>
<!-- 이미지가 뷰포트에 근접했을 때 로드가 시작됨 -->
<img src="large1.jpg" loading="lazy" alt="이미지1" />
<img src="large2.jpg" loading="lazy" alt="이미지2" />
<img src="large3.jpg" loading="lazy" alt="이미지3" />
</body>
이렇게 이미지에 Lazy-loading을 설정하면 화면에 보이기 전까진 로딩되지 않고,
해당 이미지가 실제 화면에 노출되기 직전에 로드가 시작되어서 초기 페이지 렌더링이 훨씬 빠르다.
이미지말고 컴포넌트도 Lazy-loading을 설정하면 지연 로딩을 시킬 수 있는데,
React.lazy()와 같은 방식을 쓰면 Code Splitting + Lazy Loading이 동시 적용된다.
import React, { Suspense } from 'react';
// 라우트 단위 혹은 컴포넌트 단위로 지연 로딩
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
export default App;
HTML5 환경에서는 loading="lazy" 속성을 사용하면 되고,
React 환경에서는 React.lazy와 Suspense를 사용하면 되지만 CSR 방식에서만 사용 가능하다.
SSR 방식인 Next.js 환경에서는 Image는 next/image를 사용 시 자동으로 최적화되고,
Component는 dynamic import()를 사용해서 Lazy-loading을 적용시킬 수 있다.
4. CSS Minify (CSS 압축)
CSS 파일에서 주석, 공백, 줄바꿈 등 불필요한 CSS를 제거하여 파일 용량을 줄이는 방법이다.
CSS 파일 용량을 줄여 페이지 렌더링 속도 향상시키기 위해 사용한다.
4.1 예시
/* styles.css */
body {
margin: 0;
padding: 0;
background-color: #ffffff;
}
.container {
margin: 0 auto;
width: 80%;
padding: 20px;
}
/* 불필요한 주석과 공백이 많음 */
/* 파일이 크면 크기만큼 로딩 시간이 늘어남 */
위와 같이 작성하면 사람이 보기엔 좋지만, 용량 낭비가 발생한다.
/* styles.min.css */
body{margin:0;padding:0;background-color:#fff}.container{margin:0 auto;width:80%;padding:20px}
이렇게 변경하면 불필요한 공백, 주석, 줄바꿈이 제거되어 파일 크기가 확연히 줄어든다.
CSS Minify하기 위해서 Webpack, PostCSS(cssnano), Tailwind CSS 등의 도구를 사용하면
프로덕션 빌드시 자동으로 압축이 가능하다.
4.2 적용 방법
create-react-app(CRA) 환경과 직접 Webpack 설정을 사용하는 환경, 또는 Next.js 환경에 따라 구현 방법이 조금씩 다르다.
- CRA 환경에서 빌드 시 자동 CSS Minify 적용하는 방법
create-react-app(CRA)로 생성된 프로젝트에서
npm run build(또는 yarn build)를 실행하면 프로덕션 빌드가 이뤄지면서, Webpack의 production 모드가 활성화된다.
이때 MiniCssExtractPlugin, cssnano 등의 내부 플러그인이 자동으로 동작해서 CSS가 자동으로 Minify된다.
즉, 추가 설정 없이 npm run build만 해도 아래처럼 자바스크립트와 CSS가 모두 압축된 상태로 빌드된다.
$ npm run build
Creating an optimized production build...
Compiled successfully.
빌드 결과물(build/static/css)을 보면 파일명이 해시값이 붙고, 내용이 공백 없이 압축된 형태로 출력된다.
따라서 CRA 환경에서는 npm run build 만으로도 CSS Minify가 자동 적용된다.
- Webpack을 직접 설정하는 경우
직접 Webpack을 설정하는 환경이라면,
webpack.config.js을 설정하여 css-minimizer-webpack-plugin을 추가해서 CSS 압축을 수행할 수 있다.
// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
mode: 'production', // 꼭 'production' 모드이어야 tree shaking, minify 등이 동작
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
// 필요 시 postcss-loader 등 추가
],
},
],
},
optimization: {
minimize: true,
minimizer: [
`...`, // 기존 TerserPlugin(자바스크립트 압축) 설정 유지
new CssMinimizerPlugin(), // CSS 최소화 플러그인
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
};
MiniCssExtractPlugin는 CSS를 별도의 파일로 추출,
CssMinimizerPlugin는 CSS 소스코드를 Minify,
mode는 'production'로 설정하면 프로덕션 모드에서만 최적화 적용하는 것이기 때문에
이렇게 설정 후 webpack 빌드를 실행하면, CSS가 최적화된(minified) 형태로 번들되어 나온다.
- PostCSS 플러그인 + cssnano 활용
PostCSS는 CSS를 변환하기 위한 다양한 플러그인을 관리하는 툴이며, 그 중 cssnano가 대표적인 CSS Minify 플러그인이다. autoprefixer, tailwindcss, cssnano 등을 함께 사용할 수 있다.
// postcss.config.js
module.exports = {
plugins: {
'tailwindcss': {},
'autoprefixer': {},
// 프로덕션 환경에서만 cssnano를 실행
...(process.env.NODE_ENV === 'production'
? { cssnano: { preset: 'default' } }
: {}),
},
};
위와 같이 cssnano를 추가하면, 프로덕션 빌드시 CSS가 Minify된다.
Tailwind CSS를 쓰는 경우, Tailwind 자체도 프로덕션 빌드시 css를 purge(불필요한 CSS 제거)하고 압축한다.
NODE_ENV=production 일 때만 cssnano가 동작하도록 조건부 설정하면, 개발 중에는 가독성을 위해 압축이 적용되지 않는다.
- Next.js 환경에서의 CSS Minify
Next.js 또한 프로덕션 빌드(npm run build) 시 자동으로 CSS를 Minify한다.
내부적으로는 Webpack + PostCSS를 사용하고 있기 때문에, 별도로 설정을 추가하지 않아도 기본적으로 CSS Minify가 적용된다.
만약 PostCSS를 사용하여 커스터마이징을 한다고 하면
Next.js 프로젝트 최상단에 postcss.config.js 파일을 생성해, 위에서 소개한 cssnano 등의 플러그인을 추가할 수 있다.
next.config.js에서 future나 experimental 옵션을 설정해줄 수도 있지만, 일반적으로는 기본 PostCSS 설정만으로 충분하다.
UI 반응 속도가 느리다면?
[React] 사용자에게 빠른 UI 반응을 제공하려면? Web Worker, Debounce, Throttle
UI 응답성을 개선하기 위한 성능 최적화 기법에는 Web Worker과 Debounce, Throttle가 있다.적용 대상과 역할이 조금 다르기 때문에 이 글을 읽고, 상황에 맞게 적용해 성능을 개선해보자! 1. Web Worker (웹
mi-dairy.tistory.com
'프론트엔드 > React' 카테고리의 다른 글
[React] Zustand쓰면 Redux 이제 못 씀, Zustand 파헤치기 (0) | 2025.03.29 |
---|---|
[React] 사용자에게 빠른 UI 반응을 제공하려면? Web Worker, Debounce, Throttle (0) | 2025.03.29 |
[React] DOM?? Virtual DOM?? (1) | 2025.03.25 |
[React] 라이프사이클(Lifecycle)에 대해서 (5) | 2024.09.12 |
[React] 함수형 컴포넌트와 클래스형 컴포넌트 (1) | 2024.09.11 |