Lazy loading, Suspense, Code- splitting 그리고 Automatic Batching

WE SEE WHAT WE WANT·2023년 10월 4일
0

React

목록 보기
6/6

들어가기에 앞서…

여러분은 컵라면에 물 붓고 3분을 얌전히 기다리시나요? 저는 한국인이라서인지(?) 3분이 되기도 전에 몇번이나 열어보곤 하는데요. 🤣
기다리는 것을 별로 좋아하지 않는 것은 만국 공통인것같습니다.
유저의 50% 이상이 웹페이지를 로드하는데 3초 이상 걸리는 경우, 기다리지 않고 웹사이트를 이탈한다하는데요.

💥 모바일 지연 시간이 퍼블리셔 수익에 미치는 영향

How mobile latency impacts publisher revenue - Think with Google

웹사이트의 속도를 저하시키는 원인은 무엇이 있을까요? 크게는 파일의 크기, 서버 요청 수, 페이지의 어떤 요소들이 나타나는 순서에 따른 이유 등으로 얘기 할 수 있는데요.

속도 향상을 위해 우리가 할 수 있는 방법은 어떤 것이 있을까요?


1. Code-Splitting 으로 payload 줄이기

  • 사용자가 애플리케이션을 로드할 때 초기 경로에 필요한 코드만 보내도록 JavaScript 번들 분할

    ⇒ JS번들 파일의 크기를 줄여 초기 로딩 시간 단축, 필요한 코드만 로드해서 UX 향상

  • 동적 임포트(Dynamic Import)를 사용하여 구현 가능. 이를 통해 애플리케이션의 여러 부분을 나누어 개별적인 청크(chunk)로 분할 가능. 이렇게 분할된 청크는 필요한 시점에 로드 됨.

  • JavaScript 번들 파일을 분할하려면, 주로 웹팩(Webpack)과 같은 모듈 번들러를 사용합니다. 웹팩은 코드 분할을 지원하는 여러 방법을 제공합니다.

  • 예를 들어, 웹팩에서 동적 임포트를 사용하여 코드를 분할하는 방법은 다음과 같습니다:

    1. import() 함수 사용:

      • 동적 임포트를 사용하려면 import() 함수 사용. (필요한 모듈 동적으로 가져옴)
      import("./module")
        .then((module) => {
          // 모듈을 사용합니다.
        })
        .catch((error) => {
          // 에러 처리를 합니다.
        });
    2. 웹팩 설정 파일에서 설정하기:

      • 웹팩 설정 파일(webpack.config.js)에서 코드 분할을 설정 가능 optimization.splitChunks 속성을 사용하여 청크 분할 방법을 지정
      module.exports = {
        // ...
      
        optimization: {
          splitChunks: {
            chunks: 'all',
          },
        },
      };
    3. 동적 임포트를 사용한 코드 분할:

      • 애플리케이션의 특정 부분을 동적 임포트로 분할하려면 ? 아래 예시 참고
      const handleClick = async () => {
        const module = await import("./module");
        // 모듈을 사용합니다.
      };

2. 주요 Assets을 미리 로드하여 로딩속도 향상시키기

주요 Assets을 미리 로드하여 로딩속도를 향상시키려면 알아야 할 것은 2가지!

  1. 프리로딩(preloading):
    브라우저에게 특정 리소스(이미지, 스타일시트, 스크립트 등)가 필요할 것임을 미리 알려 로드를 시작하도록 지시하는 방법. 브라우저는 현재 페이지의 로딩을 방해하지 않으면서 해당 리소스를 미리 가져옴. HTML의 <link> 태그를 사용하여 프리로딩 가능.
  • 이미지를 프리로딩하는 경우:

    	   <link rel="preload" href="path/to/image.jpg" as="image">
  • 스크립트 파일을 프리로딩하는 경우:

    	    <link rel="preload" href="path/to/script.js" as="script">
  • 스타일시트를 프리로딩하는 경우:

    	 <link rel="preload" href="path/to/style.css" as="style">
  1. 프리페칭(prefetching):
    브라우저가 현재 페이지 로딩과는 관련이 없지만, 사용자가 나중에 방문할 가능성이 높은 리소스를 미리 가져오는 방법. 다음 페이지를 방문할 때 해당 리소스의 로딩 시간을 줄여줌.

HTML의 <link> 태그를 사용하여 프리페칭 가능.

  • 다음 페이지에서 사용할 스크립트 파일을 프리페칭하는 경우 :

    <link rel="prefetch" href="path/to/script.js">

  • 이미지를 프리페칭하는 경우:

    <link rel="prefetch" href="path/to/image.jpg">

  • 스타일시트를 프리페칭하는 경우:

    <link rel="prefetch" href="path/to/style.css">


