코드 분할하기 with Suspense, lazy

코드 분할하기

브라우저에서 앱의 모든 코드를 한꺼번에 적재하지 않고 필요할 때 덩어리로 코드를 적재하는 코드 분할(Code Spliting) 기술을 사용하면 적재할 코드의 양을 관리할 수 있다.

  • 트리 셰이킹 과정을 거쳐도, 앱이 무거워질수록 번들을 최대한 작게 만들어도 여전히 로드 시간이 길어질 수 있다.

    • 트리 셰이킹 과정으로 중복 코드를 피하고 사용하지 않는 코드를 제거

  • 앱의 일부가 사용되지 않을 가능성이 높거나 아주 무거운 컴포넌트를 포함하는 경우, 초기 번들의 크기를 줄여 사용자가 상호작용을 시작할 때만 추가 번들을 적재하는 쪽으로 최적화 가능

  • 즉, 실제로 필요할 때 일부 컴포넌트를 동적으로 가져오는 기법

정적 Import를 사용하면 빌드 시 번들러가 코드 검사 후 파일 경로를 따라서 앱이 실제로 사용하는 모든 코드를 포함하는 파일인 번들을 생성한다.

초기 사용자 상호작용에 참여하지 않는 컴포넌트 최적화

lazy 메서드와 Suspense 컴포넌트를 사용해 아래 네가지 작업을 실행

  • 컴포넌트를 렌더링하려 시도 할 때만 코드를 적재

  • 컴포넌트 적재 중에 플레이스홀더를 표시

  • 앱의 나머지 부분을 계속 렌더링

  • 컴포넌트 코드가 적재된 후 플레이스홀더를 해당 컴포넌트로 대체

import 함수로 코드 동적 임포트 with Vanila JS

import 함수로 사용자의 인터렉션 이후에 코드를 동적으로 적재

// js (no react)
function handleClick() {
  import('./module') // 임포트 함수 호출
    .then((module) => { // 지역 변수에 모듈을 할당
      module.default('messagePara','hello world'); // 모듈 프로퍼티로 익스포트된 함수 호출
      moudle.sayHi('hiPara');
    })
}
  • 임포트 함수는 익스포트한 모듈로 해소되는 프로미스를 반환한다.

  • 모듈이 적재된 다음에 작업을 수행하기 위해 프로미스의 then 메서드를 호출함

  • then 대신 async/await 구문 사용 가능

  • 디폴트 익스포트는 default 프로퍼티로 할당됨

async function handleClick() {
  const module = await import(경로);
  // module 사용
}
  • 구조 분해 사용 가능

function handleClick() {
  import('./module')
    .then(({default: showmessage, sayHi}) => {
      showMessage();
      sayHi()
    })
}

async function handleClick() {
  const {default: showMessage, sayHi} = await import('./module');
}

lazy와 Suspense를 사용해 동적으로 컴포넌트 임포트

리엑트를 사용하는 경우 상태 갱신에만 집중하고, 리엑트가 DOM을 관리하게 해야 한다.

lazy 함수

React.lazy 를 사용해 컴포넌트가 처음 렌더링될 때 컴포넌트를 적재하라.

  • 렌더링할 컴포넌트가 아직 준비되지 않았을 때 리엑트가 무엇을 해야할지를 선언적으로 알려줘야 한다.

    • lazy 함수를 사용해 컴포넌트를 지연 컴포넌트로 변환하기

    • Suspense 컴포넌트를 사용해 폴백 컨텐츠 지정하기

    • 지연 적재와 Suspense가 어떻게 함께 작동하는지 이해하기

    • 경로에 따라 앱 분할하기

  • lazy에게 프로미스를 반환하는 함수를 전달

  • lazy로 불러온 컴포넌트는 반드시 default export로 만들어져야 한다?

  • 사용하는 컴포넌트에서는 반드시 Suspense로 감싸야한다.

    • lazy 컴포넌트가 적재될 때까지 렌더링할 내용을 리엑트에게 알려주어야 한다.

    • Suspense는 아직 적재되지 않은 컴포넌트가 던지는 보류 중인 프로미스를 잡아낸다.

