React Code Splitting

코드 분할

번들링은 훌륭하지만 앱이 커지면 번들도 커져서 로드 속도가 느려집니다. 프로젝트 초기라면 괜찮습니다. 하지만 프로젝트 규모가 커지고 최적화가 필요한 단계에서 Code Splitting을 한다면 서비스 퍼포먼스가 좋아지기 때문에 사용자들에게 보다 나은 사용자 경험을 줄 수 있습니다.

방법 #1. React.lazy

React.lazy 함수를 사용하면 동적 import를 사용해서 컴포넌트를 렌더링 할 수 있습니다.
(React.lazy는 React 16.6버전 부터 지원합니다.)

Before

import OtherComponent from "./OtherComponent";

After

const OtherComponent = import React.lazy(() => import('./OtherComponent'));

With Suspense

lazy 컴포넌트는 Suspense 컴포넌트 하위에서 렌더링되어야 하며, Suspense는 lazy 컴포넌트가 로드되길 기다리는 동안 로딩 화면과 같은 예비 컨텐츠를 보여줄 수 있게 해줍니다.

const OtherComponent = React.lazy(() => import("./OtherComponent"));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

하나의 Suspense 컴포넌트로 여러 lazy 컴포넌트를 감쌀 수도 있습니다.

const OtherComponent = React.lazy(() => import("./OtherComponent"));
const AnotherComponent = React.lazy(() => import("./AnotherComponent"));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </div>
  );
}

Route-based code splitting

앱 코드를 분리하는 단위는 선택이지만, 좋은 방법 중에 하나는 라우트 단위로 분할 하는 방법입니다.

import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import React, { Suspense, lazy } from "react";

const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Suspense>
  </Router>
);

코드 Build 결과

dist 디렉토리를 보면 라우팅 단위로 파일이 분리되어 생성된것을 확인 할 수 있습니다.

react-app
 dist/
  - index.html
  - main.b1234.js (contains Appcomponent and bootstrap code)
  - home.bc4567.js (contains Home)
  - about.bc4567.js (contains About)

/** index.html **/
<head>
  <div id="root"></div>
  <script src="main.b1234.js"></script>
</head>

흔히 발생할 수 있는 실수

import 하는 페이지 내부에 사용중인 컴포넌트가 사용하지 않는 다른 컴포넌트와 index.js에 export를 편의상 묶어 사용하는 경우가 있습니다. 이때 컴포넌트 import시 객체 디스트럭쳐링을 사용할 경우 내부적으로 사용하지 않는 컴포넌트도 함께 로드되기 때문에 코드 분할 시 큰 효과를 보지 못합니다. 그렇기 때문에 실사용 컴포넌트만 개별 임포트 하는것이 중요합니다.

// 예로, PageA를 코드 분할하려고 합니다.
const PageA = React.lazy(() => import("./PageA"));
function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <PageA />
      </Suspense>
    </div>
  );
}

// PageA.js 에선 Button 컴포넌트를 사용하려고 import하고 있습니다.
// X 사용하지 않는 다른 컴포넌트도 함께 로드됩니다.
import { Button } from "./components"; 
// O 개별 컴포넌트로 로드해야 불필요한 컴포넌트가 불려 번들 사이즈가 커지는것을 방지할 수 있어요.
import Button from "./components/Button";

function PageA() {
  return (
    <div>
      <h1>PageA</h1>
      <Button>OK</Button>
    </div>
  );
}

// components/index.js
export { default as Loader } from "./common/Loader";
export { default as Button } from "./common/OtherSetting";
export { default as InsideSearch } from "./common/InsideSearch";

 

방법 #2. Loadable Components

React.lazy와 Suspense는 서버 사이드 렌더링을 지원하지 않습니다. 코드 분할을 클라이언트, 서버사이드 모두 가능하도록 만들고 싶다면 Loadable Components를 사용하면됩니다. (React 공식 문서에서 권장하는 방법)

React.lazy와 비교

Library Suspense SSR Library splitting import(./${value})
React.lazy
@loadable/component

Install

npm install @loadable/component

