Skip to content

포스팅 마이크로앱 구현

CSS Modules를 활용하여 스타일 격리를 보장하고, Auth0 클라이언트로 API 인증을 처리하며, Module Federation Remote 설정으로 Shell에 연결하는 포스팅 마이크로앱을 구현한다.

학습 목표

  • 마이크로앱 내부에서 Auth0 SPA 클라이언트를 구성하고 토큰을 획득하는 방법을 이해한다
  • CSS Modules를 활용한 스타일 격리 패턴을 적용할 수 있다
  • 포스팅 CRUD(생성, 조회, 삭제) 기능을 구현할 수 있다
  • Module Federation의 exposes 설정을 통해 마이크로앱을 Remote로 노출하는 방법을 익힌다
  • Context + Provider 패턴으로 Auth0 클라이언트를 앱 전체에 공유하는 구조를 설계할 수 있다

1. 포스팅 마이크로앱의 전체 구조

포스팅 마이크로앱은 Shell에서 Module Federation으로 로드되는 Remote 앱이다. 독립 실행도 가능하고, Shell에 임베드되어 실행될 수도 있다.


2. Auth0 클라이언트 Provider 패턴

각 마이크로앱은 Auth0 SPA JS 클라이언트를 독립적으로 생성하여 토큰을 획득한다. Context + Provider 패턴으로 앱 전체에 클라이언트를 공유한다.

typescript
// src/providers/Auth0ClientProvider.tsx
import React from "react";
import { Auth0Client } from "@auth0/auth0-spa-js";

const Auth0ClientContext = React.createContext<Auth0Client | null>(null);

export { Auth0ClientContext };

const Auth0ClientProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
  const domain = process.env.REACT_APP_AUTH0_DOMAIN as string;
  const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID as string;
  const redirectUri = process.env.REACT_APP_AUTH0_CALLBACK_URL as string;

  const auth0Client = new Auth0Client({
    domain,
    clientId,
    authorizationParams: { redirect_uri: redirectUri }
  });

  return (
    <Auth0ClientContext.Provider value={auth0Client}>
      {children}
    </Auth0ClientContext.Provider>
  );
};

export default Auth0ClientProvider;
typescript
// src/hooks/useAuth0Client.ts
import { useContext } from "react";
import { Auth0ClientContext } from "../providers/Auth0ClientProvider";

export default function useAuth0Client() {
  const auth0Client = useContext(Auth0ClientContext);
  if (!auth0Client) {
    throw new Error("Auth0ClientProvider로 감싸지 않았습니다.");
  }
  return auth0Client;
}

중요: Shell은 @auth0/auth0-react를, 마이크로앱은 @auth0/auth0-spa-js를 사용한다. Shell에서 전체 인증을 관리하고, 마이크로앱에서는 getTokenSilently()로 토큰만 획득하는 구조다.


3. CSS Modules를 활용한 스타일 격리

포스팅 마이크로앱은 CSS Modules를 사용하여 클래스명 충돌을 방지한다. Webpack의 css-loader가 클래스명을 자동으로 해시 기반 고유 값으로 변환한다.

css
/* src/pages/PageHome.module.css */
.wrapper {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 680px;
  margin: 0 auto;
  padding: 20px;
}

.postForm {
  display: flex;
  flex-direction: column;
  gap: 8px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
}

.postForm textarea {
  resize: vertical;
  min-height: 80px;
  border: 1px solid #d0d0d0;
  border-radius: 4px;
  padding: 8px;
}

.postList {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
tsx
// CSS Modules 사용 예시
import styles from "./PageHome.module.css";

const PageHome: React.FC = () => {
  return (
    <div className={styles.wrapper}>
      <div className={styles.postForm}>
        <textarea placeholder="무슨 생각을 하고 계신가요?" />
        <button>게시</button>
      </div>
      <div className={styles.postList}>
        {/* PostItem 컴포넌트 렌더 */}
      </div>
    </div>
  );
};

CSS Modules vs 다른 스타일링 비교:

방식격리 수준런타임 비용사용처
CSS Modules빌드 타임 해시없음포스팅 앱
Emotion (CSS-in-JS)JS 런타임 생성있음교육 앱
Tailwind CSS유틸리티 접두사없음네트워킹 앱

4. 포스팅 CRUD 기능 구현

API 호출 함수와 PageHome 컴포넌트에서 토큰 획득 후 CRUD를 수행하는 흐름이다.

typescript
// src/api.ts
export async function getPosts(token: string): Promise<PostType[]> {
  const response = await fetch(
    "http://localhost:4000/posts?_sort=id&_order=desc",
    { headers: { authorization: `Bearer ${token}` } }
  );
  return await response.json();
}

export async function createPost(
  token: string,
  message: string
): Promise<PostType> {
  const response = await fetch("http://localhost:4000/posts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      authorization: `Bearer ${token}`
    },
    body: JSON.stringify({ message })
  });
  return await response.json();
}

