이어서 할 곳

GitHub - fc-micro-frontends/career-up at step19

REACT_APP_AUTH0_DOMAIN=dev-vcrmf0xuep020tri.us.auth0.com
REACT_APP_AUTH0_CLIENT_ID=27LLb4I9cIjiiQNSjbNIX9sQM1KECUfk
REACT_APP_AUTH0_CALLBACK_URL=http://localhost:3000
➜ pnpm i

➜ pnpm build

shell-router 에서 타입 export

// career-up/packages/shell-router/src/types.ts

**import { type RouteObject } from "react-router-dom";
**import { injectFactory } from "./injector";**

export type RouterType = "browser" | "memory";

export interface CreateRouterProps {
  type: RouterType;
  routes: RouteObject[];
  basePath?: string;
}

**export type InjectFuncType = ReturnType<typeof injectFactory>;**
➜ pnpm --filter @career-up/shell-router build

shell remoteEntry 로딩 설정

➜ pnpm --filter @career-up/shell @module-federation/utilities
// career-up/packages/shell/src/router.tsx

import React, { Suspense } from "react";
import {
  createBrowserRouter,
  Navigate,
  RouterProvider,
} from "react-router-dom";
import Layout from "./components/layout";
import {
  appEduBasename,
  appJobBasename,
  appNetworkBasename,
  appPostingBasename,
} from "./constants/prefix";
import { Auth0ProviderWithNavigator } from "./components/auth0-provider-with-navigator";
**import AppEdu from "./components/app-edu";
import AppPosting from "./components/app-posting";
import AppNetwork from "./components/app-network";
import AppJob from "./components/app-job";**

const browserRouter = createBrowserRouter([
  {
    path: "/",
    element: (
      <Auth0ProviderWithNavigator>
        <Layout />
      </Auth0ProviderWithNavigator>
    ),
    children: [
      {
        index: true,
        element: <Navigate to={`${appPostingBasename}`} />,
      },
      {
        path: `${appPostingBasename}/*`,
        element: **<AppPosting />**,
      },
      {
        path: `${appEduBasename}/*`,
        element: **<AppEdu />**,
      },
      {
        path: `${appNetworkBasename}/*`,
        element: **<AppNetwork />**,
      },
      {
        path: `${appJobBasename}/*`,
        element: **<AppJob />**,
      },
    ],
  },
]);

export default function Router() {
  return <RouterProvider router={browserRouter} />;
}
// career-up/packages/shell/src/components/app-posting.tsx

