개발 블로깅/Improving Performance

Preact로 번들 사이즈 대폭 줄이기

Hello이뇽 2023. 10. 15. 18:37

 

 

아마 React를 많이 다루어본 사람들이라면 Preact를 한번씩 들어보았을 것이다.
React와 굉장히 유사하지만 엄연히 다른 라이브러리로 존재하는 PReact.
이미 React가 강력하게 점유하고 있기 때문에, PReact를 가볍게 들여다보기만 하거나 크게 관심을 가질 정도의 라이브러리가 아니였을 수도 있을 것이다. (우선 나는 그랬다..) 

그러나 Preact를 가지고 React 프로젝트를 굉장히 가볍게 만들 수 있다는 것을 알게 되고 나서, 다시금 굉장히 관심을 가지게 되면서 Preact에 대해 어느정도 리서치를 해보게 되었다. 

이번 글에서는 Preact가 React와 어떤 점이 다른지, 어떤 원리로 React 코드의 번들 사이즈를 줄일 수 있는지, 추가로 Preact에서는 Server Side 환경도 React와 동일하게 문제가 없을지 정리해보려고 한다.

 

Preact란, 그리고 React와의 차이

우선 쉽게 소개하자면, React의 경량화된 버전이라고 생각하면 된다.
Preact 공식문서에 들어가보기만 해도 React를 대체할 수 있는 빠르고 가벼운 라이브러리임을 소개하고 있다.

 

 

기존의 React의 경우는 브라우저 내 DOM 요소에 이벤트 헨들러를 등록할 때 SyntheticEvent 이벤트 핸들러를 통해 브라우저 API에 접근한다.

그러나 PReact는 Synthetic Event System을 구현하지 않고 이벤트 핸들러를 직접 실제 브라우저의 AddEventListener를 사용하기 때문에, PReact 자체에 SyntheticEvent의 구현체가 존재하지 않고, 순수 Javascript/DOM으로써 동일하게 이벤트 네이밍과 행동으로 동작한다.

또한, React는 Virtual DOM이란 레이어를 두고 DOM요소에 접근을 했다면, PReact는 실제 DOM 요소와 밀접하게 접근하여 동작한다.

이러한 React에서 사용되던 두꺼운 레이어 요소들이 줄어들면서 PReact에서 코드 사이즈는 줄어들면서 React와 동일한 방식으로 동직시킬 수 있게 되는 것이다.

 

preact/Compat

기존의 React 프로젝트 코드 베이스에서 PReact로 대체할 수 있도록 호환해주는 모듈이다.
React API 위에 하나의 레이어 추상화를 두고 PReact를 사용할 수 있도록 preact/compat을 제공한다.

React와 react-dom요소들을 빌드 단에서 Preact로 변경해주면 된다. webpack config 설정에서 resolve.alias 섹션에서 react 관련된 요소드를 preact로 바꾸도록 설정해주면 된다.

const config = {
   //...snip
  "resolve": {
    "alias": {
      "react": "preact/compat",
      "react-dom/test-utils": "preact/test-utils",
      "react-dom": "preact/compat",     // Must be below test-utils
      "react/jsx-runtime": "preact/jsx-runtime"
    },
  }
}

 

JSX 구조

JSX 구문은 Nested Funciton Call 코드로 변환이 되는데, 기존의 React에서 제공하던 createElement이 Preact에선 hyperscript에서 제공하는 h로 사용된다.

Source: (JSX)

<a href="/">
  <span>Home</span>
</a>

 

Output:

// Preact:
h(
  'a',
  { href:'/' },
  h('span', null, 'Home')
);

// React:
React.createElement(
  'a',
  { href:'/' },
  React.createElement('span', null, 'Home')
);

 

Preact에서 hyperscript를 채택한 이유가, 현재 리액트는 JSX 구문을 React에서 제공하는 createElement 등과 같은 함수로 변환하면서 코드를 동작시키지만, 이러한 nested function call 형식으로 트리 구조를 구축하는게 JSX가 나오기 훨씬 전부터 존재하던 패러다임이고, 당시에는 hyperscript Project를 통해 Javascript 생태계에 대중화가 되었다고 한다.
Preact에서는 이러한 hyperscript가 React의 생태계보다 훨씬 더 가치가 있다고 판단했고, 원래의 일반화된 커뮤니티 표준을 가져가기로 결정했기에 Preact에서는 hyperscript를 사용하기로 했다고 나와있다.

 

