컴포넌트 또는 비동기상태의 로딩상태를 선언적으로 처리 할 Suspense와
Suspense와 함께 컴포넌트를 동적으로 import하는 방식으로 번들링 사이즈를 줄여 초기 페이지 로드 감소시킨 lazy loading을 프로젝트에 적용한 부분을 작성하려고 한다.
Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을때 로딩 화면을 보여주고 로딩이 완료되면 해당 컴포넌트를 보여주는 React에 내장되어 있는 기능이다.
Suspense를 사용하는 이유는 다음과 같다. 먼저 code spliting을 통해 필요할때 동적으로 컴포넌트를 import하는 lazy loading 방식을 Suspense와 함께 구현 할 수 있다.
또한 Suspense를 통해 컴포넌트나 비동기 관련 로직상태를 명령형 방식이 아닌 선언적인 방식으로 처리 할 수 있다.
비동기 관련 로딩상태를 처리할때, Suspense는 프로미스 기반으로 작동한다.
Suspense가 비동기를 처리하는 동작원리를 자세히 보기 위해 React 팀에서 발표한 예시코드인 wrapPromise.js를 확인해보자.
//wrapPromise.js
function wrapPromise(promise) {
let status = 'pending'
let response
const suspender = promise.then(
(res) => {
status = 'success'
response = res
},
(err) => {
status = 'error'
response = err
},
)
const read = () => {
switch (status) {
case 'pending':
throw suspender
case 'error':
throw response
default:
return response
}
}
return { read }
}
export default wrapPromise
wrapPromise는 promise를 파라미터로 받는다. 또한 Promise에서 반환되는 데이터를 읽을 준비가 되었는지 확인할 수 있는 read라는 메서드를 제공한다.
기본적으로 promise는 pending(이행대기) 상태이다. promise는 pending, fullfilled(success) , rejected(error) 3가지 상태를 갖는다.
suspender라는 변수는 파라미터로 받은 promise 결과에 따라 stats와 response를 바꾼다.
read함수는 promise의 상태가 pending이나 error라면, promise를 상위로 throw하고, 던진 promise를 받아 fallback UI를 보여준다.
만약 promise가 success 상태라면 성공한 response(data)를 return 한다.
실제 wrapPromise를 사용한 코드 예시를 보자.
//fetchData.js
import wrapPromise from './wrapPromise'
function fetchData(url) {
const promise = fetch(url)
.then((res) => res.json())
.then((res) => res.data)
return wrapPromise(promise)
}
export default fetchData
//UserWelcome.jsx
import React from 'react'
import fetchData from '../api/fetchData'
const resource = fetchData(
'https://run.mocky.io/v3/d6ac91ac-6dab-4ff0-a08e-9348d7deed51'
)
const UserWelcome = () => {
const userDetails = resource.read()
return (
<div>
<p>
Welcome <span className="user-name">{userDetails.name}</span>, here are
your Todos for today
</p>
<small>Completed todos have a line through them</small>
</div>
)
}
export default UserWelcome
fetchData라는 컴포넌트는 data를 fetching 하는 proise를 wrapPromise로 감싼 형태이다.
기본적으로 컴포넌트가 mount될때 fetchData의 read를 호출하게 된다.
비동기 로직이기 때문에, 이때 promise가 정확히 어떤 상태인지 확신 할 수 없다.
만약 promise가 success 상태라면 응답된 데이터를 return받고, promise가 pending이나 rejected된 상태라면 상위 컴포넌트로 promise를 던진다.
이후 promise가 fullfilled되는 로직은 Suspense 코드를 보면 알 수 있다.
import React from "react";
export interface SuspenseProps {
fallback: React.ReactNode;
}
interface SuspenseState {
pending: boolean;
error?: any;
}
function isPromise(i: any): i is Promise<any> {
return i && typeof i.then === "function";
}
export default class Suspense extends React.Component<
SuspenseProps,
SuspenseState
> {
public state: SuspenseState = {
pending: false
};
public componentDidCatch(catchedPromise: any) {
if (isPromise(catchedPromise)) {
this.setState({ pending: true });
catchedPromise
.then(() => {
this.setState({ pending: false });
})
.catch((err) => {
this.setState({ error: err || new Error("Suspense Error") });
});
} else {
throw catchedPromise;
}
}
public componentDidUpdate() {
if (this.state.pending && this.state.error) {
throw this.state.error;
}
}
public render() {
return this.state.pending ? this.props.fallback : this.props.children;
}
}
componentDidCatch 메소드에서 Suspense로 감싼 하위 컴포넌트에서 던진 promise를 catch한다.
던저진 초기 promise는 pending중인 상태로 간주한다.
promise 체이닝을 통해 만약 프로미스가 이행되었다면, Suspense 컴포넌트의 pending 상태를 false로 변환한다.
만약 Error가 발생했다면, Suspense Error을 상위로 던진다. 이는 Error Boundary를 통해 처리해야한다.
promise가 pending 상태라면 fallback으로 받은 컴포넌트를 render하고 fullfilled 라면 하위 컴포넌트(children)을 렌더한다.
초기에 페이지에 접속하면 웹팩으로부터 번들링 된 js 파일을 받는다.
이때 번들링에는 모든 컴포넌트가 포함되있다. 여기에 데이터 fetching까지 이뤄진다면, 사용자가 초기에 웹페이지를 보는 TTV의 시점이 매우 늦어지게 된다.
lazy loading은 코드를 분할해서 모든 컴포넌트가 번들링 된 파일을 받는것이 아니라 필요할때 동적으로 컴포넌트를 불러오는 기법이다.
컴포넌트를 동적으로 import 하려면 promise의 상태를 처리해야하기 때문에 Suspense 내부에서 사용해야한다.
CSR의 고질적인 문제점인 TTV가 일어나는 시점이 오래걸리는 문제를 해결해준다.
페이지를 동적으로 import 할때, Suspense가 이를 감지해 fallback 컴포넌트를 대신 보여준다.
import { lazy, Suspense } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import ErrorBoundary from './components/ErrorBoundary';
import Spinner from './components/Spinner';
import NotFound from './pages/NotFound/NotFound';
const Admin = lazy(() => import('./pages/Admin/Admin'));
const FoodDetail = lazy(() => import('./pages/FoodDetail/FoodDetailMain'));
const FoodList = lazy(() => import('./pages/FoodList/FoodList'));
const Login = lazy(() => import('./pages/Login/Login'));
const MainPage = lazy(() => import('./pages/MainPage/MainPage'));
const MyPage = lazy(() => import('./pages/MyPage/MyPage'));
const Register = lazy(() => import('./pages/Register/Register'));
const Router = () => {
return (
<BrowserRouter>
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/" element={<MainPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/foodList" element={<FoodList />} />
<Route path="/foodList/:id" element={<FoodDetail />} />
<Route path="/admin" element={<Admin />} />
<Route path="/mypage" element={<MyPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
};
export default Router;
lazy loading 적용 이전 데이터들을 분석해보면 다음과 같다.
번들링사이즈(Resource)는 8.7MB이고, transfer Size는 1.5 MB로 측정되었다.
resource size와 transfer size는 웹의 성능에 직접적으로 영향을 준다.
여기서 resource size는 네트워크와 관계없이 리소스를 구성하는 총 용량을 나타낸다.
transfer Size는 네트워크를 통한 전송에 필요한 총 바이트 수를 나타낸다. 일반적으로 transfer Size가 클수록 네트워크 전송에 더 오랜 시간이 걸린다.
네트워크 탭에서 밑에 DomContentLoaded, Load, Finish 라는 시간이 존재한다.
DomContentLoaded는 HTML을 파싱하여 DOM Tree 구조를 그리는데까지 걸리는 시간이다.
Load는 Dom Tree구조를 포함, 이미지까지 화면에 로드되는 시간이다. 즉 사용자가 처음 화면을 보는 시간이라고 할 수 있다.
로드까지 걸린 시간은 848ms이다.
번들링 사이즈는 4.0MB transfer Size는 797kB로 측정되었다.
로드까지 걸린시간은 626ms이다.
lazy loading 적용전과 비교해봤을때, 번들링사이즈는 약 절반가까이 줄었고(8.7 => 4.0MB), 페이지 로드 시간이 약 27% 감소했다.
웹 어플리케이션의 규모가 크면 클수록, lazy loading을 적용하면 초기 페이지 로드속도를 크게 항샹 시킬 수 있을 것이다.
ref)
React 공식문서: https://ko.reactjs.org/docs/react-api.html#reactsuspense
Suspense 동작원리 :
https://blog.logrocket.com/react-suspense-data-fetching/
https://velog.io/@seeh_h/suspense%EC%9D%98-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC#suspense-%EC%9B%90%EB%A6%AC%EC%99%80-%EC%A7%81%EC%A0%91-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0
Transfer, Resource Size : https://www.webperf.tips/tip/resource-size-vs-transfer-size/
안녕하세요. 글 잘 읽었습니다. 캡쳐해주신 네트워크 화면에서 번들링 사이즈는 어디서 확인할 수 있나요?