본글에서는 react, vite를 이용해서 라이브러리를 만드는 방법에 대해 작성하고자 합니다

Bundle?

웹 개발 및 프론트엔드 개발에서 주로 사용되는 용어로, 일반적으로 여러 파일을 하나로 결합 하는 작업을 나타낸다

이렇게 하나의 파일로 결합하는 것을 "번들링" 이라고도 하는데 번들링은 주로 JavaScript 및 CSS 파일을 하나의 파일로 결합하는 것을 의미한다.

이를 통해 웹 애플리케이션의 성능을 최적화하고 관리를 더 쉽게 만드는데 도움이 된다. 

왜 Vite 인가?

번들링 시, Rollup 기반의 다양한 빌드 커맨드를 사용할 수 있습니다.

이는 높은 수준으로 최적화된 정적(Static) 리소스들을 배포할 수 있게끔 하며, 미리 정의된 설정(Pre-configured)을 제공합니다.

또한 스토리북에서도 vite를 기반으로 연동할수 있어 편리하게 스토리북까지 이용가능합니다.

Vite Config & Build

config 구성하는 방법은 다음과 같습니다

1. vite project 생성

pnpm create vite {package name}

2. package install

# react
pnpm add react react-dom

# vite config package
pnpm add -D vite vite-plugin-dts @vitejs/plugin-react vite-tsconfig-paths rollup-preserve-directives glob @vanilla-extract/vite-plugin

# vanilla-extract package
pnpm add -D @vanilla-extract/css

3. vite.config.ts 설정

import { extname, relative, resolve } from 'path';
import { fileURLToPath } from 'url';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import react from '@vitejs/plugin-react';
import { glob } from 'glob';
import preserveDirectives from 'rollup-preserve-directives';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
import packageJson from './package.json';

export default defineConfig({
  plugins: [
    react(),
    dts({
      rollupTypes: true, // index.d.ts로 병합
    }),
    tsconfigPaths(),
    vanillaExtractPlugin({
      identifiers: 'debug',
    }),
    preserveDirectives(), // use client 유지
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      formats: ['es'],
    },
    rollupOptions: {
      external: [
        ...Object.keys(packageJson.peerDependencies || {}),
        ...Object.keys(packageJson.dependencies || {}),
        'react/jsx-runtime',
      ],
      input: Object.fromEntries(
        glob
          .sync(['src/**/*.{ts,tsx}'], {
            ignore: ['src/**/*.d.ts', 'src/**/*.stories.{ts,tsx}'],
          })
          .map(file => {
            return [
              relative('src', file.slice(0, file.length - extname(file).length)),
              fileURLToPath(new URL(file, import.meta.url)),
            ];
          }),
      ),
      output: {
        chunkFileNames: 'chunk/[name].js', // 외부모듈 관련 파일 chunk/모듈명.js 로 생성
        assetFileNames: 'theme.css', // 전체 css는 dist/theme.css에 생성됩니다
        entryFileNames(info) {
          if (!info.exports.length) {
            return 'rmdir/[name].js';
          }
          return '[name].js'; // 모든 파일의 이름을 [파일명].js로 지정합니다
        },
      },
    },
  },
});

4. package.json 수정

{
  "name": "@breadlee/ui",
  "version": "0.0.0",
  "sideEffects": false,
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./theme.css": "./dist/theme.css",
    "./reset.css": "./dist/reset.css"
  },
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "prebuild": "[ ! -d dist ] && mkdir -p dist || rm -rf dist/*",
    "build": "vite build",
    "postbuild": "rm -rf dist/rmdir",
    ...
  },
  "dependencies": {
  	...
  }
  "peerDependencies": {
	...
    "react": "^19.0",
    "react-dom": "^19.0"
  },
  "peerDependenciesMeta": {
 	...
  }
}

 

5. build

pnpm build

 

사용

pnpm add @breadlee/ui

