병원용 SaaS처럼 복잡하고 거대한 프론트엔드를 효과적으로 다루기 위해 도입한 Micro Frontend 구조와 그것을 구현하기 위한 Module Federation에 대한 설명과 적용법, 고생했던 부분들을 실제 코드와 사례를 기반으로 설명합니다.

안녕하세요, 프론트엔드 개발자 Carter입니다.
저는 현재 힐링페이퍼에서 미용 의료 병원용 SaaS 제품을 개발하고 있습니다.

아마 많은 분이 '미용 의료 병원용 SaaS 제품(이하 KOS)'이란 용어를 처음 들어보실 텐데요, 요약하자면 병원에서 일어나는 다양한 운영 활동을 효율적으로 관리하기 위한 제품입니다. 앞서 공개한 '병원향 B2B SaaS 제품에 도전합니다' 글에서 만들게 된 이유와 설명을 정리해 뒀으니 먼저 읽고 오시는 걸 추천해 드립니다.

'KOS'는 매우 많은 기능을 포함하고 있기 때문에, 제품의 규모는 상당히 큽니다. 그리고 프론트엔드 개발자로서 이는 저에게 큰 도전이었습니다. 따라서 이 거대한 프로젝트를 시작하기 전에, 팀원들과 함께 우리가 만들려는 제품의 청사진을 그리고, 개발 과정에서 발생할 수 있는 잠재적인 문제들을 사전에 논의하는 것이 필요하다고 생각했습니다.

가장 큰 문제 두 가지만 소개해 드리자면,

우리 제품이 마주할 문제들

1. 제품의 파편화

다른 B2B 제품과 동일하게, KOS도 고객사인 병원들의 다양한 특성과 요구사항에 부응해야 하므로 파편화 문제를 피해 갈 수 없습니다. 예를 들어, 어떤 병원은 그 병원에만 최적화된 예약 기능을 요구한다거나, 반대로 예약 기능을 전혀 사용하지 않겠다고 요구할 수도 있습니다. 혹은, 이전 버전의 예약 기능 사용을 요구할 수도 있습니다.

이런 다양한 요구사항으로 인해 개발자들은 지속해 변화하는 요구사항에 대응해야 하고, 이로 인해 코드 관리와 유지 보수가 복잡해질 수 있습니다. 더 나아가, 특정 기능에 대한 파편화가 전체 제품의 가치 전달을 어렵게 할 수 있으며, 이로 인해 고객에게 일관된 경험을 제공하는 것이 어려워질 수 있다고 예상했습니다.

2. 수많은 기능과 도메인이 가져오는 복잡성

'병원의 전반적인 운영'을 지원하는 저희의 제품은 예약, 시술 정보 관리, 수납, 원내 환자 관리부터, 더 나아가 고객 관리부터 데이터 분석 등 다양한 영역이 포함되어 있습니다.

제품 내의 다양한 기능들은 서로 영향을 주기 마련인데, 그 수가 늘어나면 의도치 않은 영향이 더욱 빈번하게 생길 수 있으며, 얼마나 영향을 미치는지 알기 어렵게 됩니다. 이는 전체 시스템의 안정성을 저하할 위험이 있습니다. 예를 들어, 예약 시스템을 수정하면서 내원객 관리와 같은 전혀 관련 없어 보이는 부분에 영향을 미칠 수 있는데, 이러한 부작용은 전체 프로젝트의 복잡성을 높게 만듭니다. 더구나, 전체 빌드를 재수행해야 할 때 큰 규모 때문에 느려진 빌드 시간은 개발자들의 작업 속도를 더욱 늦출 수 있습니다.

이런 문제들을 해결하기 위해서, KOS 팀은 아래와 같은 목표를 세웠습니다.

목표

1. 각 애플리케이션은 도메인을 기준으로 분리해야 한다.

도메인 논리에 의해 애플리케이션이 나눌 수 있어야 하고, 이는 전체 페이지뿐 아니라 해당 도메인 논리를 사용하는 작은 코드도 사용할 수 있음을 의미합니다.

2. 각 애플리케이션은 개별 배포가 가능해야 한다.

여러 애플리케이션에서 사용하는 기능이 수정되었을 때 이를 의존하는 애플리케이션을 다시 빌드할 필요가 없어야 합니다.

