Skip to content

채용 마이크로앱 구현

Redux Toolkit으로 전역 상태를 관리하고, Emotion으로 스타일링하며, 채용 공고 목록/상세/지원 기능을 구현하는 마이크로앱을 완성한다. Redux Store를 Module Federation shared로 설정하는 패턴을 학습한다.

학습 목표

  • Redux Toolkit의 createSliceconfigureStore로 마이크로앱 전용 Store를 구성할 수 있다
  • Redux Store를 React Provider로 감싸고 마이크로앱 라우트에 통합하는 방법을 이해한다
  • namespace() 유틸로 Redux 액션명에 접두사를 부여하여 전역 충돌을 방지하는 패턴을 적용할 수 있다
  • Module Federation에서 Redux를 shared로 설정할 때의 singleton 전략을 이해한다

1. Redux Toolkit 기반 상태 관리 아키텍처

채용 마이크로앱은 Redux Toolkit을 사용하여 채용 공고 목록과 지원 현황을 관리한다. createSlice로 액션과 리듀서를 동시에 정의하고, configureStore로 스토어를 생성한다.


2. Redux 모듈 구현

2-1. 네임스페이스 유틸리티

Redux 액션명에 마이크로앱 접두사를 부여하여 Shell이나 다른 마이크로앱의 액션과 충돌하지 않게 한다.

typescript
// src/redux/ns.ts
const prefix = "job/";

export const namespace = (name: string): string => `${prefix}${name}`;

2-2. applyStatus 슬라이스

typescript
// src/redux/modules/applyStatus.ts
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { namespace } from "../ns";
import type { ApplyStatusType } from "../../types";

interface InitialState {
  data: ApplyStatusType | null;
  loading: boolean;
  error: Error | null;
}

const initialState: InitialState = {
  data: null,
  loading: false,
  error: null
};

const { reducer, actions } = createSlice({
  name: namespace("applyStatus"),
  initialState,
  reducers: {
    start(state) {
      state.loading = true;
    },
    done(state, action: PayloadAction<ApplyStatusType>) {
      state.data = action.payload;
      state.loading = false;
    },
    fail(state, action: PayloadAction<Error>) {
      state.error = action.payload;
      state.loading = false;
    }
  }
});

export const { start, done, fail } = actions;
export default reducer;

2-3. Store 생성 팩토리

typescript
// src/redux/create.ts
import { configureStore } from "@reduxjs/toolkit";
import reducer from "./modules/rootReducer";

const create = () => {
  const store = configureStore({ reducer });
  return store;
};

export default create;

export type RootState = ReturnType<ReturnType<typeof create>["getState"]>;
export type AppDispatch = ReturnType<typeof create>["dispatch"];
typescript
// src/redux/modules/rootReducer.ts
import applyStatus from "./applyStatus";
// import jobList from "./jobList"; // 동일 패턴

const rootReducer = {
  applyStatus,
  // jobList,
};

export default rootReducer;

3. Redux Provider와 라우트 통합

typescript
// src/routes.tsx
import React from "react";
import type { RouteObject } from "react-router-dom";
import { Provider } from "react-redux";
import Auth0ClientProvider from "./providers/Auth0ClientProvider";
import { AppRoutingManager } from "@career-up/shell-router";
import Layout from "./components/Layout";
import create from "./redux/create";

const store = create();

export const routes: RouteObject[] = [
  {
    path: "/",
    element: (
      <Provider store={store}>
        <Auth0ClientProvider>
          <Layout>
            <AppRoutingManager type="app-job" />
          </Layout>
        </Auth0ClientProvider>
      </Provider>
    ),
    errorElement: <div>app-job-error</div>,
    children: [
      { index: true, element: <JobListPage /> },
      { path: ":id", element: <JobDetailPage /> }
    ]
  }
];

4. API 연동과 타입 정의

