React.js 성능 향상을 위한 코드 분할(React.lazy, Suspense)

Hyun Jin·2023년 8월 18일
0

개인 프로젝트를 진행하다가 리액트 최적화를 할 수 있는 방법을 찾아보았다.
아래 참고사이트들의 내용을 대략적으로 정리해서 스터디에서도 발표했다.

요약

React.lazySuspense 는 거의 세트로 쓰이는데,
React.lazy 는 실제로 호출된 컴포넌트만 동적으로 import 를 해와서 컴포넌트를 렌더링해 준다.
lazy 컴포넌트는 Suspense 컴포넌트 하위에서 렌더링되어야 하는데,
Suspense 는 lazy 컴포넌트가 로드 되는 동안 로딩 화면, 스피너 같은 컨텐츠(fallback 속성 내의 컴포넌트)를 보여주는 데에 주로 쓰인다.

로딩 화면을 띄워줄 때 isLoading 등의 상태를 확인하는 코드를 줄일 수 있어서 효율적일 것 같다.
비동기 데이터 관련해서 사용하는 방법도 있다는데, 상세 방법은 리액트 공식 사이트를 좀더 파고들어봐야겠다.

참고 사이트

Suspense (react.dev)
[React.js] 성능 향상을 위한 코드 분할(React.lazy, Suspense)
React Suspense 소개 (feat. React v18)
[React] Suspense을 사용해 선언적으로 로딩 화면 구현하기
사용자 경험 개선 1편 - react suspense


React.lazy


서버 측에서 렌더링하지 않는 경우 'React.lazy()'로 자바스크립트 번들을 분할하는 방법을 사용할 수 있음.

React.lazy 함수를 사용하면 동적 import를 사용해 컴포넌트 렌더링이 가능하다. (Webpack이 import 구문을 만나게 되면 앱의 코드를 분할한다.)

React.lazy는 동적 import()를 호출하는 함수를 인자로 가진다.

이 함수는 React 컴포넌트를 default export로 가진 모듈 객체가 이행되는 Promise를 반환한다.

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

fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 렌더링하려는 React Element를 받아들인다. Suspense 컴포넌트는 lazy 컴포넌트를 감싸며, 하나의 Suspense 컴포넌트로 여러 lazy 컴포넌트를 감쌀 수도 있다.

Suspense


기본적으로 리액트는 JSX 코드 안에 들어있는 모든 컴포넌트를 즉시 호출하여 바로 랜더링(rendering)을 진행함.

컴포넌트를 Suspense로 감싸주면 컴포넌트의 랜더링을 특정 작업 이후로 미루고, 그 작업이 끝날 때 까지는 fallback 속성으로 넘긴 컴포넌트를 대신 보여줄 수 있음.

Router로 분기가 나누어진 컴포넌트들을 lazy를 통해 import하면 해당 path로 이동할때 컴포넌트를 불러오게 되는데 이 과정에서 로딩하는 시간이 생기게 되는데, Suspense 는 보통 이 로딩 시간동안 로딩 화면을 보여주는 역할로 사용함.

Suspense 사용 전의 단점

React에서 이처럼 비동기 데이터를 읽어오는 컴포넌트를 작성하면 몇가지 고질적인 문제가 발생하는 것으로 알려져있는데요.

우선 최종 사용자(end user) 경험 측면에서 UI가 마치 폭포(waterfall)처럼 순차적으로 나타나는 현상이 나타날 수 있습니다. 이 waterfall 현상은 특히 한 페이지 상의 여러 컴포넌트에서 동시에 비동기 데이터를 읽어오는 경우 자주 발생하는데요. 상위 컴포넌트의 데이터 로딩이 끝나야지만 하위 컴포넌트의 데이터 로딩이 시작될 수 있기 때문에 주로 발생하게 됩니다.

뿐만 아니라 이렇게 초기 랜더링 후에 데이터 로딩 후 다시 랜더링을 수행하는 방법은 경쟁 상태(race conditions)에도 취약한 것으로 알려져있는데요. 비동기 통신은 반드시 요청한 순서대로 데이터가 응답된다는 보장이 없기 때문에 의도치 않게 싱크가 맞지 않은 데이터를 제공할 수도 있습니다.

마지막으로 개발 측면에서도 이렇게 if 조건문을 사용하여 어떤 컴포넌트를 보여줄지를 제어하는 것은 명령형(imperative) 코드에 가깝기 때문에 선언적(declarative) 코드를 지향하는 React의 기본 방향성과 맞지 않게 느껴지고요. 기본적으로 데이터 로딩과 UI 랜더링이라는 두 가지 전혀 다른 목표가 하나의 컴포넌트 안에 커플링(coupling)되어 코드가 읽기가 어려워지고 테스트를 작성하기도 힘들어집니다.

Suspense 사용 시의 장점

1. 모든 요청을 기다리지 않고도 화면을 렌더링할 수 있다.

2. 경쟁 상태 (Race Condition) 발생을 방지한다.

  • JS 에서는 여러 개의 비동기 작업(fetching response)의 결과가 하나의 DOM 객체에 반영되는 상황이 있음
  • suspense는 state 설정 시기를 바꾸어 이를 해결한다.

이전의 코드는 A 프로필 요청 -> 로딩 UI 렌더 -> A 프로필 응답 -> <Profile />에 응답 반영의 순서였다면 suspense는 이 과정을 A 프로필 요청 -> <Profile />에 A 프로필 요청 리소스 반영 -> suspense에 의해 A 요청에 대한 로딩 UI 렌더 -> 요청 리소스로 A 프로필 응답이 들어옴 -> <Profile />에 응답 반영으로 바꾼다.

경쟁 상태를 이런 방식으로 해결 가능한 이유는 suspense가 응답이 언제 오는지, 시간에 대한 것을 고려하지 않아도 되기 때문이다. 프로필을 요청함과 동시에 해당 요청 리소스를 반영하기 때문에 이전에 수행하고 있던 요청이 있더라도 해당 요청은 무시하고 새로운 요청으로 대체된다.

suspense는 이를 통해 경쟁 상태를 해결할 수 있다.

비동기 데이터 관리 라이브러리

  • Relay: graphQL을 기반으로 데이터를 다루는 라이브러리
  • SWR: 서버 데이터를 다루는 라이브러리
  • React-Query: 서버 데이터를 다루는 라이브러리
  • Recoil: 전역 상태 관리 라이브러리

사용 예시 코드

import React, { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom';
import Spinner from './items/Spinner'
//import Login from './pages/Login';
//import Main from './pages/Main';
//import Search from './pages/Search';
//import Setting from './pages/Setting';
const Main = lazy(() => import('./pages/Main'));
const Login = lazy(() => import('./pages/Login'));
const Search = lazy(() => import('./pages/Search'));
const Setting = lazy(() => import('./pages/Setting'));

function App() {

  return (
    <div>
    	<Suspense fallback={<Spinner text='페이지를 불러오는 중'/>}>
          <Routes>
            <Route exact path='/' component={Main} />
            <Route path='/login' component={Login} />
            <Route path='/setting' component={Setting} />
            <Route path='/search/query=:word' component={Search} />
          </Routes>
		</Suspense>
    </div>
  );
}

export default App;
profile
새싹 프론트엔드 개발자

0개의 댓글