⭐React 18 : 렌더링 엔진 개선 & UX 향상⭐

속도 향상을 위해 우리가 할 수 있는 방법으로 2가지를 살펴봤는데,
이제 React 에서 랜더링 엔진 개선과 UX 향상을 위해 React 18 에 내놓은 주요 기능을 살피는 시간을 가져보자!

⬛ **React 18의 핵심은 "Concurrency" 도입으로 인한 렌더링 엔진 개선과 UX 향상!!!**

✨ Suspense

React 16.6에서 실험적인 기능이었는데, React 18에서 FINALLY 정식 기능으로 출시 !! 🥳🥳🥳

  1. Suspense가 뭔데 ?

컴포넌트를 읽어야 하는데 데이터가 아직 준비가 되지 않았다고 리액트에게 알려주는 새로운 매커니즘. 데이터 로딩, 코드 분할, 서버 사이드 렌더링 등 다양한 비동기 작업을 더욱 간편하게 처리 가능!

Suspense의 주요 기능

  1. 로딩 상태 관리 : 컴포넌트의 로딩 상태를 관리. 비동기 작업이 진행되는 동안 로딩 상태를 표시하고, 작업이 완료되면 자동으로 해당 컴포넌트를 렌더링

  2. 지연된 로딩 : 지연된 로딩 구현 가능. 컴포넌트가 필요한 데이터나 리소스를 동적으로 로딩하고, 로딩이 완료되기 전까지 로딩 상태를 표시. 초기 로딩 시간을 최적화 + 필요한 부분만 렌더링

  3. 에러 처리: 비동기 작업 중 발생하는 에러 처리 기능을 제공. 비동기 작업이 실패하면 에러 처리를 위한 대체 컴포넌트나 에러 메시지를 보여줄 수 있음.

  4. suspense for Data Fetching :

    컴포넌트에서는 비동기적인 리소스를 선언 + 그 값을 읽어온다고 선언하기만 함.
    그러면 실제로 로딩상태나 에러상태 처리는 컴포넌트를 감싸는 부모컴포넌트가 대신해줌

    ⇒ 이렇게 어떤 코드 조각을 감싸는 맥락으로 책임을 분리하는 방식을 대수적 효과(Algebraic Effects)라고 함.

Suspense를 왜 반기는 걸까?

그동안 비동기 처리는 React를 쓰는 개발자들에게 가장 까다로운 문제 중 하나였는데,,,

  • 일단 요청이 성공하는 경우에만 집중해 컴포넌트를 구성하기 어려움
  • 비동기 로직이 많아질 수록 비즈니스 로직을 파악하기 점점 어려워짐 ⇒ Suspense를 통해 비동기를 보다 우아하게 작성할 수 있게 됨! (주요기능과 거의 동일)
    • 코드의 간결성
    • 지연된 로딩과 코드분할을 쉽게 구현
    • 초기 로딩시간 최적화
    • 필요한 부분만 랜더링
    • 에러 처리 방식 개선

사용 시 주의할 점

  • 네트워크 요청이 실패할 경우를 대비해 Error 처리를 추가해야 하는 것!
  • Error Boundary 설정 : 비동기 작업 중 발생하는 에러에 대한 처리를 명확히 구현해야 함.
  • 네트워크 대기 시간이 너무 길어지면 UX 가 오히려 저하될 수 있음.
  • 비동기 작업 관리: 작업의 중복 실행이나 관리를 고려해야 함. 필요에 따라 캐싱이나 작업 중단을 구현.
  1. 예시 코드
import React, { Suspense } from 'react';
// 비동기로 로딩되는 컴포넌트
const LazyComponent = React.lazy(() => import('./LazyComponent'));
// 로딩 중에 표시할 컴포넌트
const LoadingSpinner = () => <div>Loading...</div>;

const App = () => {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<LoadingSpinner />}>
<LazyComponent />
</Suspense>
</div>
);
};

export default App;
  • React.lazy() 함수를 사용하여 LazyComponent를 비동기로 로딩
  • Suspense 컴포넌트로 LazyComponent를 감싸서 로딩 상태를 관리
  • fallback prop은 로딩 중에 표시할 대체 컴포넌트 지정( LoadingSpinner 컴포넌트)
  • LazyComponent가 실제로 필요할 때까지 로딩을 지연시키고, 로딩이 완료되지 않은 동안 LoadingSpinner를 보여줌. 이를 통해 초기 로딩 시간을 최적화하고 필요한 컴포넌트만 렌더링할 수 있습니다.