const LazyComponent = lazy(() => import('./Component.tsx'));
// 아래와 같다.
// 리엑트는 처음으로 컴포넌트를 렌더링할 때 아래와 같은 promise 함수를 호출한다.
const getPromise = () => import(modulePath); // 프로미스를 반환하는 함수를 만듬
const LazyComponent = lazy(getPromise); // React.lazy에게 프로미스를 만드는 함수를 전달
  • mock Component

const module = {
  default: () => <div>Mock Component</div>
}
function getPromise() {
  return new Promise(
    (resolve) => setTimeout(() => resolve(module), 3000)
  )
}

const LazyComponent = lazy(getPromise)

const ComponentWrapper = () => {
  const [isOn, setIsOn] = useState(false);
  return isOn 
    ? (<LazyComponent />) 
    : (
      <div>
        <button onClick={() => {setIsOn(true)}}>Show Component</button>
      </div>
      )
}

Suspense

컴포넌트를 적재하려면 시간이 걸리는데 그동안 사용자에게 컴포넌트를 적재하고 있음을 알려주기 위한 인디케이터가 필요

  • 자신의 모든 자손 컴포넌트가 제데로 UI를 반환할 때 까지 표시할 내용을 지정하기 위해 fallback 프로퍼티 사용

  • 즉, 자식 컴포넌트 중 어느 하나라도 적재중인 경우 폴백 UI를 표시

lazy와 Suspense가 어떻게 함께 작동하는지 이해하기

  • 리액트가 지연 컴포넌트를 첫 렌더링하려고 할 때, 컴포넌트는 초기화되지 않았지만 모듈을 적재하기 위해 리액트가 호출할 수 있는 함수(프로미스를 반환하는 함수)가 존재한다.

  • 프로미스는 컴포넌트를 default 프로퍼티로 제공하는 모듈로 해소되어야만 한다.

  • 해소된 후 지연 컴포넌트의 상태를 resolved로 설정하고 렌더링할 준비가 된 컴포넌트를 반환한다.

  • else 절에는 상위 컴포넌트의 Suspense 컴포넌트와 통신하는 핵심 코드가 포함되어있음

    • 즉, 프로미스가 해소되지 않으면 리액트가 프로미스를 마치 예외처럼 throw한다.

    • 서스펜스는 던져진 프로미스를 catch하면서 폴백 UI를 렌더링하게 되어있다.

    • 단, 네트워크 오류등으로 프로미스가 거부되었을 때는 서스펜스가 처리하는게 아닌 에러바운더리가 처리

if (status === 'resolved') {
  return component;
} else {
  throw promise;
}

Error boundary

리액트는 자식 컴포넌트에서 발생하는 오류를 잡아내는 컴포넌트를 따로 제공하지 않음

But 클래스 컴포넌트가 오류를 잡아서 보고하고 싶을 때 구현할 수 있는 몇 가지 생명주기 메서드를 제공

  • 클래스 컴포넌트로 이런 메서드를 하나 이상 구현하면, 그 컴포넌트는 오류 경계를 설정한 것으로 간주됨

    • 오류 경계를 설정하면 오류 발생 시 앱을 언마운트하는 대신 폴백 UI를 표시

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) { // 오류를 잡아낸 경우 새 상태를 반환
    // 다음 렌더링 시 폴백 UI를 보여주도록 상태를 갱신
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) { // 오류를 잡아낸 경우 로그를 남김
    // 오류 로그를 오류 보고 서비스에 전달
    logErrorToMyService(error, errorInfo);
  }
  
  render() {
    const {
      children,
      fallback = <h1>Something went wrong.</h1>
    } = this.props;
    
    return this.state.hasError ? fallback : children;
  }
}

Last updated