3. 각 애플리케이션을 블록 조립하듯 합칠 수 있어야 한다.

병원의 요구사항에 맞는 제품을 제공할 수 있도록, 특정 애플리케이션을 제외하거나, 포함하거나, 특정 애플리케이션의 특정 버전을 선택해 조립할 수 있어야 합니다.


마이크로 프론트엔드(Micro Frontends)

목표를 달성하기 위한 여러 구조를 고민하던 와중, 거대한 모놀리스 구조의 복잡성을 더 작은 여러 개의 애플리케이션으로 분리하기 위해 시작된 마이크로 프론트엔드가 우리 제품의 문제를 해결할 수 있을 거라 판단했습니다.

마이크로 프론트엔드는 마이크로서비스 아키텍처의 원칙을 프론트엔드에 적용한 것입니다. 이는 각각의 프론트엔드 팀이 독립적으로 개발하고 배포할 수 있도록 하여, 전체 시스템의 복잡성을 줄이고 생산성을 향상하는 데 도움이 됩니다. 하지만 이러한 분리된 애플리케이션들이 결국 사용자에게는 하나의 웹 페이지로 제공해야 하므로, 이를 가능하게 하는 여러 기법이 존재합니다.

  • 서버 측 템플릿 조합(Server-side template composition)
  • 빌드 타임 통합(Build-time integration)
  • iframe을 통한 런타임 통합(Run-time integration via iframe)
  • 웹 컴포넌트를 통한 런타임 통합(Run-time integration via Web Components)
  • 자바스크립트를 통한 런타임 통합(Run-time integration via Javascript) ← 선택!

그 중 자바스크립트를 통한 런타임 통합 기법에 대해 조금 더 자세히 들여다보자면,

  • 웹 페이지를 불러온 시점에, 컨테이너 애플리케이션이 어떤 애플리케이션을 마운트할지 결정하고 관련 함수를 호출하여 애플리케이션에 렌더링할 시기와 위치를 알려주는 기법입니다.
  • 장점
    • 빌드 타임 통합과 달리, 각 애플리케이션을 독립적으로 배포할 수 있습니다.
    • iframe과 달리, 애플리케이션 간의 통합을 원하는 대로 유연하게 구축할 수 있습니다.
    • 런타임에 동적으로 애플리케이션을 로드할 수 있어 유연성이 높습니다.
  • 단점
    • 런타임에 통합되는 과정에서, 애플리케이션 마다 중복된 코드(예: React)를 불러올 수 있습니다.
    • 런타임에 동적으로 애플리케이션을 로드하기 때문에, 해당 애플리케이션을 불러오지 못해 통합하지 못하는 문제가 발생할 수 있습니다.
    • 런타임에서 통합되기 때문에 빌드 타임에 타입 검사가 어려울 수 있으며, 빌드 타임에서는 문제가 발생하지 않았으나, 통합하는 과정에서 예상치 못한 문제가 발생할 수 있습니다.

(다른 기법들에 대한 자세한 설명이 궁금하시다면 Martin Fowler의 아티클을 읽어보세요.)

위의 여러 기법 중, KOS 팀은 특히 ‘자바스크립트를 통한 런타임 통합’ 기법을 사용하면, 원하는 목표를 이룰 수 있을 거로 생각했고, ‘자바스크립트를 통한 런타임 통합’을 구현하는 여러 방법 중 하나를 선택했습니다.

  • 직접 애플리케이션을 불러오도록 구현하기
  • SystemJS을 사용해 구현하기
  • Import Maps을 사용해 구현하기
  • Module Federation을 사용해 구현하기 ← 선택!

Module Federation(모듈 페더레이션)

모듈 페더레이션은 Webpack의 기능 중 하나로, ModuleFederationPlugin을 사용하여 대규모 애플리케이션을 더 작고 관리하기 쉬운 모듈로 분리할 수 있게 해줍니다. 이를 통해 다른 애플리케이션의 모듈을 런타임에서 불러와 사용할 수 있으며, 이는 Code Splitting과 유사한 점이 있습니다.

그러나 Code Splitting과는 달리, 모듈 페더레이션은 별도의 Webpack 애플리케이션의 기능을 독립된 애플리케이션으로 분리할 수 있게 해줍니다. 덕분에 개별적으로 개발하고 배포할 수 있게 되어 유연성과 확장성을 높일 수 있습니다.