import React, { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
import { appPostingBasename } from "../constants/prefix";
import { **type InjectFuncType**, useShellEvent } from "@career-up/shell-router";
**import { importRemote } from "@module-federation/utilities";**

export default function AppPosting() {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const location = useLocation();

  useShellEvent("app-posting", appPostingBasename);

  const isFirstRunRef = useRef(true);
  const unmountRef = useRef(() => {});

  useEffect(() => {
    if (!isFirstRunRef.current) {
      return;
    }
    isFirstRunRef.current = false;
    **importRemote<{ default: InjectFuncType }>({
      url: "<http://localhost:3001>",
      scope: "posting",
      module: "injector",
      remoteEntryFileName: `remoteEntry.js`,
    })
      .then(({ default: inject }) => {
        unmountRef.current = inject({
          routerType: "memory",
          rootElement: wrapperRef.current!,
          basePath: location.pathname.replace(appPostingBasename, ""),
        });
      })
      .catch((error) => {
        console.log(error);
      });**
  }, [location]);

  useEffect(() => unmountRef.current, []);

  return <div ref={wrapperRef} id="app-posting" />;
}
// career-up/packages/shell/src/components/app-edu.tsx

import React, { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
import { appEduBasename } from "../constants/prefix";
import { **type InjectFuncType**, useShellEvent } from "@career-up/shell-router";
**import { importRemote } from "@module-federation/utilities";**

export default function AppEdu() {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const location = useLocation();

  useShellEvent("app-edu", appEduBasename);

  const isFirstRunRef = useRef(true);
  const unmountRef = useRef(() => {});

  useEffect(() => {
    if (!isFirstRunRef.current) {
      return;
    }
    isFirstRunRef.current = false;
    **importRemote<{ default: InjectFuncType }>({
      url: "<http://localhost:3002>",
      scope: "edu",
      module: "injector",
      remoteEntryFileName: `remoteEntry.js`,
    })
      .then(({ default: inject }) => {
        unmountRef.current = inject({
          routerType: "memory",
          rootElement: wrapperRef.current!,
          basePath: location.pathname.replace(appEduBasename, ""),
        });
      })
      .catch((error) => {
        console.log(error);
      });**
  }, [location]);

  useEffect(() => unmountRef.current, []);

  return <div ref={wrapperRef} id="app-edu" />;
}
// career-up/packages/shell/src/components/app-network.tsx

import React, { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
import { appNetworkBasename } from "../constants/prefix";
import { **type InjectFuncType**, useShellEvent } from "@career-up/shell-router";
**import { importRemote } from "@module-federation/utilities";**

export default function AppNetwork() {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const location = useLocation();

  useShellEvent("app-network", appNetworkBasename);

  const isFirstRunRef = useRef(true);
  const unmountRef = useRef(() => {});

  useEffect(() => {
    if (!isFirstRunRef.current) {
      return;
    }
    isFirstRunRef.current = false;
    **importRemote<{ default: InjectFuncType }>({
      url: "<http://localhost:3003>",
      scope: "network",
      module: "injector",
      remoteEntryFileName: `remoteEntry.js`,
    })
      .then(({ default: inject }) => {
        unmountRef.current = inject({
          routerType: "memory",
          rootElement: wrapperRef.current!,
          basePath: location.pathname.replace(appNetworkBasename, ""),
        });
      })
      .catch((error) => {
        console.log(error);
      });**
  }, [location]);

  useEffect(() => unmountRef.current, []);

  return <div ref={wrapperRef} id="app-network" />;
}
// career-up/packages/shell/src/components/app-job.tsx

import React, { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
import { appJobBasename } from "../constants/prefix";
import { **type InjectFuncType**, useShellEvent } from "@career-up/shell-router";
**import { importRemote } from "@module-federation/utilities";**

export default function AppJob() {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const location = useLocation();

  useShellEvent("app-job", appJobBasename);

  const isFirstRunRef = useRef(true);
  const unmountRef = useRef(() => {});

  useEffect(() => {
    if (!isFirstRunRef.current) {
      return;
    }
    isFirstRunRef.current = false;
    **importRemote<{ default: InjectFuncType }>({
      url: "<http://localhost:3004>",
      scope: "job",
      module: "injector",
      remoteEntryFileName: `remoteEntry.js`,
    })
      .then(({ default: inject }) => {
        unmountRef.current = inject({
          routerType: "memory",
          rootElement: wrapperRef.current!,
          basePath: location.pathname.replace(appJobBasename, ""),
        });
      })
      .catch((error) => {
        consol**e.log(error);
      });
  }, [location]);

  useEffect(() => unmountRef.current, []);

  return <div ref={wrapperRef} id="app-job" />;
}
{
  "name": "@career-up/shell",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "build:start": "cd dist && PORT=3000 npx serve **-s**",
    "start": "webpack serve --open --mode development",
    "start:live": "webpack serve --open --mode development --live-reload --hot"
  },
  "license": "MIT",
  "author": {
    "name": "Jack Herrington",
    "email": "[email protected]"
  },
  "devDependencies": {
    "@babel/core": "^7.15.8",
    "@babel/plugin-transform-runtime": "^7.15.8",
    "@babel/preset-env": "^7.15.8",
    "@babel/preset-react": "^7.14.5",
    "@babel/preset-typescript": "^7.10.4",
    "@types/node": "^20.10.5",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "autoprefixer": "^10.1.0",
    "babel-loader": "^8.2.2",
    "css-loader": "^6.3.0",
    "dotenv-webpack": "^8.0.1",
    "html-webpack-plugin": "^5.3.2",
    "postcss": "^8.2.1",
    "postcss-loader": "^4.1.0",
    "style-loader": "^3.3.0",
    "typescript": "5.2.2",
    "webpack": "^5.57.1",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.3.1"
  },
  "dependencies": {
    "@auth0/auth0-react": "^2.2.4",
    "@babel/runtime": "^7.23.6",
    "@career-up/shell-router": "workspace:*",
    "@career-up/ui-kit": "workspace:*",
    "@module-federation/utilities": "^3.0.5",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.21.1"
  }
}
➜ pnpm --filter @career-up/server start:live

➜ pnpm --filter @career-up/shell build

➜ pnpm --filter @career-up/shell build:start

posting 에 error 처리 및 리로드 추가

➜ pnpm --filter @career-up/posting @module-federation/utilities react-error-boundary
import "./page-home.scss";

import React, { Suspense, useEffect, useState } from "react";
import Profile from "../components/profile";
import { PostType } from "../types";
import { createPost, getPosts, removePost } from "../apis";
import Post from "../components/post";
import WritePost from "../components/write-post";
import { useAuth0Client } from "@career-up/shell-router";
**import { ErrorBoundary } from "react-error-boundary";
import { importRemote } from "@module-federation/utilities";**

const PageHome: React.FC = () => {
  const auth0Client = useAuth0Client();
  const [posts, setPosts] = useState<PostType[]>([]);

  **const RecommendConnectionsContainer = React.lazy(() =>
    importRemote({
      url: "<http://localhost:5001>",
      scope: "fragment_recommend_connections",
      module: "container",
      remoteEntryFileName: `remoteEntry.js?v=${Date.now()}`,
    })
  );

  const RecommendJobsContainer = React.lazy(() =>
    importRemote({
      url: "<http://localhost:3004>",
      scope: "job",
      module: "fragment-recommend-jobs",
      remoteEntryFileName: `remoteEntry.js?v=${Date.now()}`,
    })
  );**

  useEffect(() => {
    (async () => {
      try {
        const token = await auth0Client.getTokenSilently();
        const posts = await getPosts(token);
        setPosts(posts);
      } catch (error) {
        alert(error);
      }
    })();
  }, [auth0Client]);

  const deletePostById = async (id: number) => {
    try {
      const token = await auth0Client.getTokenSilently();

      await removePost(token, id);

      const posts = await getPosts(token);
      setPosts(posts);
    } catch (error) {
      alert(error);
    }
  };

  const writePost = async (message: string) => {
    try {
      const token = await auth0Client.getTokenSilently();

      await createPost(token, { message });

      const posts = await getPosts(token);
      setPosts(posts);
    } catch (error) {
      alert(error);
    }
  };

  return (
    <div className="posting--page-home">
      <div className="posting--page-home-left">
        <Profile />
      </div>
      <div className="posting--page-home-center">
        <WritePost writePost={writePost} />
        {posts.map((post) => (
          <Post key={post.id} {...post} deletePostById={deletePostById} />
        ))}
      </div>
      <div className="posting--page-home-right">
        **<ErrorBoundary fallback={<div>Error</div>}>
          <Suspense fallback={<div>Loading...</div>}>
            <RecommendConnectionsContainer />
          </Suspense>
        </ErrorBoundary>
        <ErrorBoundary fallback={<div>Error</div>}>
          <Suspense fallback={<div>Loading...</div>}>
            <RecommendJobsContainer />
          </Suspense>
        </ErrorBoundary>**
      </div>
    </div>
  );
};

export default PageHome;
➜ pnpm --filter @career-up/posting build

➜ pnpm --filter @career-up/posting build:start

➜ pnpm --filter @career-up/network build

➜ pnpm --filter @career-up/network build:start

➜ pnpm --filter @career-up/job build

➜ pnpm --filter @career-up/job build:start