SPA 통합

Single Page Application

각각의 SPA를 하나로 통합하는 방식에는 LinkedUnified 방식이 존재

  • 각각의 마이크로 앱이 SPA로 구성될 때, 이것들을 통합해 하나의 앱처럼 보이게 하기 위한 방법으로 2가지 방식이 있다.

Linked SPA

  • 다른 SPA 경계로 넘어갈 때 Hard Navgiation 이라고 해서 Client-side가 아닌 Server-side 라우팅이 변경되게끔 처리

    • Server-side 라우팅 처리란 서버에 요청하여 페이지를 받아오는 것을 의미한다.

  • 각각의 SPA 내부적으로는 Soft Navigatio Client-side 라우팅 처리

  • 구현하기 훨씬 쉽고 앱간 결합도(커플링)이 약하다.

Unified SPA

  • SPA를 넘나들 때 에도 Soft Navigation 을 사용하는 방식

  • App Shell 을 이용해서 껍데기로 감싸고 사용자의 라우팅 입력을 받아 어떤 SPA를 보여줄지 계산하고 렌더할지 결정한다.

  • 서로 다른 SPA 경계를 넘나들 때 마다 이전 SPA를 안보이게 하고 신규 SPA를 띄우는 등에 별도의 추가 작업들이 필요하기 떄문에 구현이 더 어렵다.

  • Soft Navigation 방식은 서버에 추가로 요청하는 통신이 없기 때문에 찐으로 하나의 SPA로 관리하는 것처럼 보여 사용자 경험이 더 좋을 수밖에 없다.

App Shell 설계

  • Module Federation은 하나의 앱에서 여러 코드들을 Code spliting을 한 다음 다른 서버로 옮기는 것을 의미

  • 라우트 코드들을 Code spliting을 통해 관리해주어야 한다.

// App Shell
import React, { lazy, Suspense } from 'react';
import { Navigate, ReactObject } from 'react-router-dom';

import { Layout } from '@/components/layout'
import { app1RoutingPrefix, app2RoutingPrefix } from '@/constants';

import App1Lazy = lazy(() => import("../components/App1"));
import App2Lazy = lazy(() => import("../components/App2"));

export const routes: ReactObject[] => [
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Navigate to={`/${app1RoutingPrefix}`} />,
      },
      {
        path: `${app1RoutingPrefix}/*`,
        element: <Suspense fallback="Loading App1..."><App1Lazy /></Suspense>,
      },
      {
        path: `${app2RoutingPrefix}/*`,
        element: <Suspense fallback="Loading App2..."><App2Lazy /></Suspense>,
      }
    ]
  }
]
// App1.tsx

useEffect(() => {
  if (!isFirstRunRef.current) return;
  // module federation을 통해 가져온 'mount'라는 함수를 이용
  unmountRef.current = mount({
    mountPoint: wrapperRef.current!,
    initialPathname: location.pathname.replace(app1Basename, ''),
  });
  isFirstRunRef.current = false;
}, [location])

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

return <div ref={wrapperRef} id="app1-mfe" />;
import React from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';

import { createRouter } from '@/routing/router-factory';
import { RoutingStrategy } from '@/routing/types';

interface Mount {
  mountPoint: HTMLElement;
  initialPathname?: string;
  routingStrategy?: RoutingStrategy;
}

export const mount = ({
  mountPoint,
  initialPathname,
  routingStrategy,
}: Mount) => {
// 마이크로 앱들은 또하나의 react-app 이기 때문에 react router가 독자적으로 구성됨
  const router = createRouter({ strategy: routingStrategy, initialPathname });
  const root = createRoot(mountPoint);
  root.render(<RouterProvider router={router} />);
  
  return () => queueMicrotask(() => root.unmount());
}
import React from 'react';
import { Outlet } from 'react-router-dom';

import { NavigationManager } from '@/components/navigation';
import { Page1, Page2 } from '@/pages';

export const routes = [
  {
    path: "/",
    element: (
      <NavigationManager>
        <Outlet>
      </NavigationManager>
    ),
    children: [
     {
       index: true,
       element: <Page1 />,
     },
     {
       path: 'page-1,
       element: <Page1 />,
     },
     {
       path: 'page-2,
       element: <Page2 />,
     }
    ]
  }
];

Last updated