Skip to main content

[Webpack 기반의 React App 만들기] 1. Create React App의 Deprecated 발표

서론

SSAFY 과정에서 Webpack(Create React App) 기반의 React App과 Vite 기반의 React App의 개발 서버 구동 및 빌드시간에 현저한 격차가 있음을 확인했습니다.
하지만, Create React App의 경우, Webpack 뿐만 아니라 내포한 다른 추가적인 라이브러리들이 있기 때문에 명확한 비교가 아니라는 것을 깨닫게 되었습니다.
따라서 최대한 여러 플러그인, 라이브러리들을 배제하고 비슷한 환경을 구성해 테스트를 해야 겠다는 생각이 들었습니다.
올해 2월, React측에서 CRA(Create-react-app)deprecated되었다고 발표했습니다.(Sunsetting Create React App – React)
따라서 발표 이후, 새로운 React App을 생성하는데에 다른 프레임워크들을 사용하기를 권장하고 있습니다. 또한, 현재 CRA기반의 React App들도 다른 프레임워크나 빌드 툴로 마이그레이션 하기를 권장하고 있습니다.
아래 내용은 위 React의 Deprecated 발표를 번역한 내용입니다.

CRA의 대체 프레임워크

Next.js

npx create-next-app@latest

Vercel에서 관리중인 풀스택 프레임워크 입니다.

React Router

npx create-react-router@latest

Vite와 결합하여 풀스택 프레임워크를 만들 수 있는 라우팅 라이브러리 입니다.

Expo

npx create-expo-app@latest

Expo에서 관리중인 풀스택 프레임워크로, 네이티브 UI를 갖춘 범용 Android, iOS 및 웹 앱을 만들수 있는 프레임워크 입니다.

빌드 툴의 한계

CRA와 같은 빌드 툴은 기본적으로 dev 서버, 린팅, production 빌드 등에 대한 설정이 되어있어 React App을 구축하기 편리했습니다. 하지만, 이러한 빌드 툴들은 실제 Production App을 빌드하는데 있어 필요한 라우팅, 데이터 페칭, 코드 스플리팅 등의 문제에 대한 해결책을 필요로 합니다. 따라서 React 측에서는 상기한 프레임워크를 사용하기를 권장하고 있습니다.

라우팅

CRA에는 어떠한 라우팅 솔루션이 포함되어 있지 않아, React Router 또는 Tanstack Router와 같은 라우팅 라이브러리의 도움을 받아 라우팅을 해야 합니다.

데이터 페칭(데이터 불러오기)

export default function Dashboard() {
  const [data, setData] = useState(null);

  // ❌ Fetching data in a component causes network waterfalls
  useEffect(() => {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => setData(data));
  }, []);

  return (
    <div>
      {data.map(item => <div key={item.id}>{item.name}</div>)}
    </div>
  )
}

CRA에는 특정한 데이터 페칭 솔루션이 포함되어 있지 않습니다. 따라서 보통 effect내부에서 fetch API와 같은 기능을 사용해 데이터를 불러오는데, 이는 컴포넌트 렌더링 후 데이터를 불러와 네트워크 워터폴이 발생할 수 있습니다.

  • 네트워크 워터폴: 네트워크 요청병렬적으로 이뤄지지 않고, 순서대로 이루어져 페이지 컨텐츠를 볼 수 있는데까지 소요되는 시간이 늘어나는 현상
  • 위 코드에서도 컴포넌트 함수 코드 불러오기 -> 컴포넌트 함수 실행 -> 컴포넌트 함수 내부 effect의 fetch 함수 실행 -> fetch를 통해 불러온 데이터 기반으로 렌더링 으로 이어지는 일련의 과정이 순차적으로 실행될 것임을 예상할 수 있음
export async function loader() {
  const response = await fetch(`/api/data`);
  const data = await response.json();
  return data;
}

// ✅ Fetching data in parallel while the code is downloading
export default function Dashboard({loaderData}) {
  return (
    <div>
      {loaderData.map(item => <div key={item.id}>{item.name}</div>)}
    </div>
  )
}

이러한 문제를 해결하기 위해 미리 데이터를 페칭(prefetch)하는 옵션을 제공하는 React Query, SWR, Apollo, Relay와 같은 라이브러리의 도움을 받을 수 있습니다. 특히 이러한 라이브러리들은 라우팅의 loader 패턴과 결합하여, 라우팅 레벨에서의 데이터 종속성을 지정할 경우 가장 적합하게 작동합니다. 위 코드의 경우, 라우터라우트 렌더링 이전 데이터를 불러와 “렌더링에 필요한 데이터"와 “라우트"를 동시에 가져오도록, 즉 병렬 처리를 하도록 할 수 있습니다.

코드 스플리팅

CRA에는 특정한 코드 스플리팅 솔루션이 포함되어 있지 않아, React App이 단일 번들 파일로 제공됩니다. 이는 사용자가 필요하지 않는 코드들까지 다운받게 하므로, 연관된 코드들끼리 묶인 번들로 분할하도록 해 앱 로딩에 걸리는 시간을 줄이는게 좋습니다.

import { lazy } from 'react';

// lazy 함수에는 React 컴포넌트를 resolve하는, Promise와 같은 thenable 객체를 인자로 넘깁니다.
// 이후 `lazy 함수로 감싼 컴포넌트`를 처음 렌더링하려고 할 때 React는 렌더링을 시작합니다.
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));

React에서는 이를 위해 React.lazy 기능을 제공하지만, 이는 컴포넌트가 렌더링 될 때까지 코드를 페칭하지 않으므로 네트워크 워터폴을 발생시킬 수 있습니다. 더 최적의 방식으로 이를 해결하려면 라우터를 통해 코드 다운로드와 함께 코드 페칭을 병렬로 수행하는 방식을 적용하는게 좋습니다.

  • 좀 더 자세히 말하자면, 컴포넌트 코드 다운로드와 함께, 컴포넌트와 연관된 loader, action등의 함수의 실행도 병렬적으로 수행한다는 것입니다.
import Home from './Home';
import Dashboard from './Dashboard';

// lazy 설정된 라우트들은 해당 라우트로 이동할 때 컴포넌트의 코드를 다운로드 합니다.
// 각 라우트들은 지정된 컴포넌트(라우트 모듈)들을 동적 import한 결과를 반환합니다.
const router = createBrowserRouter([
  {path: '/', lazy: () => import('./Home')},
  {path: '/dashboard', lazy: () => import('Dashboard')}
]);

// 라우트 모듈 내부에서는 라우트에 대해 정의하고 싶은 속성들을 export 합니다.
export async function loader({ request }) {
  let data = await fetchData(request);
  return json(data);
}
export function Component() {
  let data = useLoaderData();
  return (
    <>
      <h1>You made it!</h1>
      <p>{data}</p>
    </>
  );
}
export function ErrorBoundary() {
  let error = useRouteError();
  return isRouteErrorResponse(error) ? (
    <h1>
      {error.status} {error.statusText}
    </h1>
  ) : (
    <h1>{error.message || error}</h1>
  );
}

예시로 React Router에서 제공하는 lazy 기능을 사용하면 설정된 라우트를 동적으로 지연 로딩하여 초기 번들의 크기를 줄일 수 있습니다.

참고