윤스페이스 : 일상을 기록합니다.

CRA + JS로 시작한 프로젝트가 완성될 즈음 구글 모바일 사이트 속도 측정을 해보니 24초가 나옵니다. 처음엔 S3 문제인 줄 알았으나 프로덕션 레벨에서 S3를 사용하는 경우도 있었기에 React & React-Native 오픈 채팅방에 질문을 올렸습니다.

 

그렇게 시작한 코드 스플리팅. 기본적인 스플리팅 방법은 아래의 포스트를 참고하였습니다.

https://velog.io/@velopert/react-code-splitting

 

리액트 프로젝트 코드 스플리팅 정복하기

코드 스플리팅, 뭐 별거있나요? 그냥 웹팩에서 하라는대로 하면 되는걸요. 하지만! 리액트에서 코드 스플리팅이랑 서버사이드 렌더링을 함께 해보신 경험이 있으시다면, 이 두가지 작업을 함께 하는 경우, 굉장히 번거로워질 수도 있다는 것을 아실 것 입니다. 이 포스트에서는, 리액트에서 코드 스플리팅을 하는 방법을 기초부터 이해를 해보고, react-loadabl...

velog.io

 

여러 방법들을 고민하다 최종적으로 HoC을 이용하여 스플리팅을 진행했습니다. 

아래의 코드로 withSplitting.js 파일을 만들어줍니다.

import React, { Component } from 'react';

const withSplitting = getComponent => {
  // 여기서 getComponent 는 () => import('./SplitMe') 의 형태로 함수가 전달되야합니다.
  class WithSplitting extends Component {
    state = {
      Splitted: null
    };

    constructor(props) {
      super(props);
      getComponent().then(({ default: Splitted }) => {
        this.setState({
          Splitted
        });
      });
    }

    render() {
      const { Splitted } = this.state;
      if (!Splitted) {
        return null;
      }
      return <Splitted {...this.props} />;
    }
  }

  return WithSplitting;
};

export default withSplitting;

 

이제 withSplitting.js 파일을 활용해 컴포넌트를 불러올 때 const Comp = withSplitting(()=>import('...')) 이런 식으로 불러오면 됩니다.

 

기존에 라우터를 정의해주는 파일에서 컴포넌트를 불러올 때 Pages/index.js 파일에서 한 번에 불러오는 방법을 사용했습니다.

//src/pages/index.js

export { default as Root } from './Root';
export { default as MainMap } from './MainMap';
export { default as Add } from './Add';
export { default as AddPlace } from './AddPlace';
export { default as PlaceInfo } from './Place/PlaceInfo';
export { default as PlaceComments } from './Place/PlaceComments';
export { default as PlacePhotos } from './Place/PlacePhotos';
export { default as Comment } from './Comment';
export { default as Login } from './Auth/Login';
export { default as Callback } from './Auth/Callback';
export { default as Test } from './Test';

이제 스플리팅 하기 위해 코드를 아래와 같이 변경해줍니다.

//src/pages/index.js

import withSplitting from '../utils/withSplitting';

export const Root = withSplitting(() => import('./Root'));
export const MainMap = withSplitting(() => import('./MainMap'));
export const Add = withSplitting(() => import('./Add'));
export const AddPlace = withSplitting(() => import('./AddPlace'));
export const PlaceInfo = withSplitting(() => import('./Place/PlaceInfo'));
export const PlaceComments = withSplitting(() =>
  import('./Place/PlaceComments'),
);
export const PlacePhotos = withSplitting(() => import('./Place/PlacePhotos'));
export const Comment = withSplitting(() => import('./Comment'));
export const Login = withSplitting(() => import('./Auth/Login'));
export const Callback = withSplitting(() => import('./Auth/Callback'));
export const Test = withSplitting(() => import('./Test'));

 

코드를 변경하고 네트워크 로그를 살펴보니 정상적으로 작동한 것 같아 스플리팅 작업을 모두 마쳤다고 생각했습니다. 구글 모바일 사이트 속도 측정에서도 9.6초로 약 2배 이상 최적화된 게 수치상으로도 나타났기 때문이었죠. 

 

문제는 프로젝트를 타입스크립트로 전환하며 발생하기 시작합니다. 어쩌면 원래 문제였던 게 타입을 지정해주면서 나타났을지도 모르겠습니다. 지금이라도 발견해서 다행이라 해야 할까요. 

 

