Skip to content

프래그먼트 컴포넌트 작성

네트워킹 팀이 제공하는 "추천 1촌 맺기" 프래그먼트와 채용 팀이 제공하는 "추천 채용 공고" 프래그먼트를 Container/Presentational 패턴으로 구현하고, API를 연동한다.

학습 목표

  • 별도 런타임으로 프래그먼트를 생성하여 의존성을 격리하는 방법을 이해한다
  • 기존 마이크로앱 내부에 프래그먼트를 추가하여 exposes를 확장하는 방법을 이해한다
  • Container/Presentational 패턴으로 프래그먼트의 데이터 로직과 UI를 분리할 수 있다
  • 프래그먼트에서 shell-router의 useAuth0Client와 useShellNavigate를 활용할 수 있다

1. 두 가지 프래그먼트 제공 방식

커리어업 프로젝트에서는 두 가지 다른 방식으로 프래그먼트를 구현한다.


2. 추천 1촌 프래그먼트 (별도 런타임)

네트워킹 팀이 제공하는 프래그먼트다. fragments/ 폴더에 독립 MF 앱으로 생성하여 의존성을 철저히 격리한다.

2-1. 프로젝트 구조

fragments/fragment-recommend-connections/
  ├── package.json
  ├── webpack.config.js
  ├── .eslintrc.js
  └── src/
      ├── types.ts
      ├── apis.ts
      ├── containers/
      │   └── RecommendConnectionsContainer.tsx
      └── components/
          ├── RecommendConnections.tsx
          └── recommend-connections.css

2-2. 타입과 API

typescript
// src/types.ts
export interface ConnectionType {
  name: string;
  picture: string;
  role: string;
  networkCount: number;
}
typescript
// src/apis.ts
import type { ConnectionType } from "./types";

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

2-3. Container (데이터 로직)

typescript
// src/containers/RecommendConnectionsContainer.tsx
import React, { useState, useCallback } from "react";
import { useAuth0Client } from "@career-up/shell-router";
import { getConnections } from "../apis";
import type { ConnectionType } from "../types";
import RecommendConnections from "../components/RecommendConnections";

const RecommendConnectionsContainer: React.FC = () => {
  const auth0Client = useAuth0Client();
  const [connections, setConnections] = useState<ConnectionType[]>([]);

  const patchConnections = useCallback(async () => {
    try {
      const token = await auth0Client.getTokenSilently();
      const data = await getConnections(token);
      setConnections(data);
    } catch (e) {
      alert(e);
    }
  }, [auth0Client]);

  return (
    <RecommendConnections
      connections={connections}
      patchConnections={patchConnections}
    />
  );
};

export default RecommendConnectionsContainer;

2-4. Presentational (UI)

typescript
// src/components/RecommendConnections.tsx
import React, { useEffect } from "react";
import "./recommend-connections.css";
import type { ConnectionType } from "../types";

interface Props {
  connections: ConnectionType[];
  patchConnections: () => Promise<void>;
}

