NextFederation 플러그인 톺아보기
문득 궁금해졌다,,해당 플러그인에서 무슨 처리를 해주고 있길래 Next.js 에서 MF 를 사용할 때 일반 웹팩 내장 MF 플러그인을 사용하면 안되는건지..그래서 알아봤다.
특히나 SSR 처리 / 라우팅 관련 처리 / Next.js 의 기본 다이나믹 임포트와의 충돌 처리 등등..😧 이것저것 많이 해주고 있었다.
알아보고 나니, 일반 웹팩 플러그인 만으로는 Next.js 에서 MF 구성하기에는 해야할일이 너무 많아서 요 플러그인을 쓰는게 맞는 것 같다. 만약에 마음에 안드는 동작(?)이 있다면 이 플러그인을 기반으로 일부만 커스텀해서 써야할듯
그나저나 Next.js 라는 레이어가 중간에 껴버리면 번들러 구성이 배로 복잡해지는구나 ㅠㅠ
1. constructor : 플러그인 인스턴스 초기화
constructor(options: NextFederationPluginOptions) {
const { mainOptions, extraOptions } = setOptions(options);
this._options = mainOptions;
this._extraOptions = extraOptions;
this.name = 'ModuleFederationPlugin';
}
- setOptions 함수를 사용하여, 주어진 옵션을 메인 옵션과 추가 옵션으로 분리한다.
- 분리된 옵션을 클래스 프로퍼티에 저장한다.
- 플러그인 이름을
ModuleFederationPlugin
으로 설정한다.
2. apply : Webpack 컴파일러에 플러그인 적용
apply(compiler: Compiler) {
// 🔥 웹팩 경로를 환경변수에 설정한다.
process.env['FEDERATION_WEBPACK_PATH'] =
process.env['FEDERATION_WEBPACK_PATH'] ||
getWebpackPath(compiler, { framework: 'nextjs' });
// 🔥 옵션에 대한 유효성 검사
if (!this.validateOptions(compiler)) return;
// 🔥 서버 / 클라이언트 컴파일러 구분
const isServer = this.isServerCompiler(compiler);
// 🔥 CopyFederationPlugin 적용
new CopyFederationPlugin(isServer).apply(compiler);
// 🔥 MF 옵션 구성 및 적용
const normalFederationPluginOptions = this.getNormalFederationPluginOptions(
compiler,
isServer,
);
this._options = normalFederationPluginOptions;
this.applyConditionalPlugins(compiler, isServer);
new ModuleFederationPlugin(normalFederationPluginOptions).apply(compiler);
// 🔥 엔트리 포인트 수정
// Next.js 의 API 라우트와 관련된 엔트리포인트 수정
// MF 와 Next.js 의 API 라우트가 충돌하는 것을 방지하기 위해서
// .federation/entry 로 시작하는 엔트리를 제거하여 MF 런타임이 API 라우트에 영향없도록!
modifyEntry({
compiler,
prependEntry: (entry) => {
Object.keys(entry).forEach((entryName) => {
const entryItem = entry[entryName];
if (!entryName.startsWith('pages/api')) return;
if (!entryItem.import) return;
// unpatch entry of webpack api runtime
entryItem.import = entryItem.import.filter((i) => {
return !i.includes('.federation/entry');
});
});
},
});
const runtimeESMPath = require.resolve(
'@module-federation/runtime/dist/index.esm.js',
);
if (!compiler.options.ignoreWarnings) {
compiler.options.ignoreWarnings = [
//@ts-ignore
(message) => /your target environment does not appear/.test(message),
];
}
// 🔥 웹팩의 afterPlugin 훅에 탭된다.
// 즉, 모든 플러그인이 적용된 직후에 실행된다.
// 코드의 목적은 MF 런타임의 별칭(alias)를 설정하는 것이지만,
// 현재는 주석처리되어있어서 아무런 동작을 하지 않는다.
// 만약 주석을 해제한다면 @module-fderation/runtime 에 대한 요청을
// runtimeESMPath 로 리다이렉트함.
// 이는, 특정 버전의 런타임을 강제로 사용하거나 커스텀 런타임을 적용할 때 유용.
compiler.hooks.afterPlugins.tap('PatchAliasWebpackPlugin', () => {
compiler.options.resolve.alias = {
...compiler.options.resolve.alias,
//useing embedded runtime
// '@module-federation/runtime
- 🔥 MF 와 Next.js 의 API 라우트가 충돌하는 것을 방지하기 위해서
- 모든 엔트리 포인트 순회 후, `pages/api`로 시작하는엔트리 포인트의 `import`배열에서 `.federation/etnry`를 포함하는 항목 제거
- 왜?
- MF의 런타임코드 `.federation/entry`가 API 라우트에 포함되면 불필요한 오버헤드가 발생하거나, 예상치 못한 동작이 일어날 수 있다.
## 3. validateOptions : 컴파일러 옵션과 플러그인 옵션의 유효성 검증
```js
private validateOptions(compiler: Compiler): boolean {
const manifestPlugin = compiler.options.plugins.find(
(p: WebpackPluginInstance) =>
p?.constructor.name === 'BuildManifestPlugin',
);
if (manifestPlugin) {
//@ts-ignore
// 🔥 App directory 사용 여부 판별
// 사용시 에러 발생 (지원안함!)
if (manifestPlugin?.appDirEnabled) {
throw new Error(
'App Directory is not supported by nextjs-mf. Use only pages directory, do not open git issues about this',
);
}
}
// 🔥 각 옵션 검증
const compilerValid = validateCompilerOptions(compiler);
const pluginValid = validatePluginOptions(this._options);
const envValid = process.env['NEXT_PRIVATE_LOCAL_WEBPACK'];
if (compilerValid === undefined)
console.error('Compiler validation failed');
if (pluginValid === undefined) console.error('Plugin validation failed');
const validCompilerTarget =
compiler.options.name === 'server' || compiler.options.name === 'client';
if (!envValid)
throw new Error(
'process.env.NEXT_PRIVATE_LOCAL_WEBPACK is not set to true, please set it to true, and "npm install webpack"',
);
return (
compilerValid !== undefined &&
pluginValid !== undefined &&
validCompilerTarget
);
}
4. applyConidtionalPlugins: 서버/클라에 따라서 다른 플러그인과 설정 적용
- Next.sj 의 CSR/ SSR 에 따라서 적용
- Next.js 의 빌드 프로세스가 서버와 클라이언트 번들을 별도로 생성하며, 이 플러그인은 각 컨텍스트에 맞는 설정을 자동으로 적용해준다.
private applyConditionalPlugins(compiler: Compiler, isServer: boolean) {
// 🔥 컴파일러 출력 설정 조정
compiler.options.output.uniqueName = this._options.name;
compiler.options.output.environment = {
...compiler.options.output.environment,
asyncFunction: true,
};
// 🔥 경로 수정 적용
// Next.js 와 MF 간의 경로 충돌 해결
applyPathFixes(compiler, this._options, this._extraOptions);
// 🔥 디버그 모드 설정 (소스맵 설정)
if (this._extraOptions.debug) {
compiler.options.devtool = false;
}
// 🔥 서버 관련 설정
if (isServer) {
// 서버 컴파일러 설정
configureServerCompilerOptions(compiler);
// 서버 라이브러리와 파일 이름 설정
configureServerLibraryAndFilename(this._options);
// 서버 관련 플러그인 적용
applyServerPlugins(compiler, this._options);
// 서버의 외부 모듈 처리
handleServerExternals(compiler, {
...this._options,
shared: { ...retrieveDefaultShared(isServer), ...this._options.shared },
});
} else {
// 🔥 클라이언트일 때 설정
applyClientPlugins(compiler, this._options, this._extraOptions);
}
}
5. getNormalFederationPluginOptions: MF 에 사용할 옵션들을 생성한다.
private getNormalFederationPluginOptions(
compiler: Compiler,
isServer: boolean,
): moduleFederationPlugin.ModuleFederationPluginOptions {
// 🔥 기본 공유 의존성을 설정한다
const defaultShared = this._extraOptions.skipSharingNextInternals
? {}
: retrieveDefaultShared(isServer);
const noop = this.getNoopPath();
// 🔥 react & react-dom & next.router 는 기본적으로 설정된다.
const defaultExpose = this._extraOptions.skipSharingNextInternals
? {}
: {
'./noop': noop,
'./react': require.resolve('react'),
'./react-dom': require.resolve('react-dom'),
'./next/router': require.resolve('next/router'),
};
// 🔥 모듈 페더레이션 플러그인에 대한 옵션 구성
return {
// 🔥 기존 설정 옵션들을 포함시키고
...this._options,
runtime: false, // 🔥 런타임 모듈을 비활성화 시킨다.
remoteType: 'script', // 🔥 원격 모듈을 스크립트 형태로 로드하도록 설정
runtimePlugins: [ // 서버일 경우 `runtimePlugin` 포함
...(isServer
? [require.resolve('@module-federation/node/runtimePlugin')]
: []),
require.resolve(path.join(__dirname, '../container/runtimePlugin')),
...(this._options.runtimePlugins || []),
].map((plugin) => plugin + '?runtimePlugin'), // 🔥 각 플러그인 경로 뒤에 설정
//@ts-ignore
// 🔥 기본 expose 설정
exposes: {
...defaultExpose,
...this._options.exposes,
...(this._extraOptions.exposePages
// 🔥 exposePages 가 true 일 경우, Next.js 페이지 자동 expose
? exposeNextjsPages(compiler.options.context as string)
: {}),
},
// 🔥 사용자가 설정한 원격 모듈 포함
remotes: {
...this._options.remotes,
},
// 🔥 기본 공유 모듈 설정 포함 및 사용자가 설정한것도 포함
shared: {
...defaultShared,
...this._options.shared,
},
// 🔥 서버일 경우 빈 문자열을, 클라이언트일 경우 /static/chunks를'
// 매니페스트 파일 경로로 설정
...(isServer
? { manifest: { filePath: '' } }
: { manifest: { filePath: '/static/chunks' } }),
// nextjs project needs to add config.watchOptions = ['**/node_modules/**', '**/@mf-types/**'] to prevent loop types update
// d.ts 생성
dts: this._options.dts ?? false,
// 🔥 공유 모듈 전략 설정. 기본은 loaded-first
shareStrategy: this._options.shareStrategy ?? 'loaded-first',
experiments: {
federationRuntime: 'hoisted',
},
};
}
-
🔥 런타임 모듈이란 뭐고 왜 비활성화 처리할까?
- 런타임 모듈은 MF 의 dynamic import 처리
runtime:false
로 처리하는 이유는, Next.js 가 자체적인 코드 스플릿팅과 dynamic import 매커니즘을 가지고 있어서.- 즉, Next.js 기본 매커니즘과의 충돌을 피하고, Next.js의 최적화를 활용하기 위해 비활성화 한다.
-
🔥 서버의 runtimePlugin
- 서버 환경에서는
@module-federation/node/runtimePlugin
을 사용한다. - 해당 플러그인은 Node.js 환경에서 MF 이 제대로 작동하도록 도와준다.
- 예를들어, 서버사이드 렌더링 시 원격 모듈을 올바르게 로드하고 실행 할 수 있도록.
- 서버 환경에서는
-
🔥 매니페스트파일이란?
- Module Federation 의 구성정보를 담고 있다.
- 어떤 모듈이 expose 되었는지, 어떤 원격모듈을 사용하는지 등의 정보가 포함된다.
- 런타임에 이 정보를 사용하여 필요한 모듈을 동적으로 로드한다.
-
🔥 서버/클라의 매니페스트 파일 경로 차이
- 클라이언트:
/static/chunks
에 매니페스트 파일을 저장한다.- 이는 브라우저에서 접근 가능한 공개 경로.
- 서버:
빈문자열('')
사용- 서버에서는 매니페스트 파일이 필요없거나, 다른 방식으로 처리 가능
- 클라이언트:
-
🔥 실험적인 hoisted 기능
- MF 의 해당 기능은 런타임 코드를 애플리케이션의 엔트리포인트로 호이스팅하는 실험적인 기능.
- 이를 통해 MF 의 초기화를 더 빠르게 하고, 성능을 개선 할 수 있음.
- 또한, 다른 코드보다 먼저 Federation 런타임이 로드 되므로, 원격 모듈을 더 안정적으로 로드 가능.
-
🔥 hoisted 기능과 eager loading 의 차이는?
-
호이스팅
- Federation 런타임 코드란, MF 의 핵심기능 코드로, 원격 모듈을 동적으로 로드하고 관리하는 역할.
- 즉 특정 모듈을 호이스팅하는게 아니라, MF 의 실행환경을 미리 로드해두는것.
- MF 의 런타임 코드를 애프리케이션 진입점으로 호이스팅.
- Federation 런타임이 애플리케이션의 다른 코드보다 먼저 로드되고 실행된다.
- Federation 런타임만 호이스팅되며 실제 원격 모듈의 콘텐츠는 여전히 필요할 때 로드된다.
-
Eager Loading
- 특정 모듈이나 청크를 애플리케이션 시작 시점에 즉시 로드하는 것
- 지정된 모듈이나 청크가 애플리케이션 초기 로드 시점에 함께 다운로드 되고, 실행된다.
-
6. MF 와 Next.js 를 어떻게 연동하는가?
- 🔥 dynamic import 랩핑
- Next.js의 dynamic import 기능을 확장하여 원격 모듈 지원
- Next.js 의 dynamic import 를 직접적으로 수정하지는 않지만, 웹팩의 모듈 해석 과정을 수정하여 원격 모듈을 처리한다.
// 플러그인 내부 로직 (의사 코드)
compiler.hooks.normalModuleFactory.tap('NextFederationPlugin', (nmf) => {
nmf.hooks.resolve.tapAsync('NextFederationPlugin', (request, context, callback) => {
if (isRemoteModule(request)) {
// Module Federation 로직을 적용하여 원격 모듈 해석
resolveFederatedModule(request, context, callback);
} else {
// 일반적인 모듈 해석 프로세스 진행
callback();
}
});
});
- 🔥 라우트 인터셉터
- Next.js 의 라우팅 시스템을 후킹하여, 원격 페이지에 대한 요청을 가로챈다.
- 즉, Next.js 의 dynamic import 매커니즘과 MF 를 연동한다.
exposes: {
...defaultExpose,
...this._options.exposes,
...(this._extraOptions.exposePages
? exposeNextjsPages(compiler.options.context as string)
: {}),
},
- 위의 부분에서 원격 페이지들을 노출시킨다.
exposeNextjsPages
함수를 통해서 Next.js 페이지들을 자동으로 노출시킴.- 실제 '인터셉션'은 플러그인 자체가 아닌, Next.js의 dynamic import 매커니즘을 통해 이루어진다.
import dynamic from 'next/dynamic'
const RemoteComponent = dynamic(() => import('remote-app/Component'), { ssr: false // SSR이 필요한 경우 true로 설정
})
- 요 방식을 사용하면 Next.js 의 라우팅 시스템이 그대로 유지되면서 필요한 시점에 원격 컴포넌트를 로드 가능.
: runtimeESMPath,
};
});
}
- 🔥 MF 와 Next.js 의 API 라우트가 충돌하는 것을 방지하기 위해서
- 모든 엔트리 포인트 순회 후, `pages/api`로 시작하는엔트리 포인트의 `import`배열에서 `.federation/etnry`를 포함하는 항목 제거
- 왜?
- MF의 런타임코드 `.federation/entry`가 API 라우트에 포함되면 불필요한 오버헤드가 발생하거나, 예상치 못한 동작이 일어날 수 있다.
## 3. validateOptions : 컴파일러 옵션과 플러그인 옵션의 유효성 검증
{{CODE_BLOCK_2}}
## 4. applyConidtionalPlugins: 서버/클라에 따라서 다른 플러그인과 설정 적용
- Next.sj 의 CSR/ SSR 에 따라서 적용
- Next.js 의 빌드 프로세스가 서버와 클라이언트 번들을 별도로 생성하며, 이 플러그인은 각 컨텍스트에 맞는 설정을 자동으로 적용해준다.
{{CODE_BLOCK_3}}
## 5. getNormalFederationPluginOptions: MF 에 사용할 옵션들을 생성한다.
{{CODE_BLOCK_4}}
- 🔥 런타임 모듈이란 뭐고 왜 비활성화 처리할까?
- 런타임 모듈은 MF 의 dynamic import 처리
- `runtime:false`로 처리하는 이유는, Next.js 가 자체적인 코드 스플릿팅과 dynamic import 매커니즘을 가지고 있어서.
- 즉, Next.js 기본 매커니즘과의 충돌을 피하고, Next.js의 최적화를 활용하기 위해 비활성화 한다.
- 🔥 서버의 runtimePlugin
- 서버 환경에서는 `@module-federation/node/runtimePlugin` 을 사용한다.
- 해당 플러그인은 Node.js 환경에서 MF 이 제대로 작동하도록 도와준다.
- 예를들어, 서버사이드 렌더링 시 원격 모듈을 올바르게 로드하고 실행 할 수 있도록.
- 🔥 매니페스트파일이란?
- Module Federation 의 구성정보를 담고 있다.
- 어떤 모듈이 expose 되었는지, 어떤 원격모듈을 사용하는지 등의 정보가 포함된다.
- 런타임에 이 정보를 사용하여 필요한 모듈을 동적으로 로드한다.
- 🔥 서버/클라의 매니페스트 파일 경로 차이
- 클라이언트: `/static/chunks`에 매니페스트 파일을 저장한다.
- 이는 브라우저에서 접근 가능한 공개 경로.
- 서버: `빈문자열('')`사용
- 서버에서는 매니페스트 파일이 필요없거나, 다른 방식으로 처리 가능
- 🔥 실험적인 hoisted 기능
- MF 의 해당 기능은 런타임 코드를 애플리케이션의 엔트리포인트로 호이스팅하는 실험적인 기능.
- 이를 통해 MF 의 초기화를 더 빠르게 하고, 성능을 개선 할 수 있음.
- 또한, 다른 코드보다 먼저 Federation 런타임이 로드 되므로, 원격 모듈을 더 안정적으로 로드 가능.
- 🔥 hoisted 기능과 eager loading 의 차이는?
- 호이스팅
- Federation 런타임 코드란, MF 의 핵심기능 코드로, 원격 모듈을 동적으로 로드하고 관리하는 역할.
- 즉 특정 모듈을 호이스팅하는게 아니라, MF 의 실행환경을 미리 로드해두는것.
- MF 의 런타임 코드를 애프리케이션 진입점으로 호이스팅.
- Federation 런타임이 애플리케이션의 다른 코드보다 먼저 로드되고 실행된다.
- Federation 런타임만 호이스팅되며 실제 원격 모듈의 콘텐츠는 여전히 필요할 때 로드된다.
- Eager Loading
- 특정 모듈이나 청크를 애플리케이션 시작 시점에 즉시 로드하는 것
- 지정된 모듈이나 청크가 애플리케이션 초기 로드 시점에 함께 다운로드 되고, 실행된다.
## 6. MF 와 Next.js 를 어떻게 연동하는가?
- 🔥 dynamic import 랩핑
- Next.js의 dynamic import 기능을 확장하여 원격 모듈 지원
- Next.js 의 dynamic import 를 직접적으로 수정하지는 않지만, 웹팩의 모듈 해석 과정을 수정하여 원격 모듈을 처리한다.
{{CODE_BLOCK_5}}
- 🔥 라우트 인터셉터
- Next.js 의 라우팅 시스템을 후킹하여, 원격 페이지에 대한 요청을 가로챈다.
- 즉, Next.js 의 dynamic import 매커니즘과 MF 를 연동한다.
{{CODE_BLOCK_6}}
- 위의 부분에서 원격 페이지들을 노출시킨다.
- `exposeNextjsPages` 함수를 통해서 Next.js 페이지들을 자동으로 노출시킴.
- 실제 '인터셉션'은 플러그인 자체가 아닌, Next.js의 dynamic import 매커니즘을 통해 이루어진다.
{{CODE_BLOCK_7}}
- 요 방식을 사용하면 Next.js 의 라우팅 시스템이 그대로 유지되면서 필요한 시점에 원격 컴포넌트를 로드 가능.