On a couch

[React] Lazy와 Suspense를 이용한 코드 분할 본문

프론트엔드 공부/React

[React] Lazy와 Suspense를 이용한 코드 분할

couch 2023. 2. 19. 19:38

레퍼런스

[리액트] 코드 분할(코드 스플리팅) - React lazy, Suspense, Loadable Components

React Suspense 소개 (feat. React v18)

Speed Up Your React Apps With Code Splitting

 

 

코드 분할이 필요한 이유


브라우저에서 자바스크립트를 실행할 때, 각 기능에 대한 스크립트를 전부 읽어온다. 이 스크립트가 많아지면 네트워크 비용도 높아지고 병목현상이 생기게 된다.

따라서 자바스크립트로 작성된 모듈(js+css+img 등 dependancy가 있는 자원들 모두)을 하나의 번들파일로 만들고 tree-shaking, minify 등의 작업을 통해 사이즈를 줄이는 노력을 한다.

 

단, 하나의 번들파일로 압축한다고 하더라도 만약 앱의 규모가 커진다면 번들도 함께 커지게 된다.

당장 필요하지 않은 컴포넌트 정보도 첫 로딩 시 함께 불러오게 되므로 로딩 시간과 트래핑이 많아진다.

따라서 당장 필요하지 않은 것들은 별도의 번들로 분할해 두었다가 필요할 때 불러오는 방식이 코드 분할(code splitting)이다.

 

리액트의 코드 분할 (1) State를 이용한 동적 import


어떤 버튼을 눌렀을 때 해당 페이지의 내용이 뜨게 하고 싶다면

  1. 보여줄 내용을 Posts.js에 작성하고 export한 다음
  2. 사용할 곳의 useEffect 훅 안에서 다음과 같이 동적으로 import하고 state에 저장해 사용할 수 있다.
  3. 개발자도구의 네트워크 탭에서 확인해 보면 버튼을 클릭했을 때 xxx.chunk.js 와 같은 이름의 청크 파일이 추가로 불러와지는 것을 알 수 있다.
// 예제 출처 www.daleseo.com

import { useState, useEffect } from "react";
import Posts from "./Posts";

function User({ userId }) {
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState([]);

  useEffect(() => {
		// 동적으로 import 된 모듈은 promise 형태를 리턴한다
    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then((response) => response.json())
      .then((user) => {
        setTimeout(() => {
          setUser(user);
          setLoading(false);
        }, 3000);
      });
  });

	// 로딩 중일 때는 아래의 UI를 렌더링하고
  if (loading) return <p>사용자 정보 로딩중...</p>;
	// 로딩이 끝나면 아래의 UI에 데이터를 내려주어 렌더링한다.
  return (
    <div>
      <p>{user.name} 님이 작성한 글</p>
      <Posts userId={userId} />
    </div>
  );
}

export default User;

 

React에서 이처럼 비동기 데이터를 읽어오는 컴포넌트를 작성하면 몇가지 고질적인 문제가 발생한다.

 

1. 우선 상위 컴포넌트의 데이터 로딩이 끝나야 하위 컴포넌트의 데이터 로딩이 시작될 수 있기 때문에, 최종 사용자(end user) 경험 측면에서 UI가 마치 폭포(waterfall)처럼 순차적으로 나타나는 현상이 나타날 수 있다.

 

2. 또, 이렇게 초기 랜더링 후 비동기 통신으로 받은 데이터로 다시 랜더링을 수행하는 경우, 요청한 순서대로 데이터가 응답된다는 보장이 없기 때문에 의도치 않게 싱크가 맞지 않은 데이터를 제공할 수도 있다.

 

3. 마지막으로 개발 측면에서도 이렇게 if 조건문을 사용하여 어떤 컴포넌트를 보여줄지를 제어하는 것은 명령형(imperative) 코드에 가깝기 때문에 선언적(declarative) 코드를 지향하는 React의 기본 방향성과 맞지 않는다. 기본적으로 데이터 로딩과 UI 랜더링이라는 두 가지 전혀 다른 목표가 하나의 컴포넌트 안에 커플링(coupling)되어 코드가 읽기가 어려워지고 테스트를 작성하기도 힘들어 진다.

 

리액트의 코드 분할 (2)React.lazy와 Suspense 이용


React.lazy와 Suspense를 사용하면 (1)번 방법의 단점을 커버하면서 더욱 간편하게 코드 분할을 할 수 있다.

 

✨ React.lazy ✨

컴포넌트를 렌더링 하는 시점에 비동기적으로 로딩할 수 있게 해주는 유틸 함수이다. 로딩된 promise 객체(export된 모듈 들어있음)에 대한 resolve 처리를 대신 해 준다. state를 이용해 default 상태와 변경된 상태를 교체해 줄 필요도 없이 일반적인 컴포넌트처럼 적을 수 있다.

 

✨ Suspense

Suspense는 어떤 ‘컴포넌트가 읽어야 하는 데이터가 아직 준비가 되지 않았다’고 리액트에게 알려주는 내장 컴포넌트이다.

리액트는 기본적으로 JSX 코드 안에 들어있는 모든 컴포넌트를 바로 렌더링하지만, Suspense로 감싸주면 하위 컴포넌트에서 필요로 하는 작업 이후로 렌더링을 미루고, 그 작업이 진행되는 동안 fallback 속성으로 넘긴 컴포넌트를 대신 보여준다.

즉, 코드 분할된 컴포넌트의 로딩을 인지하고, fallback 속성을 통해 로딩이 진행중일 때 보여줄 UI를 설정할 수 있다.

https://breathtaking-life.tistory.com/134
https://www.youtube.com/watch?v=JU6sl_yyZqs

 

더 알아볼 것

동영상 강의 예제에서 사용된 중첩 라우팅과 <Outlet>에 관한 정보

React Router v6 튜토리얼

'프론트엔드 공부 > React' 카테고리의 다른 글

[Redux-toolkit] slice 동적으로 추가하기  (0) 2023.02.20
[Javascript] module 동적 import  (0) 2023.02.19