과거에서부터 웹 프론트엔드 기술은 요구되는 기능의 양이 방대해지면서 다양하게 진화를 거듭해왔다.
이를테면 여러 모듈을 번들링하고 ECMAScript를 지원하지 않는 브라우저를 위해 코드 트랜스파일링을 진행해주는 바벨 플러그인을 사용할 수 있는 웹팩의 등장, CSS를 구조적으로 분리하고 재사용성 있는 스타일 작성을 도와주는 CSS 전처리기의 등장 등 방대해지는 웹 콘텐츠를 효율적으로 작성하고 유지보수하기 위해 다양한 기술들이 등장하였다.
프론트엔드를 개발하기 위한 전략 또한 시대에 따라 변화하게 되었다.
시간이 지남에 따라 방대해진 프론트엔드를 개발하기 위해, 하나의 팀보다는 여러개의 기능을 분할해 맡아 개발을 진행할 수 있도록 팀이 분산돼 투입되었으며, 이로 인해 프론트엔드 계층은 더더욱 커지고 유지 관리가 어려워지게 되었다.
이 처럼 각각의 기능을 개발하면서 하나의 앱으로 패키징하는 기존의 방식을 프론트엔드 모노리스(Frontend Monolith) 아키텍쳐라 일컫는다.
(소스 코드가 모듈화 없이 설계된 소프트웨어 아키텍쳐)
이러한 문제를 해결하기 위해 나온것이 Micro Frontend로 이는 개별 팀들이 소유하고 있는 웹사이트나 웹앱을 하나의 기능으로 보고, 그 기능을 구성(Composition)하자는 방식의 아이디어이다.
각 팀 별로 팀에서 담당하는 뚜렷한 비즈니스 영역 역할이 있을 것이다.
각 팀내에서는 데이터베이스에서 사용자 인터페이스에 이르기 까지 상호 기능을 통해 end-to-end 기능을 개발한다.
본고에서는 NextJS에서 마이크로 아키텍쳐를 구성하고 이를 통해서 얻을 수 있는 장점을 설명하겠다.
우선 마이크로 아키텍쳐 구성에 앞서 어떻게 구성할 것인지 생각해보자.
프로덕션에서 마이크로 아키텍쳐를 구성하는 이유는 무엇일까?
이유를 알기위해 기존의 모노레포가 가지는 문제점을 정리해보겠다.
우리는 위와 같은 문제를 해결하기 위해 마이크로 프론트엔드를 도입할 때 다음과 같은 유형으로 구분하여 분리할 수 있습니다.
본고에서는 라이브러리 패키지와 서비스 패키지를 만들고 이를 컨테이너에서 다루는 처리를 구현해보겠다.
cd ./micro-services
npx create-next-app --ts micro-lib
npx create-next-app --ts micro-service
npx create-next-app --ts micro-container
각 서비스 내에서 아래 명령어를 입력하여 @module-federation/nextjs-mf
패키지를 설치하도록 한다.
yarn add @module-federation/nextjs-mf@5.5.0
./micro-services/micro-lib/src/utils/getHello.ts
const getHello = () => {
return 'Hello'
}
export default getHello
만들고 나서 웹팩 설정을 통해 해당 유틸리티를 외부에서 사용하겠다고 알려주어야 한다.
./micro-services/micro-lib/next.config.js
const NextFederationPlugin = require("@module-federation/nextjs-mf");
module.exports = {
swcMinify: true,
webpack(config, options) {
Object.assign(config.experiments, { topLevelAwait: true });
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: "lib",
remotes: {},
filename: "static/chunks/remoteEntry.js",
exposes: {
"./getHello": "./src/utils/getHello.ts", // 이 곳에 외부에서 가져와 사용할 파일 명을 입력해준다.
},
shared: {},
})
);
}
return config;
},
};
설정이 완료되었다면 yarn dev
명령어를 통해 micro-lib
서비스를 시작해보자.
.next/static/chunks
경로에 remoteEntry.js
파일이 생성됨을 확인할 수 있는데, 바로 해당 파일을 통해 외부에서 접근하여 getHello
유틸리티를 사용할 수 있는 것이다.
먼저 micro-container
에서 next.config.ts
파일을 수정해주어야 한다.
./micro-services/micro-container/next.config.js
const NextFederationPlugin = require("@module-federation/nextjs-mf");
module.exports = {
swcMinify: true,
webpack(config, options) {
config.resolve.fallback = {
lib: false,
}; // 당연히 lib 패키지는 로컬 경로에 없기 때문에 웹팩에서 오류를 발생시킬 수 있다. 현재 상황에서는 fallback을 사용해 임의로 해결하겠다.
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: "container",
filename: "static/chunks/remoteEntry.js",
remotes: {
lib:
"lib@http://localhost:3000/_next/static/chunks/remoteEntry.js",
}, // 여기에 2번의 과정에서 만들어진 remoteEntry 파일 경로를 입력한다.
exposes: {},
shared: {},
})
);
}
return config;
},
};
파일이 수정되었다면 페이지 컴포넌트에서 getHello 함수를 사용해보겠다.
./micro-services/micro-container/pages/index.tsx
// typescript 환경에서 타입 오류가 발생할 수 있다.
// 이 경우에는 declare 파일을 선언하여 해결해야한다.
// 또는 아래 패키지를 확인해보자.
// https://github.com/ogzhanolguncu/react-typescript-module-federation
import getHello from 'lib/getHello'
export default function Home() {
return <div>{getHello()}</div>
}
이번에는 페이지 컴포넌트에서 독립적으로 동작하는 컴포넌트를 하나 만들어보겠다.
과정은 2번과 유사하다.
./micro-services/micro-service/src/components/Header.tsx
const Header = () => {
return <div>헤더 컴포넌트</div>
}
export default Header
./micro-services/micro-service/next.config.js
const NextFederationPlugin = require("@module-federation/nextjs-mf");
module.exports = {
swcMinify: true,
webpack(config, options) {
Object.assign(config.experiments, { topLevelAwait: true });
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: "service",
remotes: {},
filename: "static/chunks/remoteEntry.js",
exposes: {
"./Header": "./src/components/Hello.tsx"
},
shared: {},
})
);
}
return config;
},
};
설정이 완료되었다면 yarn dev
명령어를 통해 micro-service
서비스를 시작하자.
먼저 micro-container
에서 기존의 next.config.ts
파일을 수정해주어야 한다.
./micro-services/micro-container/next.config.js
const NextFederationPlugin = require("@module-federation/nextjs-mf");
module.exports = {
swcMinify: true,
webpack(config, options) {
config.resolve.fallback = {
lib: false,
}; // 당연히 lib 패키지는 로컬 경로에 없기 때문에 웹팩에서 오류를 발생시킬 수 있다. 현재 상황에서는 fallback을 사용해 임의로 해결하겠다.
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: "container",
filename: "static/chunks/remoteEntry.js",
remotes: {
lib:
"lib@http://localhost:3000/_next/static/chunks/remoteEntry.js",
service: "service@http://localhost:3001/_next/static/chunks/remoteEntry.js"
}, // 이곳에 service 서비스에서 반출된 remoteEntry를 추가하자.
exposes: {},
shared: {},
})
);
}
return config;
},
};
추가를 완료하였다면 사용하면 된다.
다만 유의해야할 점은 본고에서는 CSR에서만 동작하도록 처리하였기 때문에 아래와 같이 next/dynamic
의 사용이 필요하다.
SSR에서도 동작하고 싶다면 추가적인 설정을 해주어야 하는데 참고할 수 있는 레포는 아래와 같다.
참고 링크 (#Github)
const Header = dynamic(
() => {
return import("service/Header");
},
{ ssr: false }
);
export default Header
지금까지 간단하게 알아보았다.
그러면 마이크로 아키텍쳐를 도입함으로서 얻을 수 있는 장점은 무엇이 있을까?
Next로 시작하기...
섹션에서 작성한 마이크로 서비스 중 getHello
함수가 Goodbye 문자열을 반환하도록 변경하고 lib
마이크로 서비스만 다시 시작하면 놀랍게도 container
서비스는 재시작하지 않았는데 화면상에 Goodbye 문자열이 보일 것이다.lib
마이크로 서비스가 변경되었다 하더라도 container
서비스에서 별도로 작업해주어야할 일은 없다.lib
마이크로 서비스의 동작이 멈추었다면 어떻게될까?마이크로 프론트엔드에 대해 직접 데모를 만들며 느낀 점으로 웹팩에 익숙하지 않다면 어느정도의 러닝커브는 있어야할 것 같다는 생각이 들었다.
물론 모노레포와 마이크로 프론트엔드에 대해 이해도가 뒤쳐진 나로서는 본문의 글이 용두사미로 끝나는 것 같다는 느낌을 없지않아 받게 되는데, 나 스스로도 직접 서비스를 만들어가며 구축하는 시간을 가져보아야할 것 같다.