테마
포스팅 마이크로앱 구현
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"]
}
}
}핵심 정리
- Auth0ClientProvider는 Context + Provider 패턴으로 Auth0 SPA 클라이언트를 앱 전체에 공유한다
getTokenSilently()로 사용자 개입 없이 토큰을 획득하고, API 요청의 Authorization 헤더에 포함한다- CSS Modules는 빌드 타임에 클래스명을 해시화하여 마이크로앱 간 스타일 충돌을 방지한다
exposes에injector.tsx를 등록하면 Shell이 이 파일을 Remote 진입점으로 사용한다injectFactory는 shell-router 패키지에서 제공하는 팩토리 함수로, 라우터 타입에 따라 BrowserRouter 또는 MemoryRouter를 생성한다.env파일을 모노레포 루트에 두고dotenv-webpack의path옵션으로 공유하면 환경 변수를 중복 관리하지 않아도 된다
다음 단계
- 03-교육-서비스.md: Emotion(CSS-in-JS)으로 스타일링하고 Jotai로 상태 관리하며, 교육 콘텐츠 목록/상세 페이지를 구현한다