[React] 코드 분할 (Code Spliting)

KIM DA MI·2023년 5월 22일
0

React

목록 보기
7/7
post-thumbnail

1. 코드 분할 (Code Spliting)


  • React 앱들은 번들링(Bundling)을 통해 HTML 웹 페이지에 JavaScript를 쉽게 추가할 수 있다.
    번들링된 앱은 모든 JavaScript 코드가 한 곳에 있으므로 페이지 설정을 위한 호출 수가 적어지고, 단일 링크 태그만으로 JavaScript를 추가할 수 있다.
  • 과거에는 웹 JavaScript 코드가 작고 간단하여 번들링해도 문제가 없었다. 그러나 모던 웹에서는 JavaScript 코드의 복잡성과 크기가 증가하면서 번들링된 코드를 해석하고 실행하는 과정에서 성능 저하가 발생할 수 있다. 이에 대응하기 위해 코드 분할(Code Splitting)이라는 기술이 사용된다.
  • 코드 분할은 번들을 물리적으로 분리하여 필요한 코드만 현재 시점에 불러오고, 필요한 코드는 나중에 불러올 수 있도록 한다.
    이를 통해 번들의 크기를 줄이고 페이지의 로딩 속도를 개선할 수 있다. 코드 분할은 Webpack, Rollup 등의 번들러에서 지원하는 기능이다.

번들 분할 혹은 줄이는 법

  • 번들링 되는 파일에는 앱을 만들면서 npm을 통해 다운로드하는 서드파티(Third Party) 라이브러리도 포함이 된다.
  • 서드파티 라이브러리는 개인 개발자나 프로젝트 팀, 혹은 업체 등에서 개발하는 라이브러리로, 즉 제3자 라이브러리이다.
  • 서드파티 라이브러리는 플러그인이나 라이브러리 또는 프레임워크 등이 존재하며, 이 라이브러리를 잘 사용하면 편하고 효율적인 개발을 할 수 있다.
  • 그러나 서드파티 라이브러리는 사용자에게 다양한 메서드를 제공하기 때문에 코드의 양이 많고, 번들링 시 많은 공간을 차지한다. 따라서 사용 중인 라이브러리의 전부를 불러와서 사용하는 것보다 따로따로 불러와서 사용할 수 있다면 많은 공간을 차지하지 않을 수 있게 된다.

예시 코드

/* 이렇게 lodash 라이브러리를 전체를 불러와서 그 안에 들은 메서드를 꺼내 쓰는 것은 비효율적이다.*/
import _ from 'lodash';

...

_.find([]);

/* 이렇게 lodash의 메서드 중 하나를 불러와 쓰는 것이 앱의 성능에 더 좋다.*/
import find from 'lodash/find';

find([]);
  • 예시 코드는 lodash라는 라이브러리를 예시로 하고 있다.
  • lodash라는 라이브러리는 하나의 폴더와 같고, 그 폴더 안에는 개발 시 다양한 상황에 쓰기 좋은 메서드들, 즉 함수 코드들이 들어 있다.
  • 이 함수 코드들의 양이 상당하기 때문에 전부 가져올 시, 정말로 필요한 것 한두 개만 쓰인다면 나머지는 그냥 쓰이지 않는 코드 뭉치로 앱 내부에 남게 된다. 이는 앱의 성능을 저하시킬 요지가 있기 때문에, 필요한 것 한두 개만 가져다 쓰는 식으로 개발하는 것이 훨씬 좋다.



2. React에서의 코드 분할


  • React는 SPA(Single-Page-Application)인데, 사용하지 않는 모든 컴포넌트까지 한 번에 불러오기 때문에 첫 화면이 렌더링 될 때까지의 시간이 오래 걸린다.
    그래서 사용하지 않는 컴포넌트는 나중에 불러오기 위해 코드 분할 개념을 도입했다.
  • React에서 코드 분할하는 방법은 dynamic import(동적 불러오기)를 사용하는 것이다.
    그전까지는 코드 파일의 가장 최상위에서 import 지시자를 사용해 사용하고자 하는 라이브러리 및 파일을 불러오는 방법을 사용했었다. 이를 static import(정적 불러오기)라고 한다.

Static Import

/* 기존에는 파일의 최상위에서 import 지시자를 이용해 라이브러리 및 파일을 불러왔다. */
import moduleA from "library";

form.addEventListener("submit", e => {
  e.preventDefault();
  someFunction();
});

