Skip to content

Redux 상태관리 컴포넌트 - ANTD 기반 MailList 구현

Redux Toolkit과 Ant Design을 조합하여 Shadow DOM 내부에서 동작하는 MailList 컴포넌트를 구현한다

학습 목표

  1. Redux Toolkit으로 마이크로 프론트엔드용 독립 스토어를 설계할 수 있다
  2. ANTD Table, Modal, Button을 활용한 MailList 컴포넌트를 구현할 수 있다
  3. Shadow DOM 내부에서 ANTD 스타일이 올바르게 동작하도록 설정할 수 있다
  4. 마이크로 컴포넌트의 상태관리 패턴을 이해한다

본문

1. MailList 컴포넌트 아키텍처

MailList 컴포넌트는 docs 프로젝트에서 제공하는 마이크로 컴포넌트로, 독립적인 Redux 스토어와 ANTD UI를 가진다. 이 컴포넌트는 다른 프로젝트(web, legacy)에서 자유롭게 소비될 수 있다.

2. Redux Toolkit으로 Store 구현

데이터 모델과 초기 상태

typescript
// store.ts
import { createSlice, configureStore, PayloadAction } from "@reduxjs/toolkit";

interface Email {
  id: number;
  sender: string;
  receiver: string;
  content: string;
}

const initialEmails: Email[] = [
  { id: 1, sender: "김철수", receiver: "이영희", content: "회의 일정 안내" },
  { id: 2, sender: "박지민", receiver: "최수진", content: "프로젝트 진행 상황" },
  { id: 3, sender: "정우성", receiver: "한지민", content: "코드 리뷰 요청" },
  { id: 4, sender: "이수현", receiver: "강동원", content: "배포 완료 알림" },
];

Slice와 Store 설정

typescript
const emailSlice = createSlice({
  name: "emails",
  initialState: initialEmails,
  reducers: {
    deleteEmail: (state, action: PayloadAction<number>) => {
      return state.filter((email) => email.id !== action.payload);
    },
  },
});

export const { deleteEmail } = emailSlice.actions;

// 셀렉터
export const selectEmails = (state: RootState) => state.emails;

// 스토어
const store = configureStore({
  reducer: {
    emails: emailSlice.reducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;

3. ANTD Table로 MailList 컴포넌트 작성

컴포넌트 구현

tsx
import { Table, Button, Modal } from "antd";
import { useSelector, useDispatch } from "react-redux";
import { selectEmails, deleteEmail } from "./store";

function MailList() {
  const emails = useSelector(selectEmails);
  const dispatch = useDispatch();
  const [selectedEmail, setSelectedEmail] = useState<Email | undefined>();

  const columns = [
    { title: "보낸 사람", dataIndex: "sender", key: "sender" },
    { title: "받은 사람", dataIndex: "receiver", key: "receiver" },
    { title: "내용", dataIndex: "content", key: "content" },
    {
      title: "처리",
      dataIndex: "id",
      key: "action",
      render: (id: number) => (
        <Button
          danger
          onClick={(e) => {
            e.stopPropagation();
            dispatch(deleteEmail(id));
          &#125;&#125;
        >
          삭제
        </Button>
      ),
    },
  ];

  return (
    <>
      <Table
        columns={columns}
        dataSource={emails}
        rowKey="id"
        onRow={(record) => ({
          onClick: () => setSelectedEmail(record),
          style: { cursor: "pointer" },
        })}
      />
      <Modal
        open={!!selectedEmail}
        title="메일 상세"
        onCancel={() => setSelectedEmail(undefined)}
        getContainer={shadowRoot}
      >
        <p>보낸 사람: {selectedEmail?.sender}</p>
        <p>받은 사람: {selectedEmail?.receiver}</p>
        <p>내용: {selectedEmail?.content}</p>
      </Modal>
    </>
  );
}

4. GlobalProvider 구성

ANTD 스타일을 Shadow DOM 내부에 삽입하기 위해 @ant-design/cssinjsStyleProvider를 사용한다.

tsx
import { Provider } from "react-redux";
import { StyleProvider } from "@ant-design/cssinjs";
import store from "./store";

interface GlobalProviderProps {
  children: React.ReactNode;
  shadowRoot: ShadowRoot;
}

function GlobalProvider({ children, shadowRoot }: GlobalProviderProps) {
  return (
    <StyleProvider container={shadowRoot}>
      <Provider store={store}>{children}</Provider>
    </StyleProvider>
  );
}

5. Shadow DOM에서의 Modal 처리

ANTD Modal은 기본적으로 document.body에 포탈로 렌더링된다. Shadow DOM 환경에서는 getContainer로 렌더링 위치를 지정해야 한다.

6. UMD Export로 마이크로 컴포넌트 제공

typescript
// index.tsx
import MailList from "./MailList";
import GlobalProvider from "./GlobalProvider";

// UMD로 Export - 다른 프로젝트에서 소비 가능
const render = (container: HTMLElement, shadowRoot: ShadowRoot) => {
  const root = ReactDOM.createRoot(container);
  root.render(
    <GlobalProvider shadowRoot={shadowRoot}>
      <MailList />
    </GlobalProvider>
  );

  return () => root.unmount();
};

// window 객체에 등록
if (!window.docs) window.docs = {};
window.docs.MailList = { render };
window.docs.default = { render: (container, props) => {
  render(container, props?.shadowRoot);
&#125;&#125;;

7. Redux와 마이크로 프론트엔드의 관계

설계 원칙설명
독립 스토어각 마이크로 컴포넌트가 자체 Redux 스토어를 가진다
상태 비공유프로젝트 간 Redux 상태를 직접 공유하지 않는다
컴포넌트 캡슐화Provider를 포함한 전체 구성을 하나의 단위로 제공한다
이벤트 통신프로젝트 간 통신이 필요하면 Custom Event를 사용한다

핵심 정리

  1. Redux Toolkit의 간결함: createSlice로 액션과 리듀서를 한 번에 정의하고, configureStore로 스토어를 구성한다
  2. ANTD + Shadow DOM: StyleProvidercontainer에 Shadow Root를 전달하고, Modal 등 포탈 컴포넌트에는 getContainer를 설정한다
  3. 마이크로 컴포넌트 패턴: Redux Provider를 포함한 전체 트리를 하나의 render 함수로 제공하여, 소비하는 측에서는 단순히 호출만 하면 된다
  4. 독립 상태관리: 각 마이크로 컴포넌트가 독립 스토어를 가지므로 프로젝트 간 상태 의존성이 없다

다음 단계

Zustand 상태관리 컴포넌트 ->