Server Side 관련 확인

preact의 Server Side Rendering 과정은 어떨까?

기존의 React에선 render-to-string 함수를 통해 React 코드를 서버 단에서 렌더링 후 HTML Document로 생성하여 클라이언트 단으로 전송한다.
react-dom/server에서는 아직 Suspense를 지원하지 않아, Data Fetching이 아직 완료되지 않은 상태에서 그냥 Server Side Rendering을 하게되면 Suspense의 Fallback된 요소로 렌더링이 되서 원하던 방식과는 다른 HTML 코드가 생성될 수 있다.

이러한 문제를 react-ssr-prepass라는 모듈을 사용하면, Data Fetching이 모두 완료될 때까지 기다린 후 render To String이 실행되면서 정상적으로 원하던 HTML을 가져오게 된다.

 preact에서도 역시나 preact-render-to-string, preact-ssr-prepass 함수가 존재한다.

import { createElement as h } from 'preact';
import { Suspense, lazy } from 'preact/compat';
import renderToString from 'preact-render-to-string';
import prepass from 'preact-ssr-prepass';

const LazyComponent = lazy(() => import('./lazy'));

const vnode = (
    <Suspense fallback={<div>I shall not be rendered on the server</div>}>
        <LazyComponent />
    </Suspense>
);

prepass(vnode)
    .then(() => {
        // <div>I shall be loaded and rendered on the server</div>
        console.log(renderToString(vnode));
    });

 

기존의 React의 Server Side 동작에 필요한 요소들이 Preact에도 우선 동일하게 가지고 있는 듯 해서 문제가 없을 것이라고 생각하였으나, 한 가지 호환이 안되는 문제가 있었다.

Next.js 13에서부터 서버 사이드 HTML을 Streaming 형식으로 내려주는 방식을 채택하면서, renderToReadableStream을 사용하게 되었는데, preact/compat에서 아직 이에 대해 지원해주는게 없다.

근데 이러한 해결한 문제를 해결한 여러 Resitory들은 발견할 수 있으나, preact에서 공식적으로 제공하는 코드는 아니므로 적용에는 주의해야할 필요가 있다. 

 

GitHub - lfre/next-13-preact: Patch Next.js 13 to use Preact

Patch Next.js 13 to use Preact. Contribute to lfre/next-13-preact development by creating an account on GitHub.

github.com

 

따라서 Next.js 프로젝트를 PReact로 변경하려면 하려면, Next.js 12이하까지만 가능할 것 같다.

 

React로 구축한 프로젝트에 PReact를 사용하도록 설정

Preact에서 react를 호환할 수 있도록 preact/compat을 제공하니, 우리는 이것을 이용해서 빌드할 때 react 코드들을 preact 코드들을 사용하도록 변경만 해주면 된다.

webpack설정에서 빌드 시 react관련 요소들을 preact/compat을 참조하도록 변경하도록 설정하면 된다.

module.exports = {
  ...
  webpack: (config, { webpack }) => {
    Object.assign(config.resolve.alias, {
        react: 'preact/compat',
        'react-dom': 'preact/compat',
        'react/jsx-runtime.js': 'preact/compat/jsx-runtime',
        'react-dom/test-utils': 'preact/test-utils',
    });
    
    return config;
  }
}

 

Preact로 변경 후, 번들 사이즈 비교

기존에 운영하던 특정 Next.js 프로젝트 기준으로 비교를 해보았다.

Preact 적용 전 번들 사이즈

 

Preact 적용 후 번들 사이즈

 

번들 사이즈가 대폭 줄었다. (굉장하다..)
Production 환경에서 동작하는데는 문제가 없는지, 사용 중인 모듈이나 Sentry 같은 에러 모니터링 모듈 등 문제가 없는지 확인해 보았고 전체적으로 이상이 없는 것 같다.

Webpack 설정을 통해 React에서 Preact로 변경한 것만으로도 엄청나게 가벼운 페이지로 변경할 수 있지만, 아직 Next.js 12이하까지만 호환이 되는 문제가 있어서 많이 아쉬운 상태인데, SSR이 아닌 Client Side 프로젝트라면 무리없이 사용할 수 있을 것 같다.

반응형