아래는 모듈 페더레이션의 주요 개념들입니다.

  1. Host: 원격 모듈을 불러오는 애플리케이션입니다.
  2. Local Module: 로컬 모듈은 현재 빌드의 일부로, 일반적인 모듈입니다. 이는 원격 모듈과 구별되며, 현재 애플리케이션 또는 프로젝트 내에서 정의되고 사용됩니다.
  3. Remote Module: 원격 모듈은 현재 빌드의 일부가 아닌, 원격 컨테이너에서 런타임에 로드되는 모듈입니다.
  4. Exposes: 원격 모듈로 공개할 부분을 지정합니다. 공개된 부분만 Host가 불러와 사용할 수 있습니다.
  5. Container: 컨테이너는 특정 모듈에 대한 비동기 접근을 노출하는 엔트리를 통해 생성됩니다. 컨테이너는 다른 컨테이너의 모듈을 사용할 수 있으며, 중첩 및 순환 의존성도 가능합니다.

그림으로 각 개념을 표현하자면 아래와 같습니다.

이제 Webpack 설정 파일에서 ModuleFederationPlugin 을 사용하는 간단한 코드와 함께 설명해 보겠습니다.

원격 모듈을 내보낼 프로젝트에서, 아래와 같이 Webpack 설정을 한 뒤,

// reservation-client/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  ...,
  plugins: [
    ...,
    new ModuleFederationPlugin({
      // 원격 모듈의 이름
      name: 'reservation',
      // 원격 모듈에서 공개할 부분
      exposes: {
        // `./ReservationRoutes`라는 path로 `./src/routes/ReservationRoutes.tsx` 를 expose함
        './ReservationRoutes': './src/routes/ReservationRoutes',
      },
    }),
    // 번들링을 할 때, remoteEntry.js 파일이 생성됨
  ],
};

호스트 역할을 할 프로젝트에서, 아래와 같이 설정합니다.

// root-client/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 호스트의 이름
      name: 'root',
      // 사용할 원격 모듈들
      remotes: {
        // reservation 원격 모듈을 http://localhost:3001/remoteEntry.js로 부터 불러옴
        reservation: 'reservation@http://localhost:3001/remoteEntry.js',
      },
    }),
  ],
};

그러면, 호스트 프로젝트에서 아래처럼 Dynamic Import나 일반적인 Import 문처럼 원격 모듈을 불러와 사용할 수 있습니다.

// 1. Dynamic Import로 불러오기 (w/ React.lazy)
const RemoteReservationRoutes = React.lazy(() => import('reservation/ReservationRoutes'));

const ReservationRoutes: React.FC = () => (
  {/* 원격 모듈을 불러오는 와중 에러가 발생할 수 있으니 ErrorBoundary로 감싸줌 */}
  <ErrorBoundary FallbackComponent={ErrorFallback}>
    {/* 원격 모듈을 불러오는 중에, 대신 로딩 컴포넌트를 보여줌 */}
    <Suspense fallback={<FullscreenLoading />}>
      <RemoteReservationRoutes />
    </Suspense>
  </ErrorBoundary>
);

const AppRotues: React.FC = () => (
  <Routes>
    ...
    <Route
      path="reservation/*"
      element={<ReservationRoutes />}
    />
  </Routes>
);

// 2. 일반적인 import 방식으로 쓰기
import ReservationRoutes from 'reservation/ReservationRoutes';

...

이렇게 구성한다면, reservation-client의 ReservationRoutes 컴포넌트가 업데이트되었을 때(예: 새로운 Route가 등록됨), root-client도 재배포할 필요가 없습니다. reservation-client의 원격 모듈만 다시 배포하면 됩니다. 위 예제에서는 React 컴포넌트를 예로 들었으나, 컴포넌트 외에도 함수나 Export 할 수 있는 모든 것들은 Expose 될 수 있습니다.

또한, 앞서 마이크로 프론트엔드 기법 중 ‘자바스크립트를 통한 통합’을 설명하며, ‘런타임에 통합되는 과정에서, 애플리케이션 마다 중복된 코드를 불러올 수 있습니다.’라고 말씀드렸는데요.