const RecommendConnections: React.FC<Props> = ({
  connections,
  patchConnections
}) => {
  useEffect(() => {
    patchConnections();
  }, [patchConnections]);

  return (
    <div className="fragment-recommend-connections">
      <div className="fragment-recommend-connections__header">
        <span>추천 1촌</span>
      </div>
      <div className="fragment-recommend-connections__list">
        {connections.map((conn, idx) => (
          <div key={idx} className="fragment-recommend-connections__item">
            <img
              src={conn.picture || "/default-avatar.png"}
              alt={conn.name}
              className="fragment-recommend-connections__avatar"
            />
            <div className="fragment-recommend-connections__info">
              <span className="fragment-recommend-connections__name">
                {conn.name}
              </span>
              <span className="fragment-recommend-connections__role">
                {conn.role}
              </span>
            </div>
            <button className="fragment-recommend-connections__btn">
              1촌 맺기
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default RecommendConnections;

3. 추천 채용 공고 프래그먼트 (기존 앱 확장)

채용 팀이 기존 apps/job 마이크로앱 내부에 프래그먼트를 추가하는 방식이다.

3-1. 프로젝트 구조 (기존 job 앱에 추가)

apps/job/src/
  ├── injector.tsx            (기존 마이크로앱 진입점)
  ├── fragments/              (새 폴더)
  │   ├── RecommendJobsContainer.tsx
  │   ├── RecommendJobs.tsx
  │   ├── RecommendJobs.styles.ts
  │   ├── RecommendJob.tsx
  │   └── RecommendJob.styles.ts
  ├── apis.ts                 (기존 API 재사용)
  └── types.ts                (기존 타입 재사용)

3-2. Container

typescript
// src/fragments/RecommendJobsContainer.tsx
import React, { useState, useCallback } from "react";
import { useAuth0Client } from "@career-up/shell-router";
import { getJobs } from "../apis";
import type { JobType } from "../types";
import RecommendJobs from "./RecommendJobs";

const RecommendJobsContainer: React.FC = () => {
  const auth0Client = useAuth0Client();
  const [jobs, setJobs] = useState<JobType[]>([]);

  const patchJobs = useCallback(async () => {
    try {
      const token = await auth0Client.getTokenSilently();
      const allJobs = await getJobs(token);
      setJobs(allJobs.slice(0, 3)); // 상위 3개만 추천
    } catch (e) {
      alert(e);
    }
  }, [auth0Client]);

  return <RecommendJobs jobs={jobs} patchJobs={patchJobs} />;
};

export default RecommendJobsContainer;

3-3. Presentational (useShellNavigate 활용)

typescript
// src/fragments/RecommendJob.tsx
import React from "react";
import { useShellNavigate } from "@career-up/shell-router";
import { RecommendJobWrapper } from "./RecommendJob.styles";

interface Props {
  id: number;
  position: string;
  company: string;
}

const RecommendJob: React.FC<Props> = ({ id, position, company }) => {
  const navigate = useShellNavigate();

  const onClick = () => {
    navigate(`/job/${id}`); // Shell의 BrowserRouter로 이동
  };

  return (
    <RecommendJobWrapper onClick={onClick}>
      <span className="job--recommend-job__position">{position}</span>
      <span className="job--recommend-job__company">{company}</span>
    </RecommendJobWrapper>
  );
};

export default RecommendJob;

기존 앱 확장 방식의 장점: 기존 apis.ts, types.ts를 그대로 재사용할 수 있다. 채용 팀이 이미 관리하고 있는 코드를 별도로 복사할 필요가 없다.


4. 두 방식의 비교와 코드 중복 문제

해결 방향: 별도 런타임 방식에서 코드가 중복되면, 팀 내부에서 공유 패키지(@career-up/network-shared)를 만들어 타입과 API 함수를 추출한다. 이것이 마이크로프론트엔드가 성숙해지며 자연스럽게 발전하는 패턴이다.


5. Webpack expose 설정

javascript
// 방식 1: fragments/fragment-recommend-connections/webpack.config.js
new ModuleFederationPlugin({
  name: "fragment_recommend_connections",
  filename: "remoteEntry.js",
  exposes: {
    "./container": "./src/containers/RecommendConnectionsContainer.tsx"
  },
  shared: {
    react: { singleton: true, requiredVersion: false },
    "react-dom": { singleton: true, requiredVersion: false },
    "@career-up/shell-router": { singleton: true, requiredVersion: false },
    "@career-up/uikit": { singleton: true, requiredVersion: false }
  }
})
javascript
// 방식 2: apps/job/webpack.config.js (기존 설정 확장)
new ModuleFederationPlugin({
  name: "job",
  filename: "remoteEntry.js",
  exposes: {
    "./injector": "./src/injector.tsx",                               // 기존
    "./fragment-recommend-jobs": "./src/fragments/RecommendJobsContainer.tsx" // 추가
  },
  shared: { /* 기존 shared 설정 그대로 */ }
})

핵심 정리

  1. 별도 런타임 방식은 의존성을 완벽히 격리하여 불필요한 코드 유입을 방지하지만, 초기 설정 비용이 높고 코드 중복이 발생할 수 있다
  2. 기존 앱 확장 방식은 설정이 간단하고 기존 코드를 재사용할 수 있지만, 프래그먼트에 불필요한 의존성이 딸려갈 수 있다
  3. Container는 useAuth0Client()로 토큰을 획득하고 API를 호출하며, Presentational은 순수하게 props만 받아 렌더한다
  4. useShellNavigate()는 CustomEvent를 통해 Shell의 BrowserRouter를 제어하므로, 프래그먼트에서 다른 마이크로앱의 라우트로 이동할 수 있다
  5. 코드 중복이 발견되면 팀 내부 공유 패키지를 만들어 타입과 API 함수를 추출하는 것이 자연스러운 발전 방향이다

다음 단계

  • 03-모듈-페더레이션-프래그먼트-설정.md: 소비자(포스팅 앱) 측에서 webpack.config.js에 프래그먼트를 remote로 등록하고, React.lazy와 Suspense로 프래그먼트를 로딩하며, 에러 처리와 동적 리모트 로딩을 구현한다