import '@breadlee/ui/reset.css';
import '@breadlee/ui/theme.css';
import { ThemeProvider } from 'next-themes';
import { Typography } from '@breadlee/ui';

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang='ko'>
      <body>
        <ThemeProvider>
        	{children}
            <Typography>test</Typography>
        </ThemeProvider>
      </body>
    </html>
  );
}

input 의 value를 변경하기 위해선 다음과 같이 코드를 작성합니다

const ControlledComponent = () => {
  const [value, setValue] = useState('');

  const handleSubmit = () => {
    onSubmit(value);
  };
  

  return (
    <form onSubmit={handleSubmit}>
      <input type='text' value={value} onChange={e => setValue(e.target.value)} />
      <button type='submit'>Submit</button>
    </form>
  );
};

const UnControlledComponent = () => {
  const inputRef = useRef();

  const handleSubmit = () => {
    const value = inputRef.current.value;
    onSubmit(value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type='text' ref={inputRef} />
      <button type='submit'>Submit</button>
    </form>
  );
}

ControlledComponent에선 state를 사용하여 value를 변경중이고 UnControlledComponent는 input tag의 기본기능을 활용하여 value를 변경합니다

Controlled Component vs UnControlled Component

구분 Controlled Component UnControlled Component
정의 컴포넌트의 상태가 React에 의해 제어되는 컴포넌트 컴포넌트의 상태가 DOM 자체에 의해 제어되는 컴포넌트
데이터 흐름 단방향 데이터 흐름 (부모 -> 자식) 양방향 데이터 흐름 (DOM -> React)
장점 - 상태를 명확하게 관리할 수 있어 디버깅이 용이함
- 입력값에 대한 즉각적인 유효성 검사가 가능함
- 모든 상태 변화가 React의 상태로 관리되므로 예측 가능성이 높음
- 초기 설정이 간단하고 빠름
- 외부 라이브러리와의 통합이 쉬움
단점 - 코드가 복잡해질 수 있음
- 성능에 영향을 줄 수 있음 (특히 많은 양의 데이터 처리 시)

- 상태를 추적하기 어려움
- 입력값의 유효성 검사가 어려움
- 초기값 설정이 복잡할 수 있음

사용 예시 - 폼 데이터가 복잡하고, 입력값에 대한 즉각적인 피드백이 필요한 경우 - 간단한 폼이나 초기값이 중요하지 않은 경우

 

react-hook-form 사용이유?

UnControlled Component를 사용하면 react에 의해 제어되지않아 데이터 관리가 어려울수 있습니다

form내에서 사용되는  input, select, textarea 같은 컴포넌트는 submit시 value값들을 가져오는데 어려움을 겪을수 있는데요

하지만 https://react-hook-form.com/과 같이 라이브러리를 이용한다면 좀더 수월하게 UnControlled Component를 다룰수 있고

form의 최적화도 할수 있게 됩니다

 

참고

Virtual DOM

Virtual DOM은 UI의 이상적인 또는 “가상”적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제” DOM과 동기화하는 프로그래밍 개념입니다

VirtualDom

 

리액트의 렌더링 과정

리액트의 렌더링 프로세스는 어플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정을 의미하며 총 3 가지로 나뉩니다

  1. 렌더링을 유발하는 단계로 createRoot의 실행 혹은 state를 업데이트하게 되면 발생한다.
  2. 렌더링단계로 컴포넌트를 호출하는 단계이다. createRoot로 렌더링이 발생했다면 루트 컴포넌트를 호출하고 state의 업데이트로 인한 렌더링이라면 해당 state가 속해있는 컴포넌트를 호출한다.
  3. 커밋 단계로 변경사항들을 실제 DOM에 적용하는 작업을 진행한다. 첫 커밋이라면 appendChild를 사용해서 스크린에 있는 모든 노드를 생성한다. 만약 첫 커밋이 아니라면 최소한의 작업을 통해 변경사항만을 실제 DOM에 적용한다. 그리고 이 변경사항은 렌더링 중에 계산된다.

렌더링이 일어나는 이유

  1. 최초 렌더링 
  2. 리렌더링
    1. state가 변경되었을때
    2. props가 변경되었을때
    3. 컴포넌트의 key props가 변경되었을때

JSX 문법이 해석 되는과정

1. 컴포넌트 작성

function Component(){
  return(
    <div classname="title">
    	<h1>HELLO</h1>
    </div>
    );
}

2. 컴파일

function Component(){
  return React.createElement("div", {
    className: 'title'
  }, React.createElement("h1", null, "HELLO"));
}

// React elements 는 DOM 요소를 virtual DOM으로 표현
// virtual DOM은 메모리에 저장되며 실제 렌더되지 않으므로 연산비용이 적음
const element = {
  type: 'div',
  props: {
    className: 'title',
    children: [
      {
        type: 'h1',
       	children: 'HELLO',
      }
      ]
  }
};

3. Reconciliation (재조정)
4. 변경사항 커밋

 ReactDOM.render(element, document.getElementById('title'));

Diffing Algorithm & Reconciliation (재조정)

 

기존의 DOM 트리를 새로운 트리로 변환하기 위하여 최소한의 연산을 하는 알고리즘을 사용합니다
이때 알아낸 조작 방식은 알고리즘 O(n^3) 의 복잡도를 가지고 있다. 만약,이 알고리즘을 React에 적용한다면, 1000개의 엘리먼트가 있다는 가정하에 실제 화면에 표시하기 위해 1000^3인 10억번의 비교 연산을 해야합니다
이는 너무 비싼 연산이기에 React는 두 가지 가정을 가지고 시간 복잡도 O(n)의 새로운 Heuristic Algorithm을 구현했습니다

  1. 각기 서로 다른 타입을 가진 두 요소는 다른 트리를 구축한다.
  2. 개발자가 제공하는 key 프로퍼티를 통해 자식 요소의 변경 여부를 표시할 수 있다

=> 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있다.
렌더링 프로세스 단계에서 Diffing Algorithm을 통해 변경사항을 체크하고
Reconciliation(재조정)을 통해 DOM을 다시 업데이트 시켜주게 됩니다

ReactFiber

React 16 이전에는 Stack Reconciler를 사용했으며 동작방식은 모든작업을 스택으로 처리하고 동기적으로 이루어져 작업이 오래걸린다면
메인 쓰레드에 과부하가 걸릴 정도의 고연산 작업이라면 유저 경험을 심각하게 저해시킬 정도의 렌더링 문제가 유발될 수 있었습니다
그래서 ReactFiber가 도입되었고 이를 증분 렌더링 이라고 합니다.
주 역할은 다음과 같습니다

  • 연산을 멈추고 다시 수행할 수 있는 기능
  • 각기 역할마다 다른 우선순위를 부여할 수 있는 기능
  • 이전에 완료된 연산을 재사용할 수 있는 기능
  • 필요가 없어진 연산을 중간에 취소하는 기능

ReactFiber Tree

FiberTree

  • type: 생성된 function Component
  • child: 해당 Component의 가장 왼쪽에 있는 자식 노드
  • sibling: 해당 Component의 형제 노드

Fiber Tree는 Fiber Tree, WokinProgress Tree 총 두개로 구성되어있습니다

  • FiberTree: 현재 모습을 담은 트리
  • WorkInProgressTree: 작업 중인 상태를 나타내는 트리

리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경해 현재 트리를 workInProgress트리로 바꿔버립니다. (이러한 기술을 더블 버퍼링이라고 하며 커밋 단계에서 수행됩니다.)
두 개의 트리가 존재하기 때문에 리액트는 미처 렌더가 끝나지 않은 모습을 노출시키지 않을 수 있고. 리액트 파이버 트리의 작업 단계는 다음과 같습니다.

  1. 모든 작업은 현재 UI 렌더링을 위해 존재하는 current 트리를 기준으로 시작된다.
  2. 만약 업데이트가 발생할 경우 리액트에서 새로 받은 데이터로 workInProgress 트리를 빌드하기 시작한다.
  3. 이 workInProgress 트리를 빌드하는 작업이 마무리되면 다음 렌더링에 이 트리를 사용한다.
  4. 이 workInProgress 트리가 UI에 최종적으로 렌더링되어 반영이 완료되면 current가 workInProgress 트리로 변경된다.

 

FiberTree 와 WorkInProgressTree

React Fiber 도입 효과

  1. 성능을 향상시켰습니다. 재조정하는 동안 다른 작업을 중지하지 않기 때문에 React는 필요할 때마다 작업을 일시 중지하거나 렌더링을 시작할 수 있게 됐습니다.
  2. 훨씬 깔끔한 방식으로 오류를 처리할 수 있게 됐습니다. 자바스크립트 런타임 오류가 발생할 때마다 흰색 화면을 표시하는 대신 Error Boundary를 설정하여 문제가 발생할 경우 백업 화면을 표시할 수 있게 됐습니다.
  3. ※ 리액트 파이버의 도입으로 Error Boundary, Suspense, React.Lazy, Fragment 그리고 Concurrency Mode가 가능해졌습니다.

참고

브라우저

웹에서 페이지를 찾아서 보여주고, 사용자가 하이퍼링크를 통해 다른 페이지로 이동할 수 있도록 하는 프로그램으로
브라우저는 유저가 선택한 자원을 서버로 부터 받아와서 유저에게 보여준다. 이 자원은 페이지 외에도 이미지, 비디오 등의 컨텐츠들도 포함된다. 받아온 자원들을 렌더링 과정을 통해 유저에게 보여주게 된다.
브라우저의 종류는 chrome, safari, firefox 등이 있다
 

Dom

Document Object Model의 약자로 문서 객체 모델을 의미한다.
문서 객체란 html, head, body와 같은 태그들을 javascript가 이용할 수 있는 (메모리에 보관할 수 있는) 객체를 의미한다.

Dom Tree



브라우저 렌더링 과정

  1. HTML 파일과 CSS 파일을 파싱해서 각각 Tree를 만든다. (DOM, CSSOM 을 생성) (Parsing)
  2. 두 Tree를 결합하여 Rendering Tree를 만든다. (Style)
  3. Rendering Tree에서 각 노드의 위치와 크기를 계산한다. (Layout)
  4. 계산된 값을 이용해 각 노드를 화면상의 실제 픽셀로 변환하고, 레이어를 만든다. (Paint)
  5. 레이어를 합성하여 실제 화면에 나타낸다. (Composite)

렌더링 과정

 

리플로우와 리페인트중 어느것이 더 부하가 클까?

  • 리플로우 (reflow)
  • DOM 요소의 기하학적 속성이 변경될때, 브라우저 사이즈가 변할때, 스타일시트가 로딩되었을때 발생하는 변화들을 다시 계산 해주는 작업을 뜻하고 레이아웃(Layout) 이라고도 한다.
  • 리페인트 (repaint)
  • 변경된 요소를 실제로 화면에 그려주는 작업을 리페인트라고 한다. 그래서 리플로우가 발생하면 필연적으로 리페인트가 실행된다.

리페인트도 굉장히 무거운 작업이긴 하지만 리플로우 처럼 모든 요소들에 대한 기하학적 정보들을 계산해주는 작업은 아니기 때문에 리플로우 보다는 상대적으로 훨씬 가벼운 작업이다
 

Dom을 직접조작하는것은 느릴까?

과거 IE와 같은 브라우저에서는 DOM은 일반적으로 객체 대신 노드(Node)를 사용하여 계층 구조를 형성하고 이를 탐색하며 메모리를 관리하는 방식을 사용해 자바스크립트와 DOM 간의 데이터 교환과 상호작용이 비효율적이었다
 
하지만 모던 브라우저(Chrome, Safari)는 DOM도 자바스크립트와 완전히 동일한 메모리 모델을 쓰기 때문에 자바스크립트의 객체를 다루는 것과 속도 차이는 거의 없다
 

Module Federation 이란?

JavaScript 아키텍처 유형 중 하나로  단일 Webpack 빌드에 포함된 모듈뿐만 아니라 여러 서버에 배포되어 있는 원격 모듈을 하나의 애플리케이션에서 로딩할 수 있는 기능입니다

Webpack 5 부터 코어로 추가 되었습니다

  • 웹 애플리케이션을 여러 개의 독립적인 모듈로 나누고, 이러한 모듈을 동적으로 로드하거나 언로드할 수 있는 웹 개발 아키텍처의 한 형태입니다. 주로 웹 애플리케이션의 규모가 커지고 복잡해질 때 유용하게 사용됩니다.
  • 핵심 개념은 각 모듈이 독립적으로 개발되고 배포될 수 있다는 것입니다. 각 모듈은 자체적으로 독립적으로 동작하며, 필요에 따라 애플리케이션에 동적으로 통합될 수 있습니다. 이는 전체 애플리케이션을 더 작고 관리하기 쉬운 조각으로 나눌 수 있게 해줍니다.
  • 이 아키텍처의 장점 중 하나는 각 모듈이 독립적으로 개발될 수 있으므로, 다른 모듈의 변경이나 업데이트가 전체 애플리케이션에 미치는 영향을 최소화할 수 있다는 것입니다. 또한, 필요에 따라 모듈을 동적으로 로딩하고 언로딩할 수 있어서 초기 로딩 속도를 최적화하고 사용자 경험을 향상시킬 수 있습니다.
  • Module Federation은 웹 애플리케이션의 확장성과 유연성을 높일 수 있는 강력한 아키텍처 중 하나로 평가되고 있습니다.

용어

  • Host : 페이지 로드 중(onLoad 이벤트가 트리거될 때) 처음 초기화되는 Webpack 빌드. Runtime에 Remote를 load 할수 있습니다
  • Remote : 또 다른 Webpack 빌드로, 그 일부가 " 호스트 " 에 의해 사용됩니다.
  • Bidirectional-hosts (양방향 호스트) : 번들 또는 Webpack 빌드가 호스트 또는 원격으로 작동할 수 있는 경우입니다. 런타임 시 다른 애플리케이션을 사용하거나 다른 애플리케이션에 의해 사용됨니다.
  • Omnidirectional-host (전방향 호스트): 호스트 자체는 시작 시 호스트인지 원격인지 알 수 없습니다. 이를 통해 webpack은 호스트 자체 공급업체를 semver 규칙을 기반으로 하는 공급업체로 변경할 수 있습니다. 필요한 경우 여러 버전을 허용합니다.

Module federation 적용 시 기대효과

서비스를 개발하고 성공해 나가다보면 규모가 점점 커질 수 있다. 서비스 규모가 커질수록 작은 변경에도 영향 범위가 커질 수 있으며 영향 범위 예측도 점점 복잡하고 어려워진다. Module Federation은 컨테이너 빌드를 따로 하므로 빌드 시간, 영향 범위, 로딩 시간 측면에서 유리하다.

구분 기존 방식 Module Federation 적용 시 기대효과
빌드범위와 배포시간 작은 변경에도 전체 빌드를 하고 배포한다. 변경된 컨테이너만 빌드하고 배포해서 시간이 줄어든다.
영향도 전체 서비스를 대상으로 영향도를 검증한다. 변경 영향이 해당 컨테이너에 국한되므로 검증 범위도 줄어든다.
(단, 원격 모듈의 인터페이스를 변경했다면 호스트 앱도 검증이 필요하다.)
로딩시간 전체 빌드가 변경되었으므로 배포 직후 로딩 시간도 오래 걸린다.
(브라우저 캐시 적용 안됨)
배포한 컨테이너의 원격 모듈만 새로 로딩하므로 배포 직후 로딩 시간도 상대적으로 짧다.

구현

Remote

remote 는 React v18.2.0, Webpack 5 Tailwindcss, Typescript 기반으로 프로젝트 구성을 하였습니다

// Component

import '../../tailwind.css';

export interface NavbarProps {}

const Navbar = () => {
  return (
    <ul className="flex gap-1">
      <li className="flex justify-center items-center border-solid border-b-2">
        <a href="https://www.naver.com">네이버</a>
      </li>
      <li className="flex justify-center items-center border-solid border-b-2">
        <a href="https://google.com">구글</a>
      </li>
      <li className="flex justify-center items-center border-solid border-b-2">
        <a href="https://www.daum.net">다음</a>
      </li>
    </ul>
  );
};

export default Navbar;
// webpack.config.js

const path = require('path');
const { EnvironmentPlugin } = require('webpack');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = (_, argv) => ({
  devServer: {
    static: { directory: path.resolve(__dirname) },
    port: 3000,
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.m?js/,
        type: 'javascript/auto',
        resolve: {
          fullySpecified: false,
        },
      },
      {
        test: /\.(css|s[ac]ss)$/i,
        use: ['style-loader', 'css-loader', 'postcss-loader'],
      },
      {
        test: /\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-typescript',
              ['@babel/preset-react', { runtime: 'automatic' }],
              '@babel/preset-env',
            ],
            plugins: ['@babel/transform-runtime'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote',
      filename: 'remoteEntry.a350ed3e.js',
      exposes: {
        './Navbar': './src/components/Navbar/index.tsx',
      },
      shared: {
        ...deps,
      },
    }),
    new HtmlWebPackPlugin({
      template: './public/index.html',
    }),
  ],
  output: {
    chunkFilename: '[id].a350ed3e.bundle.js',
    publicPath: 'auto',
    clean: true,
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
    alias: {
      '@components': path.resolve(__dirname, 'src/components/index.ts'),
    },
  },
});

 