ModuleFederationPlugin 을 설정할 때 아래처럼 shared를 설정하면, 호스트나 여러 원격 모듈에서 사용되는 공통된 패키지를 중복으로 불러오는 걸 방지할 수 있습니다.

new ModuleFederationPlugin({
  shared: {
    ...deps,
    react: {
      eager: true,
    },
  },
});

eager 외에도 requiredVersion, singleton, strictVersionshared에 대한 여러 설정을 할 수 있습니다. 자세한 설정은 아래 Webpack의 SharedConfig 인터페이스를 확인해 보세요.

자 이제, 모든 개념은 정리가 되었으니, 마이크로 프론트엔드를 KOS에 적용할 시간입니다.

실제로 적용하기

우리가 만드는 SaaS 제품의 구조

KOS 팀은 제품을 도메인 단위로 분리하여 독립적인 배포가 가능하게 하고, Host인 root-client가 각 도메인의 클라이언트로부터 원격 모듈을 불러와 조합하는 형태로 구성했습니다.

그리고, ‘엔터프라이즈 환경으로 확장하기 쉬운 멀티테넌시 서비스 구축기’ 에서 공유해 드렸듯, KOS는 매우 많은 엔터프라이즈 환경을 예상하는 Multi tenancy 환경입니다. 때문에 회원가입, 로그인 등 전체 Tenant에 적용되어야 하는 클라이언트는 common namespace에, 예약과 시술 관리 등 각 Tenant별 적용되어야 하는 클라이언트는 해당하는 namespace에 배포하고 있습니다. (배포 방식에 대해 더 알고 싶다면, 이 글을 확인해 보세요.)

사용자가 특정 URL Path에 접속하면 root-client가 해당하는 원격 모듈을 불러와 그 에 맞는 Route 컴포넌트를 보여주도록 했습니다. 만약 해당하는 원격 모듈의 클라이언트가 namespace에 배포되어 있지 않다면, 사이드바에서 제외하고, 해당 주소로 접속했을 때, 404 화면을 보여줍니다.

언제나 특정 URL Path에 접속했을 때 한 개의 원격 모듈을 불러오는 것은 아닙니다. 위의 이미지에는 없지만, 예를 들어 예약의 경우, 어떤 내원객이 예약했는지 정보를 확인할 수 있어야 하며, 필요에 따라 내원객의 정보를 수정해야 하므로 root-client가 예약 원격 모듈을 불러왔을 때, 예약 원격 모듈이 내원객 관리 원격 모듈을 불러오게 됩니다.

KOS 팀은 개발 환경과 운영 환경을 분리하여 개발하고 있습니다. 두 환경의 차이로 인해 고정적인 주소론 원격 모듈을 불러올 수 없기 때문에, 아래와 같이 ModuleFederationPlugin을 설정할 때, remotes에 고정적인 주소 대신, Promise를 전달해 런타임에서 환경에 맞는 주소로 원격 모듈을 불러오도록 구성했습니다.

// Example
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 호스트의 이름
      name: 'root',
      // 사용할 원격 모듈들
      remotes: {
        // reservation 원격 모듈을 `${window.__ENV__.root.REMOTE_BASE_URI}/remoteEntry.js`로 부터 불러오고, window.reservation Container를 resolve한다.
        reservation: `promise new Promise((resolve, reject) => {
            const remoteUrl = (window.__ENV__.root.REMOTE_BASE_URI || '') + '/reservation/remoteEntry.js';

          const script = document.createElement('script');
          script.src = remoteUrl;
          script.onload = () => {
            document.head.removeChild(script);
            resolve(window.reservation);
          };
          script.onerror = () => {
            reject();
          };
          document.head.appendChild(script);
          })`,
      },
      ...,
    }),
  ],
};

적용하면서 발생한 불편한 점들과 해결 방법

모듈 페더레이션을 사용해 ‘자바스크립트를 통한 런타임 통합’ 기법을 구현하는 건 예상했던 건 만큼 문제가 발생하지 않았습니다. 오히려 런타임보단 개발 환경과 관련된 문제를 많이 발견했습니다.

첫번째로, 원격 모듈에서 Expose가 변경될 때마다, Webpack 설정을 수정해야 했습니다.