Suspense를 사용하여 Skeleton UI를 구현하는 예시:

import React, { Suspense } from 'react';

// 비동기로 로딩되는 컴포넌트
const LazyComponent = React.lazy(() => import('./LazyComponent'));

// 로딩 중에 표시할 Skeleton UI 컴포넌트
const SkeletonUI = () => (
  <div style={{ background: '#f0f0f0', width: '200px', height: '100px' }}></div>
);

const App = () => {
  return (
    <div>
      <h1>My App</h1>
      <Suspense fallback={<SkeletonUI />}>
        <LazyComponent />
      </Suspense>
    </div>
  );
};

export default App;

React.lazy

✅(오늘 토의에서 내가 잘못알고있던 부분)

React.lazy와 Suspense 모두 코드 스플리팅을 쉽게 구현하고 애플리케이션의 성능을 개선하기 위해 사용되지만 각각 다른 목적과 사용 방법을 가지고 있음!!

  • React.lazy:
    • React.lazy는 동적으로 로드되어야 하는 컴포넌트를 정의할 때 사용됨
    • 애플리케이션의 번들 파일을 작은 청크(chunk)로 분할+ 필요한 시점에 필요한 모듈만 로드
    • React.lazy 함수는 Promise를 반환하는 함수를 인자로 받음. (동적으로 로드되는 컴포넌트를 반환)
    • 예시 (아래)
      const MyComponent = React.lazy(() => import('./MyComponent'));
    • React.lazy를 사용하면 필요한 컴포넌트가 실제로 사용되기 전까지는 로드되지 XX
    • 첫 번째 렌더링 시에는 Suspense 컴포넌트로 감싸지 않고 사용할 수 있음.
  • 요약하자면 React.lazy : 동적으로 로드되어야 하는 컴포넌트를 정의하는 데 사용되며, 코드분할과 초기 로딩 시간 개선에 중점을 둡니다. Suspense : React.lazy로 동적으로 로드되는 컴포넌트의 로딩 상태를 처리하고, 로딩 중에 보여줄 UI를 설정하는 데 사용. ⇒ 두 기능을 함께 사용하여 애플리케이션의 성능과 사용자 경험을 개선할 수 있습니다.

✅ +) lazy Loading 과 SEO 의 관계성, 작동원리 (금일 토의 중 나눈 얘기)

https://ideadigitalagency.net/blog/lazy-loading-seo/


Suspense 랑 React.lazy는 왜 같이 쓰는건데?

SuspenseReact.lazy()를 함께 사용하는 이유는 코드 분할과 비동기 컴포넌트 로딩을 효율적으로 관리하고, 사용자 경험을 개선하기 위함.

주요 이유:

  1. 초기 번들 크기 감소: React.lazy()는 컴포넌트를 동적으로 로딩하여 초기 번들 크기를 줄일 수 있음. 필요한 컴포넌트가 사용되기 전까지 해당 컴포넌트는 번들에 포함x, 별도의 작은 번들로 분리되어 로드됨.

    ⇒ 초기 로딩 속도를 향상시키고 애플리케이션의 성능을 개선!

  2. 지연된 컴포넌트 로딩: React.lazy()로 동적으로 로딩된 컴포넌트를 Suspense 컴포넌트 내에서 사용하면, 로딩 중에 대체 컨텐츠나 로딩 스피너를 표시할 수 있습니다. 이는 사용자에게 로딩 상태를 시각적으로 전달하여 사용자 경험을 향상시키는 데 도움을 줍니다.

  3. 모듈 단위 코드 분할: React.lazy()를 통해 컴포넌트를 모듈 단위로 분할 가능.

    즉, 컴포넌트가 개별적으로 + 필요한 시점에 로딩 ⇒ 로딩 속도 향상 + 불필요한 자원낭비x

  • React.lazy를 단독으로 사용했을 때:

    • Code Splitting에 대한 이점이 있음.
    • BUT, 로딩 상태 처리에 대한 기능이 없기 때문에 컴포넌트 로딩 상태 처리를 위한 별도의 로딩 UI 구현이 필요함
  • Suspense를 단독으로 사용했을 때:

    • Code Splitting이 불가능 (컴포넌트의 번들 파일을 작은 청크로 분할하여 필요한 시점에 필요한 모듈만 로드하는 기능을 활용할 수 없음 ⇒ 초기 로딩 시간 늘어남)
    • 사용 범위가 제한됨 (다른 비동기 작업, 예를 들어 데이터 요청이나 파일 로딩과 같은 작업에 대한 로딩 상태 처리에는 Suspense를 사용할 수 없음)
    • 사용자 정의 로딩 UI의 유연성이 제한됨
  • 함께 사용했을 때:

    • Code Splitting과 로딩 상태 처리를 한꺼번에 처리 가능
    • React.lazy로 동적 로드되는 컴포넌트를 정의 + Suspense로 해당하는 컴포넌트의 로딩 중 상태를 처리
    • ⇒ 별도의 로딩 UI를 구현할 필요 없어 간편하다!!

