성능의 향상을 위해서는 코드를 잘 처리해야 합니다.
내가 작성한 코드들을 전달할 때 모아서 한꺼번에 처리를 하는게 좋을까요, 아니면 쪼개서 작은 단위로 처리하는 것이 좋을까요?
batching은 react의 성능 향상을 위해 여러 개의 state 업데이트를 한 번의 re-rendering으로 묶어서 진행하는 것을 말합니다.
이를 통해 불필요한 render 함수의 호출을 방지하여 렌더링 성능을 향상시킬 수 있습니다.
예를 들어 문서를 출력할 때 낱장으로 하는 것보다 한꺼번에 출력하는 것이 시간과 과정을 단축시키는 것과 같이 리액트의 batching도 이와 비슷한 원리로 동작합니다.
👨🍳 더 재미있는 예시로는
레스토랑에서 종업원이 손님이 주문하는 첫번째 메뉴를 듣자마자 주방으로 달려가지 않듯이 Batching은 반드시 필요한 하나의 리렌더링을 수행한다.
가 있을 것 같습니다.
React 18 버전 이하에서는 리액트의 이벤트 핸들러 내부의 state update 작업에 대해서만 batching이 가능했습니다. 브라우저의 이벤트가 실행되는 중에만 batching을 수행했기 때문에 이벤트가 종료된 후에 실행되는 경우는 batching이 불가능했습니다.
// Before React 18 only React events were batched
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will render twice, once for each state update (no batching)
}, 1000);
하지만 React 18 버전 이후부터는 18 이전에 포함되지 않았던 작업에 대해서도 batching을 자동으로 수행될 수 있도록 변경되었습니다.
// After React 18 updates inside of timeouts, promises,
// native event handlers or any other event are batched.
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}, 1000);
React18에서 제공하는 ReactDOM.createRoot
메서드를 기반으로 렌더링을 진행할 경우 모든 state update 작업은 자동으로 batching 처리되는데 이 기능을 Automatic Batching 이라고 합니다.
// before React18
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// after React18
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
react의 예시 말고 다른 걸 예시로 들자면...
// before react18
import { useEffect } from 'react';
const UserList = ({ userIds }) => {
useEffect(() => {
// 일반적인 방법: 각각의 사용자 정보를 순차적으로 로드하고 처리
userIds.forEach(userId => {
const user = loadUserInfo(userId);
processUserInfo(user);
});
}, [userIds]);
return <div>User List</div>;
};
위 코드에서는 useEffect를 사용하여 컴포넌트가 렌더링될 때마다 각 사용자의 정보를 순차적으로 로드하고 처리합니다.
아래 코드에서는 useEffect 내에서 Automatic Batching을 구현하여 정의된 batchSize에 따라 자동으로 데이터를 묶어주고 각 batch에 대해 작업을 수행합니다. 이렇게 하면 자동으로 batching이 이루어지므로 코드가 간결해지고 성능 향상이 가능합니다.
// after react18
import { useEffect } from 'react';
const UserList = ({ userIds }) => {
useEffect(() => {
// Automatic Batching을 사용한 경우
const batchSize = 3;
for (let i = 0; i < userIds.length; i += batchSize) {
const batch = userIds.slice(i, i + batchSize);
const users = batch.map(userId => loadUserInfo(userId));
processUserInfos(users);
}
}, [userIds]);
return <div>User List</div>;
};
batchSize는 한 번에 처리할 데이터의 묶음 크기를 나타냅니다. Automatic Batching에서 사용되는 개념으로, 데이터를 미리 정의된 크기의 작은 묶음으로 나누어 처리함으로써 성능을 향상시킬 수 있습니다.
react-dom 라이브러리에 추가된 ReactDOM.flushSync()
메서드는 Auto Batching 을 무시하고 즉시 DOM을 렌더링해줍니다.
React에서는 공식적으로 해당 메서드의 사용을 추천하진 않으며 (de-opt case), 필요한 상황이 있을 경우에만 사용할 것을 강조했습니다.
import { flushSync } from "react-dom";
// handleClick 시 총 두 번의 리렌더링을 수행
function handleClick() {
// flushSync 메서드가 실행되는 즉시 DOM을 업데이트
flushSync(() => setCounter((c) => c + 1));
flushSync(() => setFlag((f) => !f));
}
SPA(Single Page Application)의 특성상 첫 페이지 진입 시 웹팩(webpack)에서 압축한 bundle file을 다운받게 됩니다.
즉, 전체 리소스를 한번에 다운받게 되므로 결과적으로 client는 전체 리소스의 bundle file 을 다운받기 전에는 화면을 볼 수 없습니다. 이는 인터넷 속도가 빠른 환경에서는 큰 차이를 느낄 수 없지만 인터넷 환경이 느린 경우에 사용자 경험을 저하시키는 요인이 될 수 있으므로 사이트의 상황에 따라 신경을 써줘야 하는 경우가 생길 수 있습니다.
코드분할(code splitting)을 통해 위 문제를 해결할 수 있습니다.
코드분할을 하면 필요한 값들을 미리 제공하는 데 필요한 최소한의 코드를 전송할 수 있어 페이지의 로드 시간 단축을 할 수 있습니다. 당장 필요하지 않은 나머지 값들은 요청 시 로드할 수 있습니다.
코드 양을 줄이지 않고도 사용자가 필요하지 않은 코드를 불러오지 않게 적절한 사이즈의 코드가 적절한 타이밍에 동적으로 load되도록 해줍니다.
코드 분할은 react.lazy
와 suspense
로 구현할 수 있습니다.
웹페이지에는 데이터 사용량과 페이지 로드 속도에 영향을 미치는 많은 이미지가 포함되어 있는 경우가 많습니다. lazy loading은 페이지를 읽어들이는 시점에 중요하지 않은 리소스 로딩을 추 후에 하는 기술 입니다. 대신에 이 중요하지 않은 리소스들은 필요할 때 로드가 되어야 합니다.
lazy loading을 사용하면 페이지가 placeholder 콘텐츠로 작성되며, 사용자가 필요할 때만 실제 콘텐츠로 대체 됩니다.
1️⃣ 글꼴
본적으로 글꼴 요청은 렌더링 트리가 구성될 때까지 지연되므로 텍스트 렌더링이 지연될 수 있습니다.
<link rel="preload">
, CSS 글꼴 표시 속성, 글꼴 로딩 API 등을 사용하여 기본 동작을 재정의하고 웹 글꼴 리소스를 미리 load할 수 있습니다.
2️⃣ 이미지와 동영상
<img>
과 <iframe>
의 loading 속성을 사용하면 사용자가 화면 근처에 스크롤할 때까지 화면 밖에 있는 이미지/동영상의 load를 연기하도록 브라우저에 지시할 수 있습니다. 이를 통해 중요하지 않은 리소스는 필요한 경우에만 로드할 수 있으므로 잠재적으로 초기 페이지 로드 속도가 빨라지고 네트워크 사용량이 줄어듭니다.
<img loading="lazy" src="image.jpg" alt="..." />
<iframe loading="lazy" src="video-player.html" title="..."></iframe>
이미지가 많은 사이트에서 자주 사용되며 unsplash.com을 예시로 들 수 있습니다. 페이지의 해당 부분을 스크롤하면 placeholder image가 full-res 사진으로 대체됩니다.
일반적인 로직은 이와 같습니다.
1) 이미지 태그에 src값을 넣지 않고 data-src와 같은 data 어트리 뷰트를 사용하여 해당 data 어트리 뷰트에 실제 로드할 이미지 주소를 기입합니다.
2) 이미지들을 모두 읽어서 객체에 담습니다.
3) 해당 이미지가 로드가 완료되면(onload) data-src(data 어트리뷰트)에 있는 주소 값을 src값으로 셋팅을 하고 data-src어트리 뷰트는 삭제합니다.
3️⃣ component
React.lazy 함수와 Suspense 컴포넌트를 사용하여 컴포넌트를 지연 로딩하는 방법은 아래와 같습니다.
먼저, MyLazyComponent.js라는 파일을 생성하고 지연 로딩할 컴포넌트를 정의합니다.
// MyLazyComponent.js
import React from 'react';
const MyLazyComponent = () => {
return (
<div>
<p>This is a lazily loaded component!</p>
</div>
);
};
export default MyLazyComponent;
그런 다음, 메인 애플리케이션 파일에서 React.lazy를 사용하여 위에서 만든 MyLazyComponent를 지연 로딩합니다.
// App.js
import React, { lazy, Suspense } from 'react';
// MyLazyComponent를 React.lazy를 사용하여 지연 로딩
const MyLazyComponent = lazy(() => import('./MyLazyComponent'));
const App = () => {
return (
<div>
<h1>Lazy Loading Example</h1>
{/* Suspense 컴포넌트를 사용하여 로딩 중에 표시할 내용 정의 */}
<Suspense fallback={<div>Loading...</div>}>
{/* 지연 로딩된 컴포넌트 렌더링 */}
<MyLazyComponent />
</Suspense>
</div>
);
};
export default App;
이제 App.js 파일에서 MyLazyComponent를 가져올 때 React.lazy를 사용하여 동적으로 로딩합니다. Suspense 컴포넌트는 해당 컴포넌트가 로딩되는 동안 표시할 내용을 정의합니다.
이 예제에서는 "Loading..." 텍스트를 사용했지만, 실제 프로덕션 환경에서는 스피너나 로딩 애니메이션 혹은 직접 제작한 loading 페이지 등으로 대체하는 것을 권장합니다.
최신 브라우저는 intersection observer api를 통해 요소가 화면에 표시 되는 것을 검사하는 작업을 보다 효율적이고 수행할 수 있습니다.
(일부 브라우저에서는 Intersetion observer가 지원되지 않습니다. 지원되지 않는 브라우저에서는 비교적 성능은 떨어지지만 호환성을 더 높은 scroll, resize 이벤트 핸들러 사용이 가능합니다.
Intersection event observer는 요소가 화면에 표시하는 것을 계산하는 코드를 작성하는 대신 관찰자를 등록하기만 하면 되기 때문에 다양한 이벤트 핸들러에 의존하는 코드보다 사용하고 읽는 것이 더 쉽습니다.
두 기술은 각각의 상황에서 성능 향상을 위해 사용되며, Lazy Loading은 초기 로딩 속도를 개선하고, Automatic Batching은 불필요한 렌더링을 방지하여 React 애플리케이션의 효율성을 높이는 데 기여합니다.
lazy loading
대규모 애플리케이션에서 초기 로딩 속도를 최적화하려는 경우
특정 페이지나 컴포넌트가 사용자의 상호작용에 따라 동적으로 로딩되어야 하는 경우
[사례]
라우팅을 통해 페이지 간 전환 시 필요한 컴포넌트를 지연 로딩
특정 이벤트에 응답하여 필요한 기능을 동적으로 로딩
automatic batching
여러 개의 상태 업데이트가 발생하는 상황에서 최적의 렌더링 성능을 추구할 때
다수의 UI 업데이트를 특정 프레임에 일괄 처리하여 렌더링을 최소화하고 성능을 향상시킬 때
[사례]
이벤트 핸들러에서 여러 번의 setState 호출이 있는 경우, 자동으로 이를 일괄 처리하여 최적화
Together?
대규모의 복잡한 React 애플리케이션에서는 두 가지가 함께 사용될 수도 있습니다.
1️⃣ 페이지 기반 지연 로딩
특정 페이지로 이동할 때 해당 페이지의 컴포넌트를 지연 로딩하여 초기 로딩 속도를 향상시키는 경우가 많습니다.
예를 들어, 사용자가 특정 페이지에 접근했을 때 해당 페이지의 기능을 로드하도록 Lazy Loading을 적용할 수 있습니다.
2️⃣ 대규모 어플리케이션에서의 자동 일괄 처리
대규모 애플리케이션에서는 많은 수의 상태 업데이트가 발생할 수 있습니다. 이때 자동 일괄 처리를 통해 불필요한 렌더링을 최소화하고 성능을 향상시킬 수 있습니다.
여러 상태를 업데이트하는 경우, 일괄 처리를 통해 한 번의 렌더링만 발생하도록 할 수 있습니다.
3️⃣ 컴포넌트 기반 로딩
특정 컴포넌트가 사용자 상호작용에 의해 동적으로 로딩되어야 하는 경우가 있습니다.
이런 상황에서는 지연 로딩과 함께 해당 컴포넌트의 로딩 시점을 최적화하여 필요한 시점에만 로드할 수 있습니다.
4️⃣ 성능 최적화의 필요성
특히 모바일 환경이나 느린 네트워크 상황에서는 초기 로딩 속도와 성능 최적화에 대한 요구가 더 높아집니다.
Lazy Loading과 Batching은 이러한 상황에서 특히 유용하게 적용될 수 있습니다.
import React, { lazy, Suspense, useState } from 'react';
// Lazy Loading을 적용한 컴포넌트
const LazyComponent = lazy(() => import('./LazyComponent'));
const App = () => {
// 여러 상태 업데이트를 일괄 처리하기 위한 상태
const [state1, setState1] = useState('');
const [state2, setState2] = useState('');
const handleClick = () => {
// 여러 상태를 업데이트하고 일괄 처리됨
setState1('New State 1');
setState2('New State 2');
};
return (
<div>
<h1>Lazy Loading and Batching Example</h1>
{/* 지연 로딩된 컴포넌트를 Suspense로 감싸서 로딩 중에 표시할 내용 설정 */}
<Suspense fallback={<div>Loading...</div>}>
{/* Lazy Loading된 컴포넌트 렌더링 */}
<LazyComponent />
</Suspense>
<button onClick={handleClick}>Update States</button>
<p>State 1: {state1}</p>
<p>State 2: {state2}</p>
</div>
);
};
export default App;
결과적으로 어떤 기술을 사용할지는 애플리케이션의 요구사항과 구조에 따라 다르며, 성능 최적화를 위해 두 기술을 조화롭게 사용하는 것이 중요할 것입니다.
references.
officials'
https://react.dev/blog/2022/03/29/react-v18
https://react.dev/blog/2022/03/08/react-18-upgrade-guide#automatic-batching
https://react.dev/reference/react/lazy#lazy
https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading
developer's
https://nukw0n-dev.tistory.com/33
https://hwani.dev/react-automatic-batching/
https://velog.io/@rookieand/React-18%EC%97%90%EC%84%9C-%EC%B6%94%EA%B0%80%EB%90%9C-Auto-Batching-%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
https://hwani.dev/react-code-splitting/
https://hwani.dev/react-real-code-splitting/