const someFunction = () => {
  /* 그리고 코드 중간에서 불러온 파일을 사용했다. */
}
  • 기존에는 항상 import 구문은 문서의 상위에 위치해야 했고, 블록문 안에서는 위치할 수 없는 제약 사항이 있었다.
  • 번들링 시 코드 구조를 분석해 모듈을 한 데 모으고 사용하지 않는 모듈은 제거하는 등의 작업을 하는데, 코드 구조가 간단하고 고정이 되어 있을 때에야만 이 작업이 가능해지기 때문이다.
  • 그러나 이제는 구문 분석 및 컴파일해야 하는 스크립트의 양을 최소화하기 위해 dynamic import 구문을 지원합니다.

Dynamic Import

form.addEventListener("submit", e => {
  e.preventDefault();
	/* 동적 불러오기는 이런 식으로 코드의 중간에 불러올 수 있게 된다. */
  import('library.moduleA')
    .then(module => module.default)
    .then(someFunction())
    .catch(handleError());
});

const someFunction = () => {
    /* moduleA를 여기서 사용한다. */
}
  • 이런 식으로 dynamic import를 사용하게 되면 불러온 moduleA 가 다른 곳에서 사용되지 않는 경우, 사용자가 form을 통해 양식을 제출한 경우에만 가져오도록 할 수 있다.
  • dynamic importthen 함수를 사용해 필요한 코드만 가져온다.
    가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 한다.
    이 방식을 사용하면 번들링 시 분할된 코드(청크)를 지연 로딩시키거나 요청 시에 로딩할 수 있다.
  • dynamic import는 React.lazy 와 함께 사용할 수 있다.



3. React.lazy()


  • React.lazy 함수를 사용하면 dynamic import를 사용해 컴포넌트를 렌더링할 수 있다.
  • React는 SPA이므로 한 번에 사용하지 않는 컴포넌트까지 불러오는 단점이 있다.
    이때 React.lazy를 사용하면 컴포넌트를 동적으로 import 할 수 있어 초기 렌더링 지연시간을 어느 정도 줄일 수 있게 된다.

    import Component from './Component';
    
    /* React.lazy로 dynamic import를 감쌉니다. */
    const Component = React.lazy(() => import('./Component'));
    • React.lazy로 감싼 컴포넌트는 단독으로 쓰일 수는 없고, React.suspense 컴포넌트의 하위에서 렌더링을 해야 한다.



4. React.Suspense


  • Router로 분기가 나누어진 컴포넌트들을 위 코드처럼 lazy를 통해 import하면 해당 path로 이동할 때 컴포넌트를 불러오게 되는데 이 과정에서 로딩하는 시간이 생기게 된다.
  • Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능이다.

    /* suspense 기능을 사용하기 위해서는 import 해와야 한다. */
    import { Suspense } from 'react';
    
    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
    
    function MyComponent() {
      return (
        <div>
                {/* 이런 식으로 React.lazy로 감싼 컴포넌트를 Suspense 컴포넌트의 하위에 렌더링한다. */}
          <Suspense fallback={<div>Loading...</div>}>
                    {/* Suspense 컴포넌트 하위에 여러 개의 lazy 컴포넌트를 렌더링시킬 수 있다. */}
            <OtherComponent />
                    <AnotherComponent />
          </Suspense>
        </div>
      );
    }
    • Supense 컴포넌트의 fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 로딩 화면으로 보여줄 React 엘리먼트를 받아들인다.

    • Suspense 컴포넌트 하나로 여러 개의 lazy 컴포넌트를 보여줄 수도 있다.



5. React.lazy와 Suspense의 적용


  • 앱에 코드 분할을 도입할 곳을 결정하는 것은 사실 까다롭기 때문에, 중간에 적용시키는 것보다는 웹 페이지를 불러오고 진입하는 단계인 Route에 이 두 기능을 적용시키는 것이 좋다.

    import { Suspense, lazy } from 'react';
    import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
    
    const Home = lazy(() => import('./routes/Home'));
    const About = lazy(() => import('./routes/About'));
    
    const App = () => (
      <Router>
        <Suspense fallback={<div>Loading...</div>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </Suspense>
      </Router>
    );
    • 라우터에 Suspense를 적용하는 것은 간단한 편이다.
      라우터가 분기되는 컴포넌트에서 각 컴포넌트에 React.lazy를 사용하여 import한다.

    • 그리고 Route 컴포넌트들을 Suspense로 감싼 후 로딩 화면으로 사용할 컴포넌트를 fallback 속성으로 설정해 주면 된다.

    • 초기 렌더링 시간이 줄어드는 분명한 장점이 있으나 페이지를 이동하는 과정마다 로딩 화면이 보이기 때문에 서비스에 따라서 적용 여부를 결정해야 한다.

0개의 댓글