[React] 지연로딩(lazy loading)

SuamKang·2023년 8월 19일
2

React

목록 보기
34/34
post-thumbnail

lazy loading : 지연로딩


리엑트 애플리케이션을 배포하기전에 많은 테스트와 최적화를 이루기도 하지만, 브라우저로 불러오는 자바스크립트 파일들의 로딩되는 부분을 수동으로 제어할 수 있는 기능이 있다.


🤔 이걸 왜하는가??

애플리케이션을 이루는 코드들 중 특정 코드를 필요할 때만 로딩하는 테크닉이 있다.


App.js

import { createBrowserRouter, RouterProvider } from "react-router-dom";

import BlogPage, { loader as postsLoader } from "./pages/Blog";
import HomePage from "./pages/Home";
import PostPage, { loader as postLoader } from "./pages/Post";
import RootLayout from "./pages/Root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "posts",
        children: [
          { index: true, element: <BlogPage />, loader: postsLoader },
          { path: ":id", element: <PostPage />, loader: postLoader },
        ],
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;

해당 파일에서 사용되는 로직들은 전부 무언가의 의존되는 import문을 추가하여 구성되어있다.

예를 들면
BlogPage컴포넌트를 import해야 App.js 안에서 그 컴포넌트를 사용할 수 있다. 결국 이 import문들은 여러 파일을 연결해주고 있다.

즉, 브라우저 화면에 최종적으로 사용자에게 전달하려면 화면에 무언가 그려지기 전 import문들이 먼저 다 처리되어야 한다는 것이다.(상호 의존 관계)


그래서 나중에 앱을 빌드할때, import되는 파일들이 하나의 커다란 파일에 병합된다.

만약 수십,수백개의 라우트와 컴포넌트가 있다면 모든 파일(코드)을 불러오는게 문제가 될 수도 있으며, 첫 페이지의 로딩속도도 느려질것이다.(CSR의 단점)


사용자가 웹사이트를 처음 방문하면 모든코드들을 다운로드한 후에야 화면에 무언가 뜰것이다.
큰 웹사이트들에게 이러한 문제가 발생하기 때문에 지연로딩이라는 개념이 필요한 것이다.

lazy loading의 핵심은 특정 컴포넌트를 나중에 불러오는것이다.


현재 불러오는 컴포넌트 파일중 하나를 예시로 들어보자.

BlogPage.js

import { useLoaderData } from "react-router-dom";

import PostList from "../components/PostList";

function BlogPage() {
  const posts = useLoaderData();
  return <PostList posts={posts} />;
}

export default BlogPage;

export function loader() {
  return fetch("https://jsonplaceholder.typicode.com/posts");
}

이 페이지는 블로그 페이지이기때문에 블로그 페이지에 사용자가 방문했을 경우에만 필요하므로 그때 모든 코드가 다운로드 되어야하는게 좋다.


우선 App에서 라우팅 정의한 곳에서 불러오는 import문을 제거하고한다.(그렇지 않으면 항상 로딩되니깐)

1) loader 재설정

그리고 먼저 해당 Blog페이지에서 import한것은 컴포넌트뿐 아니라 loader도 있으니 로더도 대체해주어야한다.
-> 이전에 등록했던 postsLoader를 지우고 함수를 지정해 그안에 import를 해준다.

import { createBrowserRouter, RouterProvider } from "react-router-dom";

// import BlogPage, { loader as postsLoader } from "./pages/Blog";
import HomePage from "./pages/Home";
import PostPage, { loader as postLoader } from "./pages/Post";
import RootLayout from "./pages/Root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "posts",
        children: [
          {
            index: true,
            element: <BlogPage />,
            loader: () => {}
          },
          { path: ":id", element: <PostPage />, loader: postLoader },
        ],
      },
    ],
  },
]);

그리고 이 import 함수는 프로미스를 리턴한다. ( 비동기적으로 관리 가능 )
리턴한 프로미스를 then으로 받아서 그 경로에서 로딩되는 파일을 module이라 설정하면 그곳에 속한 loader파일을 리턴해준다.
() => import('해당 함수또는 컴포넌트가 있는 경로').then(module => module.loader())


...
{
  index: true,
  element: <BlogPage />,
  loader: () => import('./pages/Blog').then(module => module.loader())
 }
...

이렇게 설정하면, loader함수가 실행되고 loader함수는 프로미스를 또 반환하므로 전체 함수에 대해 모든게 지연되어 로딩되게 되는것이다.