문제 해결 방법

 

1. getComponent 타입 지정

 

  • getComponent가 당연히 컴포넌트를 받아올 거라 생각해 React.Component를 지정해줬더니 .then() 타입이 없다며 뱉어냅니다.

  • getComponenet에 import를 사용했던게 생각나 dynamic import component typescript type 이런 키워드들로 구글링을 해봤으나 별 다른 정보를 얻지 못했습니다.
  • Promise<React.Component>로 시도해봤으나 함수형 컴포넌트, 클래스형 컴포넌트가 섞여있어 그런지 타입 오류를 뱉어냅니다. 

  • 'missing the following properties from type 'Component<{}, {}, any>': context, setState, forceUpdate, render, and 3 more.'

  • () => Promise<{ default: React.ComponentType}> 으로 지정했더니 Component 타입 지정까진 성공했습니다.

 

2. JSX element type 'Splitted' does not have any construct or call signatures.

 

  • 이제 최종적으로 타입 지정이 된 Splitted를 렌더링 해주면 되는데 계속 렌더링에서 실패하는 문제가 생겼습니다.
  • 혹시나 해서 Splitted 로그를 찍어봐도 정상적으로 넘어오는 게 확인됩니다.

  • public state = { Splitted: null } 로 처음에 타입 지정을 해준 게 잘못이었습니다. 타입이자 값을 정해줘 버린 거죠. 아래와 같이 state interface를 만들어줍니다.

  • null값을 지정해줘도 계속해서 같은 오류가 반복되길래 state를 초기화시켜줬습니다. 초기화시켜줘도 이게 초기에 null값을 반환해서 그런지 같은 오류가 나옵니다. 혹시나 해서 Loading 컴포넌트를 가져와서 초기 값에 넣어주니 정상적으로 작동하는 걸 확인할 수 있었습니다.

 

최종적인 withSplitting.tsx 코드

 

import React, { Component } from 'react';
import Loading from '../components/Loading';

interface IState {
  Splitted: React.ComponentType<any> | null;
}

const withSplitting = (getComponent: () => Promise<{ default: React.ComponentType<any> }>) => {
  class WithSplitting extends Component<any, IState> {

    public state = {
      Splitted: Loading,
    };

    constructor(props: any) {
      super(props);
      getComponent().then(({ default: Splitted }) => {
        this.setState({
          Splitted,
        });
      });
    }

    public render() {
      const { Splitted } = this.state;
      if (!Splitted) {
        return null;
      }
      return <Splitted />;
    }
  }

  return WithSplitting;
};

export default withSplitting;

 

20191006 수정 / loadable component를 사용하세요 !

https://www.npmjs.com/package/@loadable/component

 

@loadable/component

React code splitting made easy.

www.npmjs.com

import loadable from "@loadable/component";

export const NotFound = loadable(() => import("./NotFound"));
export const Root = loadable(() => import("./Root"));
export const Login = loadable(() => import("./Login"));
export const Identity = loadable(() => import("./Identity"));

두달 간 사용해본 결과 성능도 직접 스플리팅 해주는 것 보다 잘 나와주고 있습니다.

 

마무리

최종적으로 페이지 초기 로딩 속도를 4.6초까지 줄였습니다. 처음 24초에 비하면 약 6배 빨라진 속도입니다. 기존 JS 스플리팅 함수에서 느렸던 게 어쩌면 타입이 어긋나 뭔가 꼬였던 게 아닌가 싶습니다. 문제를 해결하는 데에 도움 주신 김민혁 님, 박수한 님, 최강혁 님께 진심으로 감사합니다. 4개월 전 리액트를 처음 공부하기 시작했을 때 작성했던 코드들을 바라보면 한숨밖에 안 나옵니다. 오늘부터 그 코드들을 하나하나 최적화 시켜보려 합니다. 별 탈 없이 풀리길 바라봅니다.

 

React & React-Native 오픈 채팅방에 놀러오세요!

 

React & React-Native

#programming #react #reactnative #hybrid #mobile #web #리액트 #리액트네이티브

open.kakao.com

 

'DEVELOPMENT' 카테고리의 다른 글

리액트 프로젝트 코드 스플리팅 (with typescript)  (1) 2019.09.13