안녕하세요!
해당 프로젝트에서 Vite를 사용하였는데요.
간단하게 코드 스플리팅을 활용하여 성능 최적화를 진행해보았습니다 .
서론
자바스크립트로 웹 개발을 하신 분이라면 여러개 JavaScript 파일을 만들고 개발하신 경험이 있으실텐데요.
만약 이렇게 여러 개의 자바스크립트 파일을 보내게 되면 비효율적일 수 있습니다. 이런 이유로 자바스크립트 프로젝트에서는 모든 코드를 하나의 번들로 묶어서 만듭니다. 물론 작은 프로젝트는 번들 크기가 작지만, 큰 프로젝트의 경우 번들 크기가 커지게 됩니다. 리액트처럼 SPA는 첫 로딩 시 많은 작업이 필요합니다. 이때 큰 파일이 로드되면 성능에 문제가 발생할 수 있습니다.
그래서 점진적 렌더링이 필요합니다. 즉, 필요한 부분을 우선순위에 따라 로딩하여 최대한 UX에 문제가 발생하지 않도록 하는 것입니다. 예로는 Lazy Loading, 코드 스플리팅 등이 있습니다.
저는 여기서 자바스크립트 파일을 필요한 모듈만 가져오도록 하는 코드 스플리팅에 대해 이야기하려 합니다.
코드 스플리팅
코드 스플리팅은 주로 3가지 이유로 분할합니다.
1. 라우터 단위
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading, please wait...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</Suspense>
</Router>
);
export default App;
라우터마다 하나의 컴포넌트로 관리하고 있다면, 동적 임포트를 사용하여 분리된 파일을 비동기적으로 가져올 수 있습니다.
2. 컴포넌트 단위
import React, { useState, Suspense, lazy } from 'react';
// 모달 컴포넌트 동적 임포트
const Modal = lazy(() => import('./Modal'));
const EmailPage = () => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>새 메일 작성</button>
{showModal && (
<Suspense fallback={<div>Loading...</div>}>
<Modal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
);
};
export default EmailPage;
이처럼 모달처럼 현재 페이지에는 보이지 않지만 눌러야지 보이는 컴포넌트들이 있습니다. 이 때 모달 컴포넌트를 동적으로 import하면 됩니다.
3. 하나의 페이지를 스플리팅 하기
페이지가 긴 경우 뷰포트에 보이는 기능만 먼저 불러오고 그 뒷부분을 컴포넌트로 만들어 스플리팅을 할 수 있습니다.
라우터 기반 코드 스플리팅
저 같은 경우 라우터 기반으로 코드 스플리팅을 구현하였습니다.
제 프로젝트는 vanilla JS 기반이기 때문에 Dynamic Import 기법을 사용하였습니다.
Dynamic Import로 불러온 데이터는 Promise를 반환하게 되는데요. 이 Promise를 throw해서 관심사를 분리합니다.
코드를 통해 살펴보겠습니다.
browserRouter() {
const path = window.location.pathname;
if (path === "/") {
throw import("../components/search/SearchView");
} else if (path === "/ticket") {
throw import("../components/ticket/TicketView");
}
}
해당 코드에서 동적으로 import한 결과를 throw하여 상위에서 이를 캐치하도록 구성했습니다.
물론 현재 코드 내부에서 직접 처리할 수도 있지만, 그보다는 Suspense와 유사한 기능을 분리해 구현하기 위해 throw 방식으로 구성한 것입니다
이 코드는 Suspense 코드입니다.
export default class Suspense extends View<null> {
BrowserRoute: any;
constructor(browserRoute) {
super(null);
this.BrowserRoute = browserRoute;
this.setState("state", "pending");
this.setState("view", html` <div>pending...</div> `);
try {
this.BrowserRoute();
} catch (promise) {
promise.then((module) => {
this.setState("state", "fullfilled");
const { SearchView, TicketView } = module;
if (SearchView) this.setState("view", new SearchView());
else this.setState("view", new TicketView());
});
}
}
override template() {
const state = this.getState("state");
const view = this.getState("view");
switch (state) {
case "pending":
return html`${view}`;
case "fullfilled":
return html`${view}`;
default:
return html`<div>error</div>`;
}
}
}
하위 컴포넌트에서 throw를 발생시키기 때문에, 상위 Suspense 컴포넌트에서는 이를 try-catch 구문을 통해 캐치해야 합니다.
그러면 then 함수를 호출합니다. 동적으로 import한 JS 파일이 로딩을 완료하면, 해당 Promise는 fulfilled 상태가 되고 then 함수가 호출됩니다.
이때 결과를 내부 상태(state)에 저장하여 다시 렌더링을 트리거합니다.
즉, pending 상태에서는 로딩 컴포넌트를, fulfilled 상태에서는 동적으로 import한 컴포넌트를, 그 외의 경우에는 에러 컴포넌트를 출력하도록 구성할 수 있습니다.
그 결과, React.lazy처럼 dynamic import를 활용하여 코드 스플리팅을 구현하는 기능을 직접 구성할 수 있었습니다.
결과
build 후 결과를 한번 살펴보겠습니다.
searchView 라우터입니다. searchView 라우터에 필요한 JavaScript 파일만 동적으로 import 된것을 볼 수 있습니다.
이번에는 ticketView 라우터로 가보겠습니다.
추가적으로, ticketView 라우터에 필요한 JavaScript 파일만 동적으로 import된 것을 확인할 수 있습니다. 이로 인해, 불필요한 코드가 로드되지 않으며, 총 113 바이트가 줄어든 것을 확인할 수 있습니다.
감사합니다.
'project > reactify' 카테고리의 다른 글
이벤트 위임, 메모리 누수 해제 (0) | 2025.05.12 |
---|---|
Browser Router와 컴포넌트 메모이제이션 (0) | 2025.02.18 |
Auto Batching, 전역 상태 관리 (0) | 2025.02.18 |
태그 리터럴을 활용한 JSX 유사 컴포넌트 (0) | 2025.02.18 |