마이크로 프론트엔드 정복기

LUCAS·2022년 11월 14일
2

글을 시작하며..

과거에서부터 웹 프론트엔드 기술은 요구되는 기능의 양이 방대해지면서 다양하게 진화를 거듭해왔다.
이를테면 여러 모듈을 번들링하고 ECMAScript를 지원하지 않는 브라우저를 위해 코드 트랜스파일링을 진행해주는 바벨 플러그인을 사용할 수 있는 웹팩의 등장, CSS를 구조적으로 분리하고 재사용성 있는 스타일 작성을 도와주는 CSS 전처리기의 등장 등 방대해지는 웹 콘텐츠를 효율적으로 작성하고 유지보수하기 위해 다양한 기술들이 등장하였다.

프론트엔드를 개발하기 위한 전략 또한 시대에 따라 변화하게 되었다.
시간이 지남에 따라 방대해진 프론트엔드를 개발하기 위해, 하나의 팀보다는 여러개의 기능을 분할해 맡아 개발을 진행할 수 있도록 팀이 분산돼 투입되었으며, 이로 인해 프론트엔드 계층은 더더욱 커지고 유지 관리가 어려워지게 되었다.

이 처럼 각각의 기능을 개발하면서 하나의 앱으로 패키징하는 기존의 방식을 프론트엔드 모노리스(Frontend Monolith) 아키텍쳐라 일컫는다.
(소스 코드가 모듈화 없이 설계된 소프트웨어 아키텍쳐)

이러한 문제를 해결하기 위해 나온것이 Micro Frontend로 이는 개별 팀들이 소유하고 있는 웹사이트나 웹앱을 하나의 기능으로 보고, 그 기능을 구성(Composition)하자는 방식의 아이디어이다.

각 팀 별로 팀에서 담당하는 뚜렷한 비즈니스 영역 역할이 있을 것이다.
각 팀내에서는 데이터베이스에서 사용자 인터페이스에 이르기 까지 상호 기능을 통해 end-to-end 기능을 개발한다.

본고에서는 NextJS에서 마이크로 아키텍쳐를 구성하고 이를 통해서 얻을 수 있는 장점을 설명하겠다.


우선 마이크로 아키텍쳐 구성에 앞서 어떻게 구성할 것인지 생각해보자.
프로덕션에서 마이크로 아키텍쳐를 구성하는 이유는 무엇일까?

이유를 알기위해 기존의 모노레포가 가지는 문제점을 정리해보겠다.

  1. 모든 컴포넌트와 기능이 하나의 레포안에 모여있기 때문에 작은 변경 사항이여도 모든 내용에 대해 빌드를 진행해야합니다.
  2. 소스 코드에서 오류가 발생한다면 하나의 런타임을 사용하기에 전체 서비스가 뻗습니다. (의존성 문제)

우리는 위와 같은 문제를 해결하기 위해 마이크로 프론트엔드를 도입할 때 다음과 같은 유형으로 구분하여 분리할 수 있습니다.

  1. 인프라 패키지 - 동일한 빌드 툴링을 공유하기 위함
  2. 라이브러리 패키지 - 공통 소스 코드를 관리하기 위함
  3. 서비스 패키지 - 페이지에서 독립적으로 동작하는 소스코드를 관리하기 위함

NextJS로 시작하기...

본고에서는 라이브러리 패키지와 서비스 패키지를 만들고 이를 컨테이너에서 다루는 처리를 구현해보겠다.

1. 먼저 next-create-app을 통해 3개의 서비스를 만들어보겠다!

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

2. 라이브러리 패키지에서 공통 소스 코드를 하나 만들어보자!

./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 유틸리티를 사용할 수 있는 것이다.

3. 컨테이너 서비스에서 공통 소스 코드를 가져와 사용해보자.

먼저 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>
}

4. 서비스 패키지에서도 공통 소스 코드를 하나 만들어보자!

이번에는 페이지 컴포넌트에서 독립적으로 동작하는 컴포넌트를 하나 만들어보겠다.
과정은 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 서비스를 시작하자.

5. 컨테이너 서비스에 마이크로 서비스도 담아보자!

먼저 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

지금까지 간단하게 알아보았다.

그러면 마이크로 아키텍쳐를 도입함으로서 얻을 수 있는 장점은 무엇이 있을까?

도입하면 좋은 이유?

  1. 빌드 속도가 단축된다.
  • 빌드 속도를 최소화할 수 있는 방법은 빌드를 하지 않는 것이라고 했다.
  • Next로 시작하기... 섹션에서 작성한 마이크로 서비스 중 getHello 함수가 Goodbye 문자열을 반환하도록 변경하고 lib 마이크로 서비스만 다시 시작하면 놀랍게도 container 서비스는 재시작하지 않았는데 화면상에 Goodbye 문자열이 보일 것이다.
  • 즉, 변경 사항이 발생하는 마이크로 서비스만 재빌드를 진행하면 되니 빌드 속도가 점진적으로 향상될 수 있다는 장점이 생긴다.
  1. 의존성 지옥의 탈출.
  • 상기 1번의 특징에서 보았듯, lib 마이크로 서비스가 변경되었다 하더라도 container 서비스에서 별도로 작업해주어야할 일은 없다.
  • 만약 lib 마이크로 서비스의 동작이 멈추었다면 어떻게될까?
    하나의 레포만을 사용하는 모노 레포라면 서비스는 결국 전체 서비스를 의미하기에 서비스 전체가 내려가는 일이 발생할 것이다.
    다만, 마이크로 프론트엔드라면 해당하는 마이크로 서비스만 멈추기 때문에 예외 처리만 해준다면 문제가 발생하지 않는다.

글을 마치며

마이크로 프론트엔드에 대해 직접 데모를 만들며 느낀 점으로 웹팩에 익숙하지 않다면 어느정도의 러닝커브는 있어야할 것 같다는 생각이 들었다.
물론 모노레포와 마이크로 프론트엔드에 대해 이해도가 뒤쳐진 나로서는 본문의 글이 용두사미로 끝나는 것 같다는 느낌을 없지않아 받게 되는데, 나 스스로도 직접 서비스를 만들어가며 구축하는 시간을 가져보아야할 것 같다.

profile
안녕하세요! FE개발자 최근원입니다.

0개의 댓글