Host

host는 Nextjs SCSS, Typescript 기반으로 구성되어있으며 Remote app을 integration 해보았습니다

// nextjs에서 module federation을 사용하기 위해선 @module-federation/nextjs-mf를 이용해야 합니다
pnpm add -D @module-federation/nextjs-mf
//next.config.js

const NextFederationPlugin = require('@module-federation/nextjs-mf');

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack(config, options) {
    const { isServer } = options;
    if(!isServer) {

      config.plugins.push(
        new NextFederationPlugin({
          name: 'host',
          remotes: {
            remote: `remote@http://localhost:4000/remoteEntry.a350ed3e.js`,
          },
          filename: 'static/chunks/remoteEntry.a350ed3e.js',
        }),
        );
        
      }
      return config;
  },
};

module.exports = nextConfig;
// Component

import dynamic from "next/dynamic";

const Navbar = dynamic(() => import('remote/Navbar'), { ssr: false, loading: () => <>...loading</>})

export default function Home() {
  return (
    <div>
      hello world
      <Navbar />
    </div>
  );
}

동작방식

  • main.js: 최초에 앱을 초기화하는 역할을 합니다. 가장 상위의 Micro App A의 HTML에서 가장 먼저 불러와지는 청크가기도 합니다. Micro App A의 Container Reference를 가진 청크로, 해당 처리를 해줍니다. 이 외에도 createRoot등, 브라우저에서도 앱을 최초로 초기화하는데 필요한 코드들을 가지고 있습니다.
  • remoteEntry.js: Container을 가진 Micro App을 초기화하는 청크입니다. 특정 마이크로 앱에서 다른 Micro App을 import할 때 가장 먼저 불리는 청크가기도 합니다. Micro App B, C의 Container를 가진 청크로, 해당 처리를 해줍니다.
  • XXX.js : 이외 청크입니다. 공유 의존성 청크과 Micro App 본문에 대한 청크입니다. main.js, remoteEntry.js내부의 런타임 처리를 통해 로드가 제어됩니다.