심각한 문제는 아니지만, 매번 Webpack 설정의 exposes에 Path와 Expose 할 파일의 경로를 적어야 하므로, Path에 오타가 있거나, 파일 경로를 잘못 입력하는 문제가 발생할 수 있었습니다.

// reservation-client/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  ...,
  plugins: [
    ...,
    new ModuleFederationPlugin({
      name: 'reservation',
      exposes: {
        './ReservationRoutes': './src/routes/ReservationRoutes',
        ...,
      },
    }),
  ],
};

팀 내에서 ‘직접 Webpack 설정을 수정하지 않고, Next.js apppages처럼, 디렉터리 기반으로 exposes 설정이 자동 생성되면 어떨까?’라는 아이디어를 냈고, 기존에 공통된 Webpack 설정을 공유하기 위해 내부적으로 사용하고 있던 client-build-helper라는 패키지를 확장하기로 했습니다.

프로젝트의 src/exposes 폴더 안에 .ts 혹은 .tsx 파일을 생성하면, 자동으로 파일명 기반으로 Path와 파일 경로를 불러와 Exposes 설정을 만들도록, 간단한 함수를 구현했습니다.

아래 예제처럼 사용할 수 있습니다.

예:

# 프로젝트 구조
reservation-client/
├─ src/
│  ├─ exposes/
│  │  ├─ routes.tsx
│  │  ├─ state/
│  │  │  ├─ slice.ts
...
// 자동으로 생성될 exposes 설정
{
  "./routes": "./src/exposes/routes",
  "./state/slice": "./src/exposes/state/slice",
}
// root-client 혹은 다른 Host 애플리케이션
const Routes = React.lazy(() => import('reservation/routes'));
...

const { default: reservationViewer } = await import('reservation/state/slice');
// reservation-client의 webpack.common.js (dev, prod의 공통 설정)
const { common } = require('@healingpaper-solution/client-build-helper');

module.exports = common({
    name: 'reservation',
});

덕분에, 직접 설정 파일을 수정할 필요가 없어졌으며, 추가로 Expose 하려면 정해진 디렉터리에 파일을 생성하는 게 강제되다 보니, 로컬 모듈로만 사용되는 코드와 원격 모듈로 사용될 수 있는 코드를 명확히 구분할 수 있게 되었습니다.

두 번째는 첫 번째 문제보다 심각했습니다. TypeScript 환경의 Host에서 원격 모듈을 불러오려고 할 때, 타입 문제가 발생했습니다.

‘빌드 타임 통합’ 기법과 달리 KOS의 원격 모듈은 런타임에서 통합되기 때문에, 개발 환경에서의 TypeScript 입장에선 어떤 모듈이 무엇을 Expose 했는지, Expose 한 컴포넌트나 함수가 어떤 인자를 받는 지, 어떤 타입인지를 알 수 없었습니다. 게다가 KOS는 모노레포가 아닌 폴리레포 구조를 사용하고 있기 때문에 tsconfig의 paths 속성을 활용해 Resolve 하는 방법도 불가했습니다. 이 문제를 겪으면서 개발할 순 없었기 때문에, 타입 문제를 해결하고, 안정적으로 개발하기 위해 expose-typed라는 간단한 CLI를 만들었습니다.

expose-typed는 아래 과정을 통해 Definitely Typed(@types/패키지명) 패키지처럼 원격 모듈의 타입 선언 패키지를 생성해 줍니다.

  1. Exposes 폴더 내의 TypeScript 파일을 기반으로, d.ts 파일을 생성합니다.
  2. 생성되었을 때, declare module "./src/exposes/..." 와 같이, Host 실제 불러올 때 사용하는 모듈 명과 다른 경우가 있기 때문에 declare module "reservation/..." 와 같이 모듈 명을 수정합니다.
  3. Host 애플리케이션이나, 해당 원격 모듈을 사용하는 프로젝트에서 사용할 수 있도록, exposed-<클라이언트 이름> 으로 GitHub Packages에 배포합니다.

그 후 아래와 같이 원격 모듈을 사용하는 프로젝트에서 해당 패키지를 설치하면, 문제가 해결됩니다.

root-client가 reservation-client의 원격 모듈을 사용하는 경우를 예로 들면,

$ yarn add -D @healingpaper-solution/exposed-reservation