export async function deletePost(
  token: string,
  id: number
): Promise<void> {
  await fetch(`http://localhost:4000/posts/${id}`, {
    method: "DELETE",
    headers: { authorization: `Bearer ${token}` }
  });
}

5. Module Federation Remote 설정

포스팅 마이크로앱을 Remote로 노출하기 위한 Webpack 설정이다.

javascript
// apps/posting/webpack.config.js (주요 설정)
const { ModuleFederationPlugin } = require("webpack").container;
const Dotenv = require("dotenv-webpack");

module.exports = {
  // ...
  plugins: [
    new Dotenv({ path: "../../.env" }),
    new ModuleFederationPlugin({
      name: "posting",
      filename: "remoteEntry.js",
      exposes: {
        "./injector": "./src/injector.tsx"
      },
      shared: {
        react: { singleton: true, requiredVersion: false },
        "react-dom": { singleton: true, requiredVersion: false },
        "react-router-dom": { singleton: true, requiredVersion: false },
        "@career-up/shell-router": {
          singleton: true,
          requiredVersion: false
        },
        "@career-up/uikit": {
          singleton: true,
          requiredVersion: false
        }
      }
    })
  ]
};
typescript
// src/injector.tsx (Module Federation이 노출하는 진입점)
import { injectFactory } from "@career-up/shell-router";
import { routes } from "./routes";

const inject = injectFactory({ routes });

export default inject;
typescript
// src/routes.tsx
import React from "react";
import type { RouteObject } from "react-router-dom";
import Auth0ClientProvider from "./providers/Auth0ClientProvider";
import { AppRoutingManager } from "@career-up/shell-router";
import PageHome from "./pages/PageHome";

export const routes: RouteObject[] = [
  {
    path: "/",
    element: (
      <Auth0ClientProvider>
        <AppRoutingManager type="app-posting" />
      </Auth0ClientProvider>
    ),
    errorElement: <div>app-posting-error</div>,
    children: [
      { index: true, element: <PageHome /> }
    ]
  }
];

독립 실행 vs Shell 연결:

진입 경로흐름라우터 타입
독립 실행index.ts -> bootstrap.tsx -> inject({ routerType: "browser" })BrowserRouter
Shell 연결Shell -> injector.tsx -> inject({ routerType: "memory" })MemoryRouter

6. Shell에서 Remote 등록

Shell의 Webpack 설정에서 포스팅 마이크로앱을 Remote로 등록한다.

javascript
// apps/shell/webpack.config.js (remotes 설정)
new ModuleFederationPlugin({
  name: "shell",
  remotes: {
    posting: "posting@http://localhost:3001/remoteEntry.js",
    // edu, network, job도 동일 패턴으로 등록
  },
  shared: { /* ... */ }
})
typescript
// apps/shell/tsconfig.json (paths 설정)
{
  "compilerOptions": {
    "paths": {
      "posting/injector": ["../posting/src/injector.tsx"]
    }
  }
}

핵심 정리

  1. Auth0ClientProvider는 Context + Provider 패턴으로 Auth0 SPA 클라이언트를 앱 전체에 공유한다
  2. getTokenSilently()로 사용자 개입 없이 토큰을 획득하고, API 요청의 Authorization 헤더에 포함한다
  3. CSS Modules는 빌드 타임에 클래스명을 해시화하여 마이크로앱 간 스타일 충돌을 방지한다
  4. exposesinjector.tsx를 등록하면 Shell이 이 파일을 Remote 진입점으로 사용한다
  5. injectFactory는 shell-router 패키지에서 제공하는 팩토리 함수로, 라우터 타입에 따라 BrowserRouter 또는 MemoryRouter를 생성한다
  6. .env 파일을 모노레포 루트에 두고 dotenv-webpackpath 옵션으로 공유하면 환경 변수를 중복 관리하지 않아도 된다

다음 단계

  • 03-교육-서비스.md: Emotion(CSS-in-JS)으로 스타일링하고 Jotai로 상태 관리하며, 교육 콘텐츠 목록/상세 페이지를 구현한다