typescript
// src/types.ts
export interface ApplyStatusType {
  jobCount: number;
  myOnlineClassesCount: number;
  mySavedCount: number;
  updatesCount: number;
}

export interface JobType {
  id: number;
  company: string;
  position: string;
  location: string;
}
typescript
// src/apis.ts
import type { JobType, ApplyStatusType } from "./types";

export async function getJobs(token: string): Promise<JobType[]> {
  const response = await fetch("http://localhost:4000/jobs", {
    headers: { authorization: `Bearer ${token}` }
  });
  return await response.json();
}

export async function getApplyStatus(token: string): Promise<ApplyStatusType> {
  const response = await fetch("http://localhost:4000/applyStatus", {
    headers: { authorization: `Bearer ${token}` }
  });
  return await response.json();
}

5. Container에서 Redux 액션 디스패치

typescript
// src/containers/ApplyStatusContainer.tsx
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import type { RootState, AppDispatch } from "../redux/create";
import { start, done, fail } from "../redux/modules/applyStatus";
import useAuth0Client from "../hooks/useAuth0Client";
import { getApplyStatus } from "../apis";
import ApplyStatus from "../components/ApplyStatus";

const ApplyStatusContainer: React.FC = () => {
  const auth0Client = useAuth0Client();
  const dispatch = useDispatch<AppDispatch>();
  const applyStatus = useSelector(
    (state: RootState) => state.applyStatus.data
  );

  const patchApplyStatus = useCallback(async () => {
    try {
      dispatch(start());
      const token = await auth0Client.getTokenSilently();
      const data = await getApplyStatus(token);
      dispatch(done(data));
    } catch (e) {
      dispatch(fail(e as Error));
    }
  }, [auth0Client, dispatch]);

  return (
    <ApplyStatus
      applyStatus={applyStatus}
      patchApplyStatus={patchApplyStatus}
    />
  );
};

export default ApplyStatusContainer;

6. Module Federation에서 Redux shared 설정

Redux를 shared로 설정하면 Host와 Remote가 동일한 Redux 런타임을 공유한다. 다만 Store 인스턴스는 각 마이크로앱이 독립적으로 생성한다.

javascript
// apps/job/webpack.config.js
new ModuleFederationPlugin({
  name: "job",
  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 },
    "react-redux": { singleton: true, requiredVersion: false },
    "@reduxjs/toolkit": { singleton: true, requiredVersion: false },
    "@career-up/shell-router": { singleton: true, requiredVersion: false },
    "@career-up/uikit": { singleton: true, requiredVersion: false }
  }
})

4개 마이크로앱 기술 스택 종합 비교:

PostingEducationNetworkJob
포트3001300230033004
스타일링CSS ModulesEmotionTailwind CSSEmotion
상태 관리useStateJotaiRecoilRedux Toolkit
격리 접두사해시 (자동)edu--network--job--
shared 추가---react-redux, @reduxjs/toolkit

핵심 정리

  1. Redux Toolkit의 createSlice는 액션 타입, 액션 생성자, 리듀서를 한 번에 정의하여 보일러플레이트를 크게 줄인다
  2. namespace() 유틸로 액션 이름에 job/ 접두사를 부여하면 다른 마이크로앱의 Redux 액션과 충돌하지 않는다
  3. configureStore 팩토리 함수로 Store를 생성하고, RootStateAppDispatch 타입을 export하여 타입 안전한 useSelector/useDispatch를 사용한다
  4. Module Federation의 sharedreact-redux@reduxjs/toolkitsingleton: true로 설정하면 Redux 런타임은 하나만 로드되지만 Store 인스턴스는 각 앱이 독립적으로 관리한다
  5. Provider 래핑 순서는 Provider(Redux) > Auth0ClientProvider > Layout > AppRoutingManager이다
  6. Container가 dispatch(start() -> done()/fail())하는 3단계 비동기 패턴으로 로딩/성공/실패 상태를 관리한다

다음 단계