이렇게 타입 문제가 해결되고 안정적으로 개발할 수 있게 되었습니다.

추가로, expose-typed CLI가 하나의 문제를 더 해결할 수 있도록 기능을 추가했습니다.

아래 코드는, 런타임에서 원격 모듈을 불러올 주소를 결정하기 위해 Promise를 선언한 Host의 Webpack 설정입니다.

// root-client/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'root',
      remotes: {
        reservation: `promise new Promise((resolve, reject) => {
				    const remoteUrl = (window.__ENV__.root.REMOTE_BASE_URI || '') + '/reservation/remoteEntry.js';
				
			      const script = document.createElement('script');
			      script.src = remoteUrl;
			      script.onload = () => {
			        document.head.removeChild(script);
			        resolve(window.reservation);
			      };
			      script.onerror = () => {
			        reject();
			      };
			      document.head.appendChild(script);
				  })`,
      },
      procedureManage: `promise new Promise((resolve, reject) => {
			    const remoteUrl = (window.__ENV__.root.REMOTE_BASE_URI || '') + '/procedure-manage/remoteEntry.js';
			
		      const script = document.createElement('script');
		      script.src = remoteUrl;
		      script.onload = () => {
		        document.head.removeChild(script);
		        resolve(window.procedureManage);
		      };
		      script.onerror = () => {
		        reject();
		      };
		      document.head.appendChild(script);
			  })`,
      },
      ...,
    }),
  ],
};

해당 원격 모듈을 불러오는 곳이 하나면 감당할 수 있지만, 해당 원격 모듈을 여러 곳에서 사용한다면 매번 저렇게 긴 Promise 코드를 등록하는 건 여간 번거로운 일이 아닙니다.

그래서 원격 모듈을 불러오는 쪽이 아닌, 내보내는 쪽에서 해당 Promise 코드를 관리하고, 더욱 수월하게 어떤 원격 모듈을 불러오는 지 확인할 수 있도록 expose-typed CLI가 Remote 용 Promise 코드를 내보내고, 불러오는 쪽에서 사용할 수 있도록 확장했습니다.

덕분에 위의 설정을 아래와 같이 간단하게 관리할 수 있게 되었습니다.

// root-client의 webpack.common.js (dev, prod의 공통 설정)
const { common } = require('@healingpaper-solution/client-build-helper');
const { reservationRemote } = require('@healingpaper-solution/exposed-reservation/remote');
const { procedureManageRemote } = require('@healingpaper-solution/exposed-procedure-manage/remote');

module.exports = common({
    name: 'root',
    remotes: [
        reservationRemote(),
        procedureManageRemote(),
        ...
    ]
});

마치며

이번 글에서는 왜 마이크로 프론트엔드 구조를 선택했고, 어떻게 구성하였는지를 공유해 드렸습니다.

현재 KOS 팀은 위에서 공유해 드렸던 설정들과 CI/CD 설정, 여러 환경 설정을 매번 처음부터 하지 않도록, client-template 이라는 Template Repository를 만들어서 관리하고 있습니다. Repository의 이름만 정하면 모든 설정이 자동으로 되어, 손쉽게 새로운 클라이언트 프로젝트를 시작할 수 있습니다.

다음에는 각 마이크로 프론트엔드 애플리케이션 내부를 중점으로 설명해 보려 합니다. 어떻게 상태를 관리하고 있는지와 각 애플리케이션 내부의 구조는 어떻게 구성된 지 등 실제 구현과 가까운 내용들을 공유해 드리겠습니다.

감사합니다.

SaaS 시리즈
프론트엔드
Micro Frontends를 위해 Module Federation 적용하기
[TBA] 복잡한 웹 애플리케이션을 위해 복잡하지 않은 구조 만들기
[TBA] TDD로 개발하기
백엔드
Private AWS EKS Cluster With Github Actions Enterprise 환경으로 확장하기 쉬운 Multi-Tenancy 서비스 구축하기 시간여행이 가능한 시스템 아키텍처
[TBA] Enterprise 서비스 비밀 정보 관리하기
[TBA] SaaS 서비스 이벤트 소싱하기
[TBA] TDD로 개발하기
Carter
Software Engineer
미용 의료 병원용 SaaS를 만들고 있습니다. 궁극의 제너럴리스트가 되는 게 꿈입니다.