Remote 를 빌드한 이후 Host에서 호출하면 다음과 같이 Runtime Integration 을 하게 됩니다

Host
http://localhost:3000
Remote
http://localhost:4000
Remote build file & Host network call

 

참고자료

'Frontend' 카테고리의 다른 글

[React] vite를 이용한 react library 만들기  (4) 2024.12.30
[React] UnControlled Component  (3) 2024.12.24
[React] ReactFiber와 렌더링 과정  (6) 2024.12.24
[WEB] 브라우저 렌더링 과정  (0) 2024.12.23
[React] Polymorphic component  (5) 2024.01.28

Polymorphic component란?

  • 다양한 Semantic을 표현할 수 있는 UI 컴포넌트
  • 다양한 속성을 가질 수 있는 UI 컴포넌트
  • TypeScript를 사용하면 정적 유형 지정을 활용하여 강력한 유형의 다형성 구성 요소를 생성하고 유형 안전성을 제공하고 잠재적인 런타임 오류를 방지

개발

버튼 컴포넌트를 만든다고하면 다음과 같이 구성될수 있고 anchor 태그와 button 태그가 사용될 수 있습니다

interface ButtonProps {
    children: ReactNode;
    onClick(e: HTMLButtonElement | HTMLAnchorElement)?: void;
    href?: string;
    target?: HTMLAttributeAnchorTarget;
    className?: string;
    size?: 'xlarge' | 'large' | 'medium' | 'small';
    color?: 'primary' | 'secondary';
}
 
