Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team
12/13/2024
Timothy @ Daangn Frontend Core
Software Engineer, Frontend @Daangn Frontend Core Team
ex) Senior Lead Software Engineer @NHN Dooray (2021 ~ 2023)
ex) Lead Software Engineer @ProtoPie (2016 ~ 2021)
ex) Microsoft MVP
이 웅재
Module Federation 약 팔려는 것이 아닙니다.
Module Federation 은 계속해서 진화 중입니다.
어제까지 문제가 있었지만, 오늘은 괜찮을 수 있습니다.
하지만 생각보다 문제가 있을 수 있습니다.
적절한 경우에 사용하지 않으면, 힘든 여정이 될 수 있습니다.
Module Federation 은 (서버 측의 Microservices 와 유사한)
JavaScript 애플리케이션의 탈중앙화를 위한 아키텍처 패턴입니다.
Module Federation 은 (서버 측의 Microservices 와 유사한)
JavaScript 애플리케이션의 탈중앙화를 위한 아키텍처 패턴입니다.
이를 통해 여러 JavaScript 애플리케이션 (또는 마이크로 프론트엔드) 간에
코드와 리소스를 공유할 수 있습니다.
Module Federation 은 (서버 측의 Microservices 와 유사한)
JavaScript 애플리케이션의 탈중앙화를 위한 아키텍처 패턴입니다.
이를 통해 여러 JavaScript 애플리케이션 (또는 마이크로 프론트엔드) 간에
코드와 리소스를 공유할 수 있습니다.
여러 작은 JavaScript 애플리케이션이 모여 하나의 커다란 애플리케이션을 이루는
Micro Frontends 를 달성할 수 있는 구체적인 실행 기술 중 하나입니다.
function add(a: number, b: number): number {
return a + b;
}
const PI: number = 3.14;
console.log(add(2, 3)); // 5
console.log(PI); // 3.14
// main.ts
import('./math').then(({
add,
PI,
}) => {
console.log(add(2, 3)); // 5
console.log(PI); // 3.14
});
// math.ts
export function add(
a: number,
b: number,
): number {
return a + b;
}
export const PI: number = 3.14;
Module 을 같은 서버에 둡니다.
Module 은 코드의 재사용성과 유지보수를 향상시키기 위해 코드를 독립된 단위로 분리하고 관리할 수 있도록 하는 구조입니다.
모듈은 변수, 함수, 클래스 등을 외부로 내보내거나(export) 다른 파일에서 가져와(import) 사용할 수 있습니다.
실제로 자바스크립트 런타임에서 모듈 시스템을 활용해서 코드를 실행하는 방식은 여러가지가 있습니다.
대부분 번들러들이 각자의 방식으로 import/export 를 처리합니다.
최신 번들러들은 리소스도 하나의 모듈로 취급합니다.
// main.ts
import { add, PI } from "./math";
console.log(add(2, 3)); // 출력: 5
console.log(PI); // 출력: 3.14
// math.ts
export function add(
a: number,
b: number,
): number {
return a + b;
}
export const PI: number = 3.14;
Module 을 다른 서버에 둡니다.
Module 을 다른 서버에 둡니다.
"exposes
설정을 가진 모듈 페더레이션 빌드 플러그인을 통해 다른 JavaScript 애플리케이션이 사용할 수 있도록 다른 모듈을 노출하는 애플리케이션 ” 을 모듈 페더레이션에서 Provider (Producer) 라고 합니다. Producer 는 Consumer 역할을 할 수도 있습니다.
"remotes
설정이 있는 모듈 페더레이션 빌드 플러그인을 통해 다른 Producers 들의 모듈을 사용하는 애플리케이션"을 Consumer 라고 합니다. Consumer 는 Producer 역할을 할 수도 있습니다.
코드 중복 감소
코드 유지보수성 향상
애플리케이션의 전체 크기 감소
애플리케이션의 성능 향상
코드 유지보수성 향상
애플리케이션의 전체 크기 감소
애플리케이션의 성능 향상
코드 중복 감소
코드 유지보수성 향상
프로바이더와 컨슈머가 연결되는 Edge에서만 주의하면, 관심사를 분리해낼 수 있어요.
마이크로서비스 A의 배포가 마이크로서비스 B의 동작에 영향이 가지 않아야 해요.
휴먼이 유지보수 가능한 범위를 줄여요.
애플리케이션의 전체 크기 감소
애플리케이션의 성능 향상
코드 중복 감소
코드 유지보수성 향상
애플리케이션의 전체 크기 감소
모놀리식 < 여러 마이크로 애플리케이션의 합
단일 애플리케이션의 크기 감소
애플리케이션의 성능 향상
코드 중복 감소
코드 유지보수성 향상
애플리케이션의 전체 크기 감소
애플리케이션의 성능 향상
모듈을 적절히 나누고, dynamic import 를 하고, 적절히 Suspense 를 처리하고...
원래 해야할 일을 하게 해주는 역할
모듈 페더레이션 2.0 은 모듈 내보내기, 로드, 종속성 공유의 핵심 기능뿐만 아니라 동적 유형 힌트, 매니페스트, 페더레이션 런타임 및 런타임 플러그인 시스템을 추가로 제공한다는 점에서 Webpack5 에 내장된 모듈 페더레이션과 다릅니다.
이러한 기능 덕분에 모듈 페더레이션은 대규모 웹 애플리케이션에서 마이크로 프론트엔드 아키텍처로 사용하기에 더욱 적합합니다.
⚡ Code sharing、Dependency reuse
📝 Manifest
🎨 Module Federation Runtime
🧩 Runtime Plugins System
🚀 Dynamic type prompt
🛠️ Chrome Devtool (manifest 만)
런타임에 공유 종속성을 등록하고 원격 모듈을 동적으로 등록 및 로드하는 기능을 지원합니다.
이전의 모듈 페더레이션에서는 모듈 내보내기와 소비 모두 순전히 빌드 동작이었으며 모든 모듈 로딩 프로세스가 빌드 도구에 의해 캡슐화되었습니다. Systemjs나 esmodule과 같은 모듈 로더와 비교하면 다음과 같은 두 가지 이점이 있습니다.
그러나 이 모델은 프로젝트의 유연성과 빌드 플러그인의 유지 관리 비용에 다음과 같은 영향을 줍니다:
따라서 새 버전의 모듈 페더레이션 설계에서는 런타임이 분리되어 여러 빌드 도구가 런타임을 기반으로 모듈 내보내기 빌드, 공유 모듈 정보 수집, 원격 모듈 참조 처리 등을 구현합니다. 공유 종속성 재사용 및 원격 모듈 로딩과 같은 기타 특정 동작은 모두 Runtime에 내장되어 있습니다.
페더레이션 런타임은 새 버전의 모듈 페더레이션의 주요 기능 중 하나입니다. 런타임에 공유 종속성 등록, 원격 모듈의 동적 등록 및 로드를 지원하며 플러그인을 통해 런타임에 모듈 페더레이션의 기능을 확장할 수 있습니다. 빌드 플러그인은 런타임의 기본 구현을 기반으로 합니다.
Federation Runtime 방식 | Build Plugin 방식 |
---|---|
빌드 플러그인과 독립적으로 사용할 수 있으며, 웹팩4와 같은 프로젝트에서 모듈 로딩을 위해 순수 런타임을 직접 사용할 수 있습니다. | 빌드 플러그인에는 Webpack5, Rspack, Vite 이상이 필요합니다. |
모듈의 동적 등록 지원합니다. | 모듈의 동적 등록을 지원하지 않습니다. |
import 구문을 사용한 모듈 로딩을 지원하지 않습니다. |
import 구문과 동시에 모듈 로딩 지원 |
모듈 로딩을 위한 loadRemote 지원 |
모듈 로딩을 위한 loadRemote 지원 |
shared 는 특정 버전 및 인스턴스 정보를 제공해야 합니다. |
shared 에는 특정 버전이나 인스턴스 정보 없이 구성 규칙만 필요합니다. |
shared 종속성은 외부에서만 사용할 수 있으며, 외부 shared 종속성을 사용할 수 없습니다. |
shared 종속성은 특정 규칙에 따라 양방향으로 공유할 수 있습니다. |
런타임의 플러그인 메커니즘을 통해 로딩 프로세스에 영향을 줄 수 있습니다. | 런타임 플러그인 설정을 통해 로딩 프로세스에 영향을 줄 수 있습니다. |
퓨어 런타임은 원격 유형 힌트를 지원하지 않습니다. | 원격 유형 힌트 지원 |
import { init } from "@module-federation/enhanced/runtime";
import React from "react";
import ReactDOM from "react-dom";
init({
name: "host",
remotes: [],
shared: {
react: {
version: "18.3.1",
lib: () => React,
shareConfig: {
singleton: true,
requiredVersion: "^18.3.1",
},
},
"react-dom": {
version: "18.3.1",
lib: () => ReactDOM,
shareConfig: {
singleton: true,
requiredVersion: "^18.3.1",
},
},
},
});
import("./bootstrap");
import { registerRemotes } from "@module-federation/enhanced/runtime";
import App from "./App";
/**
* register remotes
*/
registerRemotes([
{
name: "hashtag_flea_market",
entry: "https://hashtag-flea-market.alpha.karrotwebview.com/remoteEntry.js",
// entry: "http://localhost:4001/remoteEntry.js",
type: "module",
},
{
name: "hashtag_community_agora",
entry:
"https://hashtag-community-agora.alpha.karrotwebview.com/remoteEntry.js",
// entry: "http://localhost:4002/remoteEntry.js",
type: "var",
},
]);
...
import { loadRemote } from "@module-federation/enhanced/runtime";
import { type ElementType, useEffect, useState } from "react";
export function useDynamicImport({ module, scope }: { module: string; scope: string; }) {
const [component, setComponent] = useState<ElementType | null>(null);
useEffect(() => {
if (!module || !scope) return;
const loadComponent = async () => {
try {
const { default: Component } = (await loadRemote(
`${scope}/${module}`,
)) as {
default: ElementType;
};
setComponent(() => Component);
} catch (error) {
console.error(`Error loading remote module ${scope}/${module}:`, error);
}
};
loadComponent();
}, [module, scope]);
return component;
}
모듈 페더레이션은 대부분의 기능을 구현하고 사용자가 기능을 확장할 수 있는 경량 런타임 플러그인 시스템을 제공합니다. 개발자가 개발한 플러그인은 모듈 페더레이션의 기본 동작을 수정하고 다음과 같은 다양한 추가 기능을 추가할 수 있습니다.
컨텍스트 정보 가져오기
수명 주기 후크 등록하기
모듈 페더레이션 구성 수정하기
…
npm 패키지와 마찬가지로 모듈 페더레이션 제품도 타입을 생성하고 유형 핫 리로딩을 지원하지만, 제품이 원격 cdn에서 호스팅됩니다.
@module-federation/enhanced
는 기본적으로 타입 프롬프트 기능을 활성화합니다.
@module-federation/enhanced
에서 제공하는 빌드 플러그인을 사용하여 빌드하면 자동으로 유형 파일이 생성됩니다.
Build Plugin 방식
Federation Runtime 방식
@module-federation/enhanced
에서 제공하는 빌드 플러그인을 사용하여 빌드하면 자동으로 유형 파일이 생성됩니다.
/@mf-types.d.ts
/@mf-types.zip
Build Plugin 방식
Federation Runtime 방식
// tsconfig.json
{
"compilerOptions": {
"paths": {
"*": ["./@mf-types/*"]
}
},
}
// tsconfig.json
{
"include": ["./@mf-types/*"]
}
실제 서비스되는 해시태그 웹뷰를 만드는 T
rsbuild 를 사용해서 웹뷰를 제작
중고 거래팀에서 제공하는 해시태그 피드 아이템 컴포넌트를 만드는 F
vite 를 사용해서 제작
커뮤니티실에서 제공하는 해시태그 피드 아이템 컴포넌트를 만드는 R
rsbuild 를 사용해서 제작
기본적으로 provider 는 build plugin 방식을 사용한다.
official을 사용한다.
import { federation } from "@module-federation/vite";
import { ModuleFederationPlugin }
from "@module-federation/enhanced/rspack";
consumer는 build plugin 방식을 사용하지만,
@module-federation/enhanced/runtime
을 사용해서
동적으로 consume 할 수 있는지 검토한다.
shared module 때문
consumer 는 provider 의 역할을 하지 않는다.
함께 생각해보아요
제 생각은 ?
pnpm add @module-federation/vite -D
const mfConfig = {
name: "hashtag_flea_market",
filename: "remoteEntry.js",
manifest: true,
exposes: {
"./FeedCardFlea": "./src/components/FeedCardFlea.tsx",
},
shared: {
react: {
singleton: true,
requiredVersion: "18.3.1",
},
"react-dom": {
singleton: true,
requiredVersion: "18.3.1",
},
"comma-number": {
version: "2.1.0",
},
"react-lazy-load-image-component": {
version: "1.6.2",
},
"@seed-design/design-token": {
version: "1.0.3",
},
// ERR_REQUIRE_ESM
// "@daangn/react-monochrome-icon": {
// version: "0.0.11",
// },
// ERR_PACKAGE_PATH_NOT_EXPORTED (patch)
"react-intersection-observer": {
version: "9.13.1",
},
// ERR_PACKAGE_PATH_NOT_EXPORTED (patch)
"@daangn/sprout-components-layout": {
version: "0.0.0-main-20240404063637",
},
// ERR_PACKAGE_PATH_NOT_EXPORTED (patch)
"@daangn/sprout-components-text": {
version: "0.0.0-main-20240405062222",
},
},
};
import { federation } from "@module-federation/vite";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import checker from "vite-plugin-checker";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
build: {
modulePreload: false,
target: "esnext",
minify: false,
cssCodeSplit: false,
},
base: process.env.BASE_URL ?? "http://localhost:4001",
server: {
port: 4001,
},
preview: {
port: 4001,
},
plugins: [
process.env.STORYBOOK !== "true" && federation({ ...mfConfig }),
react(),
vanillaExtractPlugin({
unstable_mode: "transform",
}),
checker({
typescript: true,
}),
topLevelAwait(),
],
});
normalize.css 은 모듈에서 제공하지 않는다
vite 플러그인은 아직 타입 생성 기능을 지원하지 않는다.
풀어낼 방법이 필요함
type 만 제공하는 패키지를 만들거나
type 을 다른 플러그인 처럼 제공하는 방식
ERR_PACKAGE_PATH_NOT_EXPORTED 에 대한 대응이 필요하다.
일단 patch
내부 패키지는 지원하는 방향 검토
ERR_REQUIRE_ESM 에 대한 대응이 필요하다.
vanillaExtractPlugin 의 transform 모드를 설정한다.
base 설정이 필요하고, 빌드 타임에 넣어줘야 한다.
mf-manifest.json 안에 값을 들어있다.
singleton 을 기억하자.
pnpm add @module-federation/enhanced -D
const mfConfig = {
name: "hashtag_community_agora",
filename: "remoteEntry.js",
exposes: {
"./FeedCardAgora": "./src/components/FeedCardAgora.tsx",
},
shared: {
react: {
singleton: true,
requiredVersion: "18.3.1",
},
"react-dom": {
singleton: true,
requiredVersion: "18.3.1",
},
"@daangn/react-monochrome-icon": {
version: "0.0.11",
},
"@daangn/sprout-components-layout": {
version: "0.0.0-main-20240404063637",
},
"@daangn/sprout-components-text": {
version: "0.0.0-main-20240405062222",
},
"@seed-design/design-token": {
version: "1.0.3",
},
"comma-number": {
version: "2.1.0",
},
"react-intersection-observer": {
version: "9.13.1",
},
"react-lazy-load-image-component": {
version: "1.6.2",
},
},
};
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { VanillaExtractPlugin } from "@vanilla-extract/webpack-plugin";
export default defineConfig({
html: {
template: "./index.html",
},
source: {
entry: {
index: "./src/entry.tsx",
},
},
dev: {
assetPrefix: "http://localhost:4002",
},
server: {
port: 4002,
},
plugins: [
pluginReact({
reactRefreshOptions: {
exclude: [/node_modules/, /\.css\.ts$/],
},
}),
],
tools: {
rspack: {
plugins: [
new VanillaExtractPlugin({
externals: ["@seed-design"],
}),
process.env.STORYBOOK !== "true" &&
new ModuleFederationPlugin({ ...mfConfig }),
],
},
},
output: {
overrideBrowserslist: ["chrome >= 64", "ios_saf >= 14"],
assetPrefix: process.env.BASE_URL ?? "http://localhost:4002",
filenameHash: true,
},
});
normalize.css 은 모듈에서 제공하지 않는다
rspack 플러그인은 타입 생성 기능을 지원한다.
https://hashtag-community-agora.alpha.karrotwebview.com/@mf-types.zip
consumer 가 rsbuild 이면 너무 편하다.
@vanilla-extract/webpack-plugin
을 사용한다.
assetPrefix 설정이 필요하고, 빌드 타임에 넣어줘야 한다.
mf-manifest.json 안에 값을 들어있다.
설정 자체가 크게 다르지 않다.
pnpm add @module-federation/enhanced -D
const mfConfig = {
name: "hashtag_webview",
remotes: {
hashtag_community_agora: process.env.HASHTAG_COMMUNITY_AGORA_URL
? `hashtag_community_agora@${process.env.HASHTAG_COMMUNITY_AGORA_URL}/mf-manifest.json`
: "hashtag_community_agora@https://hashtag-community-agora.alpha.karrotwebview.com/mf-manifest.json",
},
shared: {
react: {
singleton: true,
requiredVersion: "18.3.1",
},
"react-dom": {
singleton: true,
requiredVersion: "18.3.1",
},
"@daangn/react-monochrome-icon": {
version: "0.0.11",
},
"@daangn/sprout-components-layout": {
version: "0.0.0-main-20240404063637",
},
"@daangn/sprout-components-text": {
version: "0.0.0-main-20240405062222",
},
"@seed-design/design-token": {
version: "1.0.3",
},
"comma-number": {
version: "2.1.0",
},
"react-intersection-observer": {
version: "9.13.1",
},
"react-lazy-load-image-component": {
version: "1.6.2",
},
},
};
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { VanillaExtractPlugin } from "@vanilla-extract/webpack-plugin";
export default defineConfig({
html: {
template: "./index.html",
},
server: {
port: 3000,
},
source: {
entry: {
index: "./src/entry.tsx",
},
define: {
"import.meta.env.VITE_DEV_AUTH_TOKEN": JSON.stringify(
import.meta.env.VITE_DEV_AUTH_TOKEN,
),
"import.meta.env.VITE_SENTRY_DSN": JSON.stringify(
import.meta.env.VITE_SENTRY_DSN,
),
},
},
plugins: [
pluginReact({
reactRefreshOptions: {
exclude: [/node_modules/, /\.css\.ts$/],
},
}),
],
tools: {
rspack: {
plugins: [
new VanillaExtractPlugin({
externals: ["@seed-design"],
}),
process.env.STORYBOOK !== "true" &&
new ModuleFederationPlugin({
...mfConfig,
runtimePlugins: ["./src/mfPlugins"],
}),
],
},
},
output: {
overrideBrowserslist: ["chrome >= 64", "ios_saf >= 14"],
},
});
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext", "DOM"],
"esModuleInterop": true,
"jsx": "preserve",
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"*": ["./@mf-types/*"]
}
}
}
import { registerRemotes } from "@module-federation/enhanced/runtime";
/**
* register remotes
*/
registerRemotes([
{
name: "hashtag_flea_market",
entry:
stage === "production"
? "https://hashtag-flea-market.karrotwebview.com/mf-manifest.json"
: "https://hashtag-flea-market.alpha.karrotwebview.com/mf-manifest.json",
type: "module",
},
]);
import { useDynamicImport } from "../hooks/useDynamicImport";
const CardList: React.FC<CardListProps> = ({
...
}) => {
const FeedCardFlea = useDynamicImport({
module: "FeedCardFlea",
scope: "hashtag_flea_market",
});
...
return (
<>...</>
);
};
export default CardList;
import { loadRemote } from "@module-federation/enhanced/runtime";
import * as Sentry from "@sentry/react";
import { type ElementType, useEffect, useState } from "react";
interface DynamicImportProps {
module: string;
scope: string;
}
export function useDynamicImport({ module, scope }: DynamicImportProps) {
const [component, setComponent] = useState<ElementType | null>(null);
useEffect(() => {
if (!module || !scope) return;
const loadComponent = async () => {
try {
const { default: Component } = (await loadRemote(
`${scope}/${module}`,
)) as {
default: ElementType;
};
setComponent(() => Component);
} catch (error) {
Sentry.captureException(error);
}
};
loadComponent();
}, [module, scope]);
return component;
}
import { ErrorBoundary } from "@sentry/react";
import { Suspense, lazy } from "react";
const FeedCardAgora = lazy(
() => import("hashtag_community_agora/FeedCardAgora"),
);
const FeedCardAgoraContainer: React.FC<FeedCardAgoraContainerProps> = ({
...
}) => {
...
return (
<ErrorBoundary>
<Suspense fallback={null}>
<FeedCardAgora
...
/>
</Suspense>
</ErrorBoundary>
);
};
export default FeedCardAgoraContainer;
rspack 플러그인은 실행을 하면, 타입을 자동으로 가져온다.
@mf-types 폴더가 생성된다.
lazy import를 하지 않으면, 앱이 실행될 때 모듈을 가져오려고 한다.
Suspense 와 ErrorBoundary 를 이용해서 적절한 UI를 표현해야 한다.
useDynamicImport 는 null 일때 Suspense 같은 처리가 되어야 한다.
모듈 페더레이션 런타임을 모듈 페더레이션 빌드 플러그인과 함께 사용 가능하다.
import { defineConfig } from "@stackflow/config";
import { feedActivityLoader } from "../activities/FeedActivity.loader";
import { myFollowActivityLoader } from "../activities/MyFollowActivity.loader";
export const stackflowTheme = /iphone|ipad|ipod/i.test(
window.navigator.userAgent.toLowerCase(),
)
? "cupertino"
: "android";
export const config = defineConfig({
activities: [
{
name: "FeedActivity",
path: "/v1/feed",
loader: feedActivityLoader,
},
{
name: "MyFollowActivity",
path: "/v1/my-follow",
loader: myFollowActivityLoader,
},
],
transitionDuration: stackflowTheme === "cupertino" ? 270 : 350,
});
import { IconChevronLeftLine } from "@daangn/react-monochrome-icon";
import { vars } from "@seed-design/design-token";
import { basicUIPlugin } from "@stackflow/plugin-basic-ui";
import { historySyncPlugin } from "@stackflow/plugin-history-sync";
import { basicRendererPlugin } from "@stackflow/plugin-renderer-basic";
import { stackDepthChangePlugin } from "@stackflow/plugin-stack-depth-change";
import { stackflow } from "@stackflow/react/future";
import React from "react";
import FeedActivity from "../activities/FeedActivity";
import MyFollowActivity from "../activities/MyFollowActivity";
import { bridge } from "../bridge";
import { config, stackflowTheme } from "./stackflow.config";
export const { Stack } = stackflow({
config,
components: {
FeedActivity,
MyFollowActivity,
},
plugins: [
basicRendererPlugin(),
basicUIPlugin({
appBar: {
borderColor:
stackflowTheme === "cupertino"
? vars.$semantic.color.divider3
: vars.$semantic.color.divider2,
closeButton: {
onClick() {
bridge.closeRouter({});
},
renderIcon: () => (
<IconChevronLeftLine height="1.5rem" width="1.5rem" />
),
},
iconColor: vars.$semantic.color.inkText,
textColor: vars.$semantic.color.inkText,
},
backgroundColor: vars.$semantic.color.paperDefault,
theme: stackflowTheme,
}),
historySyncPlugin({
config,
fallbackActivity: () => "FeedActivity",
}),
stackDepthChangePlugin({
onDepthChanged({ depth }) {
bridge.styleCurrentRouter({
router: {
backSwipable: depth === 1,
enableSafeAreaInsets: false,
navbar: false,
scrollable: false,
},
});
},
onInit({ depth }) {
bridge.styleCurrentRouter({
router: {
backSwipable: depth === 1,
enableSafeAreaInsets: false,
navbar: false,
scrollable: false,
},
});
},
}),
],
});
pnpm add @module-federation/enhanced -D
const mfConfig = {
name: "hashtag_my_follow",
filename: "remoteEntry.js",
exposes: {
"./MyFollowActivity": "./src/activities/MyFollowActivity.tsx",
"./MyFollowActivity.loader": "./src/activities/MyFollowActivity.loader.ts",
},
shared: {
react: {
singleton: true,
requiredVersion: "18.3.1",
},
"react-dom": {
singleton: true,
requiredVersion: "18.3.1",
},
"@stackflow/config": {
requiredVersion: "1.2.0",
},
"@stackflow/core": {
requiredVersion: "1.1.0",
},
"@stackflow/plugin-basic-ui": {
requiredVersion: "1.10.0",
},
"@stackflow/plugin-history-sync": {
requiredVersion: "1.7.0",
},
"@stackflow/plugin-renderer-basic": {
requiredVersion: "1.1.13",
},
"@stackflow/plugin-stack-depth-change": {
requiredVersion: "1.1.5",
},
"@stackflow/react": {
requiredVersion: "1.4.0",
},
"@stackflow/react/future": {
requiredVersion: "1.4.0",
},
"@daangn/react-monochrome-icon": {
version: "0.0.11",
},
"@daangn/sprout-components-layout": {
version: "0.0.0-main-20240404063637",
},
"@daangn/sprout-components-text": {
version: "0.0.0-main-20240405062222",
},
"@seed-design/design-token": {
version: "1.0.3",
},
},
};
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { VanillaExtractPlugin } from "@vanilla-extract/webpack-plugin";
export default defineConfig({
html: {
template: "./index.html",
},
server: {
port: 3000,
},
source: {
entry: {
index: "./src/entry.tsx",
},
define: {
"import.meta.env.VITE_DEV_AUTH_TOKEN": JSON.stringify(
import.meta.env.VITE_DEV_AUTH_TOKEN,
),
"import.meta.env.VITE_SENTRY_DSN": JSON.stringify(
import.meta.env.VITE_SENTRY_DSN,
),
},
},
plugins: [
pluginReact({
reactRefreshOptions: {
exclude: [/node_modules/, /\.css\.ts$/],
},
}),
],
tools: {
rspack: {
plugins: [
new VanillaExtractPlugin({
externals: ["@seed-design"],
}),
process.env.STORYBOOK !== "true" &&
new ModuleFederationPlugin({ ...mfConfig }),
],
},
},
dev: {
assetPrefix: "http://localhost:3000",
},
output: {
overrideBrowserslist: ["chrome >= 64", "ios_saf >= 14"],
assetPrefix: process.env.BASE_URL ?? "http://localhost:3000",
filenameHash: true,
},
});
const mfConfig = {
name: "hashtag_webview",
remotes: {
hashtag_community_agora: process.env.HASHTAG_COMMUNITY_AGORA_URL
? `hashtag_community_agora@${process.env.HASHTAG_COMMUNITY_AGORA_URL}/mf-manifest.json`
: "hashtag_community_agora@https://hashtag-community-agora.alpha.karrotwebview.com/mf-manifest.json",
hashtag_my_follow: process.env.HASHTAG_MY_FOLLOW_URL
? `hashtag_my_follow@${process.env.HASHTAG_MY_FOLLOW_URL}/mf-manifest.json`
: "hashtag_my_follow@https://hashtag-my-follow.alpha.karrotwebview.com/mf-manifest.json",
},
shared: {
...
"@stackflow/config": {
requiredVersion: "1.2.0",
},
"@stackflow/core": {
requiredVersion: "1.1.0",
},
"@stackflow/plugin-basic-ui": {
requiredVersion: "1.10.0",
},
"@stackflow/plugin-history-sync": {
requiredVersion: "1.7.0",
},
"@stackflow/plugin-renderer-basic": {
requiredVersion: "1.1.13",
},
"@stackflow/plugin-stack-depth-change": {
requiredVersion: "1.1.5",
},
"@stackflow/react": {
requiredVersion: "1.4.0",
},
"@stackflow/react/future": {
requiredVersion: "1.4.0",
},
},
};
import { type ActivityLoaderArgs, defineConfig } from "@stackflow/config";
import { feedActivityLoader } from "../activities/FeedActivity.loader";
export const stackflowTheme = /iphone|ipad|ipod/i.test(
window.navigator.userAgent.toLowerCase(),
)
? "cupertino"
: "android";
export const config = defineConfig({
activities: [
{
name: "FeedActivity",
path: "/v1/feed",
loader: feedActivityLoader,
},
{
name: "MyFollowActivity",
path: "/v1/my-follow",
loader: ({ params }: ActivityLoaderArgs<"MyFollowActivity">) =>
import("hashtag_my_follow/MyFollowActivity.loader").then((module) =>
module.myFollowActivityLoader({ params, config }),
),
},
],
transitionDuration: stackflowTheme === "cupertino" ? 270 : 350,
});
import { IconChevronLeftLine } from "@daangn/react-monochrome-icon";
import { vars } from "@seed-design/design-token";
import { basicUIPlugin } from "@stackflow/plugin-basic-ui";
import { historySyncPlugin } from "@stackflow/plugin-history-sync";
import { basicRendererPlugin } from "@stackflow/plugin-renderer-basic";
import { stackDepthChangePlugin } from "@stackflow/plugin-stack-depth-change";
import { stackflow } from "@stackflow/react/future";
import React from "react";
import FeedActivity from "../activities/FeedActivity";
import { bridge } from "../bridge";
import { config, stackflowTheme } from "./stackflow.config";
export const { Stack } = stackflow({
config,
components: {
FeedActivity,
MyFollowActivity: React.lazy(
() => import("hashtag_my_follow/MyFollowActivity"),
),
},
...
});
Stackflow 만세
Activity Component 와 loader 함수를 동적으로 사용한다.
다른 액티비티를 보는 동안 코드를 가져오지 않도록 한다.
문제가 있을 경우, 에러 페이지가 보인다.
공유 모듈의 singleton 처리가 적절하게 필요하다.
타입 전달 문제
vite 에서 진행할 가능성이 높아요.
vite 가 host 인 경우 top level await 문제
공유를 버리거나, vite 가 고치거나, rsbuild 를 쓰거나,
shared 모듈을 좀더 올바르게 쓰기
전역 공유 패키지의 규정
shell 이 중요
일해라
인터페이스의 변경 배포
여러가지 전략이 가능
By Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team