쉽게말해, 사용자가 Blog페이지를 방문해야만 해당 블로그 파일이 import되고 그 안에 설정된 loader함수가 실행되는것이다.
-> 동적으로 파일전달!!!


2) 컴포넌트 재설정 - lazy

이제 BlogPage컴포넌트에도 지연로딩을 적용하자.
함수를 선언해주고 동적으로 import한 블로그 페이지를 리턴하도록 만드는것이다.

다만,
현재 함수지만 컴포넌트 함수를 정의하는것이기 때문에 반드시 jsx코드와 같은걸 리턴해주어야 한다. 하지만 import문은 항상 프로미스를 리턴해주고 있기에 유효한 컴포넌트 함수는 아니다.

이때, 리엑트에선 해당 함수를 매핑할 특수한 함수키워드를 제공한다.

lazy 함수
lazy함수는 실행될 때, 동적으로 import하는 해당 함수를 인자로 받는다.

const BlogPage = lazy(() => import("./pages/Blog"));

3) Suspense컴포넌트로 감싸기

그리고 해당 컴포넌트를 적절하게 불러오려면 추가적으로 해주어야할 수순이 있다.
바로 Suspense컴포넌트로 해당 컴포넌트를 래핑해주어야 한다.

Suspense는 어떤 컴포넌트가 읽어야 하는 데이터가 아직 준비가 되지 않았다고 리액트에게 알려주는 메커니즘이라고 볼 수 있다.(실제 컨텐츠를 렌더링하기 전에 컨텐츠의 로딩을 기다려주는데 사용함)

그리고 속성엔 기다리는동안 대신 보여줄 컴포넌트를 fallback속성을 통해 설정해주면 된다.

import { createBrowserRouter, RouterProvider } from "react-router-dom";

// import BlogPage, { loader as postsLoader } from "./pages/Blog";
import HomePage from "./pages/Home";
import PostPage, { loader as postLoader } from "./pages/Post";
import RootLayout from "./pages/Root";
import { lazy, Suspense } from "react";

// 동적 import
const BlogPage = lazy(() => import("./pages/Blog"));

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "posts",
        children: [
          {
            index: true,
            element: (
              <Suspense fallback={<p>Loading..</p>}>
                <BlogPage />
              </Suspense>
            ),
            loader: () =>
              import("./pages/Blog").then((module) => module.loader()),
          },
          { path: ":id", element: <PostPage />, loader: postLoader },
        ],
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;

이렇게 다 설정을하고 브라우저에서 동작하는 네트워크탭을 확인해 보면,

블로그탭 버튼을 클릭해서 방문하게되면 이때 자바스크립트 파일이 동적으로 다운로드 된걸 확인할 수 있다.


이를 동적으로 불러오지 않고있는 부분과 비교하기위해 각 게시글탭을 눌러 방문하게 되면 서버에서 데이터를 다운로드하는 요청은 있지만 해당 컴포넌트의 코드를 다운로드하는 요청은 없다.

그럼 이제 블로그 페이지 말고 각각 게시글이 있는 페이지도 동적으로 불러와 보도록 설정해보자.

import { lazy, Suspense } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

// import BlogPage, { loader as postsLoader } from "./pages/Blog";
// import PostPage, { loader as postLoader } from "./pages/Post";
import HomePage from "./pages/Home";
import RootLayout from "./pages/Root";

const BlogPage = lazy(() => import("./pages/Blog"));
const PostPage = lazy(() => import("./pages/Post"));

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "posts",
        children: [
          {
            index: true,
            element: (
              <Suspense fallback={<p>Loading..</p>}>
                <BlogPage />
              </Suspense>
            ),
            loader: () =>
              import("./pages/Blog").then((module) => module.loader()),
          },
          {
            path: ":id",
            element: (
              <Suspense fallback={<p>Loading...</p>}>
                <PostPage />
              </Suspense>
            ),
            loader: (
              { params } // params가 필요하기에 전달해줘야함
            ) =>
              import("./pages/Post").then((module) =>
                module.loader({ params })
              ),
          },
        ],
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;

이렇게 설정하고 Post탭에 접속할때 자바스크립트 파일을 다운로드하는지 보면,

데이터 패칭과 더불어 script를 다운받은 모습을 확인할 수가 있다.


이렇게 리엑트가 제공하는 lazy와 Suspense를 통해서 지연 로딩을 적용하고 구현해봤는데, 사실 단순한 앱에선 두각이 드러나진 않지만 복잡한 앱에선 알아둘 필요가 있을것 같다는 생각이 들었다.

profile
마라토너같은 개발자가 되어보자

0개의 댓글