const Button = ({ children, onClick, href, target, className, size, color }: ButtonProps) => {
    const StyledButton = (
        <span className={`button__box--${size} button__box--${color}`}>
            <Typography>{children}</Typography>
        </span>
    );
 
    if (href) {
        return (
            <a className="button" href={href} target={target}>
                {StyledButton}
            </a>
        );
    }
    return (
        <button className="button" onClick={onClick}>
            {StyledButton}
        </button>   
    );
};

 

위와 같은 방식으로 제작시 다음과 같은 문제가 발생합니다

  1. anchor 태그 또는 button 태그에 필요한 attr이 생길때마다 ButtonProps를 정의해주어야 하고 추가 해주어야한다.
  2. Next에서 사용시 anchor태그 대신 Link 컴포넌트를  사용해야한다.
  3. props에 href를 미사용하였을때는 onClick props는 필수값이지만 현재는 무조건 optional로 되어있다.

따라서 사용성이 더 용이할수있게 컴포넌트를 설계하여야합니다

generic을 활용하여 다형성 컴포넌트를 개발할수있습니다

type PolymorphicComponentProps<T extends React.ElementType, PropsType = Record<string, unknown>> = 
  { component?: T; } & PropsType &
    React.ComponentPropsWithoutRef<T> & { ref?: React.ComponentPropsWithRef<T>['ref']; };