✨ Automatic Batching

리액트의 렌더링 최적화를 위한 신규 업데이트는! 자동배칭(Automatic Batching) 🎉 🎉

ReactDOM.createRoot 메서드를 기반으로 렌더링 할 경우 state update 작업은 자동으로 Batching 처리 되는 것 . 이것을 자동배정이라고 함.

이전에는 자동배칭이 없었다고 생각하실 수 있는데, 그렇지 않음. (다만 단점이 존재했을뿐..)

  • 단점이 뭔지 궁금하다면..?
    • 브라우저의 이벤트가 실행되는 중에만 Batching 작업을 수행 → 이벤트가 종료된 후에 실행되는 경우는 작업 불가!
    • 이벤트 핸들러 내부의 state update 작업에 대해서만 Batching 이 가능(Promise나 setTimeout, Native Event Handler 내부 작업 불가)

우선 Batching에 대해 간략하게 설명하자면,

  • state의 업데이트 작업을 일괄 묶어서 변경하는 작업을 처리하는 방식.
  • 즉, 리액트에서는 state 들의 업데이트를 하나의 리랜더링으로 일괄 묶는 것을 의미
  • 불필요한 리랜더링을 방지할 수 있음!
const handleClick = () => {
  setState1(newValue1);
  setState2(newValue2);
  setState3(newValue3);
};
  • handleClick 함수가 3번 실행되었다고 생각할 수 있지만, 실제 리렌더링은 한번만 발생.
  • 여러 번의 state 업데이트 작업을 큐에 몰아넣고 일정 주기마다 큐에 등록된 작업을 순차적으로 일괄 시행하면서 불필요한 리렌더링을 방지.

장점과 단점, 그리고 주의할 점**

장점:

  1. 성능 향상: 여러 setState() 호출을 일괄 처리하여 중복된 렌더링을 줄임 ⇒ 성능향상 + 불필요한 렌더링이 최소화.
  2. 코드 간결성: 개발자가 일일이 일괄 처리를 관리할 필요가 없음.
  3. 예측 가능한 동작: 이벤트 처리의 동작이 일관되고 예측 가능해짐. setState() 호출이 일괄 처리되므로 컴포넌트의 업데이트가 예상한 대로 동작할 것으로 예상

단점:

  1. 배치 크기의 한계 : 한 번에 처리할 수 있는 일괄 배치 크기에는 제한이 있음. 규모가 큰 배치가 발생하거나, 무한 루프가 있는 경우 일괄 처리가 제대로 이루어지지 않을 가능성 있음.
  2. 비동기 작업과의 상호작용: 대부분의 동기적인 setState() 호출에 적용되나,,, 비동기 작업 내에서의 setState() 호출은 자동으로 일괄 처리되지 않을 수 있음. (수동처리필요)

주의할 점:

  1. 일괄 처리 유지 : setState() 호출을 순차적으로 작성하면 자동으로 일괄 처리됨. 따라서 중간에 다른 코드 또는 비동기 작업이 있는 경우에도 일괄 처리가 유지되도록 주의.
  2. shouldComponentUpdate()와의 함께 사용 시 주의: shouldComponentUpdate() 메서드를 사용하여 컴포넌트의 업데이트를 조건부로 제어하는 경우, 일괄 처리되는 방식에 영향을 줄 수 있음.

참고링크

https://web.dev/reduce-javascript-payloads-with-code-splitting/

https://www.thinkwithgoogle.com/intl/en-154/marketing-strategies/app-and-mobile/need-mobile-speed-how-mobile-latency-impacts-publisher-revenue/

https://web.dev/i18n/ja/preload-critical-assets/

https://blog.mathpresso.com/conceptual-model-of-react-suspense-a7454273f82e

https://youtu.be/FvRtoViujGg

profile
프론트엔드 주니어입니다. 그런데 서비스 기획을 곁들인,,,

0개의 댓글