# or use yarn
yarn add @loadable/component

loadable Import

import loadable from "@loadable/component";

const OtherComponent = loadable(() => import("./OtherComponent"));

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

With Suspense

React Suspense를 지원하여 함께 사용할 수 있습니다.

import React, { Suspense } from "react";
import { lazy } from "@loadable/component";

const OtherComponent = lazy(() => import("./OtherComponent"));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Full dynamic import

다이나믹하게 import() 하는 Loadable Component를 만들어서 코드의 반복을 줄일 수 있습니다.

import loadable from "@loadable/component";

const AsyncPage = loadable((props) => import(`./${props.page}`));

function MyComponent() {
  return (
    <div>
      <AsyncPage page="Home" />
      <AsyncPage page="Contact" />
    </div>
  );
}

번외, React-loadable

React-loadable 모듈은 React 공식 문서에서도 오랫동안 권장하던 방법이었습니다. 그러나 현재는 더 이상 유지 관리되지 않으며 Webpack v4 + 및 Babel v7 +와 호환되지 않는 부분들이 있습니다.
그래서 React.lazy 또는 Loadable components로 변경하는것을 추천합니다.

참고

WebRTC 소개

WebRTC (Web Real-Time Communication)는 웹 브라우저 간에 플러그인 설치 없이 스트림과 데이터를 상호 통신할 수 있도록 설계된 JavaScript API입니다.

WebRTC는 구글이 오픈소스화한 프로젝트에서 기원하였으며, 그 뒤로 국제 인터넷 표준화 기구(IETF)가 프로토콜 표준화 작업을, W3C가 API 정의를 진행하였으며, 음성 통화, 영상 통화, P2P 파일 공유 등으로 활용 될 수 있습니다.

WebRTC 지원 브라우저

최신 버전의 주요 브라우저들이 지원을 하고있습니다.

WebRTC 지원 브라우저

하지만 경험상 Edge브라우저는 개발과정에 디버깅이 힘들고 타 브라우저에 비해 성능도 떨어집니다.
Edge 크로미움 버전이 추후 나온다니 좋은 성능을 보여주기를 기대합니다.
경험상 크롬에서 먼저 개발하고 어느정도 안정화가 되면 브라우저를 확장해 나가는것을 추천합니다.

WebRTC 주요 API

  • GetUserMedia
    사용자의 카메라와 마이크 접근을 담당
  • GetDisplayMedia
    화면공유를 위한 접근을 담당
  • RTCPeerConnection
    Peer간의 연결을 위한 인스턴스를 생성하고, 연결 후 스트림 전송에 사용 (생성시 STUN 서버 요청)
  • RTCDataChannel
    Peer간의 Data를 주고 받을 수 있는 Tunnel API (WebSocket과 유사하지만, P2P라 속도가 보다 빠름)

WebRTC P2P기반 NAT 및 방화벽 통과 기법

  • STUN (Session Traversal Utilities for NAT) 서버
    사내망 환경에 NAT를 통해 공인IP, Port를 알아내는 역할을 함
  • TURN (Traversal Using Relays around NAT) 서버
    P2P 연결이 안되는 환경일때 트래픽을 중계하는데 사용함
  • ICE (Interactive Connectivity Establishment)
    P2P 간 다이렉트로 통신을 위해 STUN, TURN 등의 기술을 종합 활용하여 라우팅 경로를 찾아내는 기술로 UDP hole punching (P2P간 공인IP가 아니더라도 최대한 연결 가능하도록 하는 기법)을 지원.

WebRTC P2P 연결 절차

WebRTC Connection Flow

위 과정은 아래의 webrtc-internals 화면을 열어서 함께 보는것이 이해하는데 도움이 됩니다.

WebRTC Connection Step 확인

크롬 주소창에 chrome://webrtc-internals 입력하면 WebRTC 연결 흐름과 상태를 볼 수 있습니다.
실제 개발을 할 때 개발자 콘솔창을 보는 시간 만큼 자주 보게되는 화면입니다.

WebRTC 디버깅

WebRTC 활용한 서비스

참고