interface ButtonProps {
  children: React.ReactNode;
  size?: 'xlarge' | 'large' | 'medium' | 'small';
  color?: 'primary' | 'secondary' | 'black';
}

const Button = <T extends React.ElementType = 'button'>({ component, children, size, color, ...props }: PolymorphicComponentProps<T, ButtonProps>) => {
  const Component = component ?? 'button';
  
  return (
    <Component className="button" {...props}>
        <span className={`button__box--${size} button__box--${color}`}>
            <Typography>{children}</Typography>
        </span>
    </Component>
  )
};

 

사용예시 및 타입추론

PolymorphicComponentProps  type 은 component props에 element를 받아

React.ElementType과 PropsType을 제네릭으로 해당 element의 attr 타입 및 PropsType을  합쳐 Intersection type으로 추론하게 됩니다

 

사용 예시는 다음과 같습니다

// Next에서 사용
<Button component={Link} href="https://leeyc924.tistory.com" prefetch={false}>버튼</Button>
// React, react-router에서 사용
<Button component={Link} to="https://leeyc924.tistory.com">버튼</Button>
// button으로 사용
<Button onClick={handleClick} color="secondary">버튼</Button>

 

component  props 에 anchor 를 넣었을때 onClick의 타입은  React.MouseEventHandler<HTMLAnchorElement>로 추론되는것을 확인할수 있습니다

component  props 에 button을 넣었을때 href 타입은 에러가 발생합니다

component props에 Nextjs의 Link 를 넣으면 prefetch 와 같은 props가 추론되는것을 확인할수있습니다

참고자료

+ Recent posts