본문 바로가기
WIKI 기술 지식 베이스

프론트엔드 경로

원문 보기 위키 갱신

프론트엔드 경로 (Frontend Routes)

각 Backstage 플러그인은 일반적으로 다른 플러그인과 직접 통신하지 않는 격리된 기능 조각이에요. 이를 달성하기 위해 프론트엔드 시스템에는 크로스 플러그인 통신을 위한 간접(indirection) 계층을 제공하는 많은 부분이 있으며, 라우팅 시스템도 그중 하나예요.

출처: 문서

본문

소개 (Introduction)

각 Backstage 플러그인은 일반적으로 다른 플러그인과 직접 통신하지 않는 격리된 기능 조각이에요. 이를 달성하기 위해 프론트엔드 시스템에는 크로스 플러그인 통신을 위한 간접 계층을 제공하는 많은 부분이 있으며, 라우팅 시스템도 그중 하나예요.

Backstage 라우팅 시스템은 각 개별 플러그인이 라우팅 계층에서 다른 플러그인의 구체적인 경로나 위치, 심지어 자신의 경로조차 알 필요 없이 플러그인 경계를 넘어 탐색을 구현할 수 있게 해줘요. 이는 라우트 참조(route references)라는 개념을 통해 달성돼요. 라우트 참조는 앱의 여러 부분에 대한 구체적인 링크를 만드는 데 공유되고 사용될 수 있는 불투명한 참조 값이에요. 라우트 참조 경로는 플러그인 수준(플러그인 개발자가)과 앱 수준(통합자가) 모두에서 구성할 수 있어요. 앱에서 연결하거나 연결받을 수 있게 하려는 플러그인의 모든 페이지 콘텐츠에 대해 라우트 참조를 만드는 것은 플러그인 개발자의 몫이에요.

라우트 참조 (Route References)

플러그인 개발자는 RouteRef를 만들어 Backstage 라우팅 시스템에 경로를 노출해요. 아래에서 경로가 프로그래밍 방식으로 어떻게 정의되는지 볼 수 있겠지만, 코드를 살펴보기 전에 앱 수준에서 구성하는 방법을 설명할게요. 플러그인 개발자가 자신의 플러그인이 제공하는 경로에 대해 기본 라우트 경로를 선택하지만, 경로는 구성 가능해요. 따라서 앱 통합자는 원할 때마다 경로에 커스텀 경로를 설정할 수 있어요(자세한 내용은 다음 섹션에서).

라우트 참조에는 일반 라우트, 하위 라우트, 외부 라우트의 세 가지 유형이 있으며, 각각의 개념과 코드 정의를 모두 다룰 거예요.

라우트 참조 만들기 (Creating a Route Reference)

라우트 참조는 "절대(absolute)" 또는 "일반(regular)" 라우트라고도 하며, 다음과 같이 만들어요.

plugins/catalog/src/routes.ts

import { createRouteRef } from '@backstage/frontend-plugin-api';// Creates a route reference, which is not yet associated with any plugin pageexport const indexRouteRef = createRouteRef();

라우트 참조 자체는 플러그인 인스턴스를 만드는 파일과 다른 파일(예: 최상위 routes.ts)에 만드는 것이 좋다는 점에 유의하세요. 이는 같은 플러그인의 다른 부분에서 라우트 참조를 사용할 때 순환 import를 피하기 위함이에요.

라우트 참조는 자체적으로 어떤 동작도 가지지 않아요. 그것은 앱의 라우트 대상을 나타내는 불투명한 값이며, 런타임에 특정 경로에 바인딩돼요. 그 역할은 서로 라우팅 방법을 알지 못하는 다른 페이지를 연결하는 데 도움을 주는 간접 계층을 제공하는 것이에요.

플러그인에 라우트 참조 제공하기

이전 섹션의 코드 스니펫은 그 경로가 어떤 플러그인에 속하는지 나타내지 않아요. 그러려면 페이지 확장 같은 모든 종류의 라우팅 가능한 확장을 만들 때 사용해야 해요.

plugins/catalog/src/plugin.tsx

import {  createFrontendPlugin,  createPageExtension,} from '@backstage/frontend-plugin-api';import { indexRouteRef } from './routes';const catalogIndexPage = createPageExtension({  // The `name` option is omitted because this is an index page  path: '/entities',  routeRef: indexRouteRef,  loader: () => import('./components').then(m => <m.IndexPage />),});export default createFrontendPlugin({  pluginId: 'catalog',  routes: {    index: indexRouteRef,  },  extensions: [catalogIndexPage],});

위 예시에서 indexRouteRef를 catalogIndexPage 확장에 연결하고 Catalog 플러그인을 통해 라우트 참조와 페이지를 모두 제공했어요. 따라서 이 플러그인이 앱에 설치되면 인덱스 페이지가 새로 만든 RouteRef와 연결되고, 그 라우트 참조를 사용해 페이지 확장을 탐색할 수 있게 돼요.

경로가 이미 확장에 전달되었는데 왜 플러그인을 만들 때 routes 옵션을 구성하는지 명확하지 않을 수 있어요. 우리는 다른 플러그인이 우리 페이지로 라우팅할 수 있게 하기 위해 그렇게 하는데, 이는 라우트 바인딩 섹션에서 자세히 설명돼요.

경로 매개변수가 있는 참조 정의하기

라우트 참조는 선택적으로 params 옵션을 받으며, 나열된 매개변수 이름이 라우트 경로에 있어야 해요. /entities/:kind/:namespace/:name 같은 경로처럼 kind, namespace, name 매개변수가 필요한 라우트 참조를 만드는 방법은 다음과 같아요.

plugins/catalog/src/routes.ts

import { createRouteRef } from '@backstage/frontend-plugin-api';export const detailsRouteRef = createRouteRef({  // The parameters that must be included in the path of this route reference  params: ['kind', 'namespace', 'name'],});

라우트 참조 사용하기

라우트 참조는 같은 플러그인의 페이지나 다른 플러그인의 페이지에 연결하는 데 사용할 수 있어요. 이 섹션에서는 첫 번째 시나리오를 다룰 거예요. 다른 플러그인의 페이지에 연결하는 데 관심이 있다면 아래 외부 라우트 섹션으로 가세요.

"Foo" 컴포넌트 상세 페이지에 대한 링크가 있는 Catalog 인덱스 페이지를 렌더링하는 플러그인을 만든다고 가정해 보겠어요. 인덱스 페이지의 코드는 다음과 같아요.

plugins/catalog/src/components/IndexPage.tsx

import { useRouteRef } from '@backstage/frontend-plugin-api';import { detailsRouteRef } from '../routes';export const IndexPage = () => {  const getDetailsPath = useRouteRef(detailsRouteRef);  return (    <div>      <h1>Index Page</h1>      {getDetailsPath && (        <a          href={getDetailsPath({            kind: 'component',            namespace: 'default',            name: 'foo',          })}        >          See "Foo" details        </a>      )}    </div>  );};

useRouteRef 훅을 사용해 상세 페이지 경로를 반환하는 링크 생성기 함수를 만들어요. 먼저 라우트를 사용할 수 있는지 확인해야 해요. 사용할 수 없으면 링크 생성기 함수는 undefined가 돼요. 그런 다음 링크 생성기를 호출하며 kind, namespace, name 객체를 전달해요. 이 매개변수들은 "Foo" 상세 페이지에 대한 구체적인 경로를 구성하는 데 사용돼요.

상세 페이지가 URL에서 매개변수를 얻는 방법을 살펴볼까요.

plugins/catalog/src/components/DetailsPage.tsx

import { useRouteRefParams } from '@backstage/frontend-plugin-api';import { detailsRouteRef } from '../routes';export const DetailsPage = () => {  const params = useRouteRefParams(detailsRouteRef);  return (    <div>      <h1>Details Page</h1>      <ul>        <li>Kind: {params.kind}</li>        <li>Namespace: {params.namespace}</li>        <li>Name: {params.name}</li>      </ul>    </div>  );};

위 코드에서는 useRouteRefParams 훅을 사용해 URL에서 엔터티 구성 ID를 가져와요. 매개변수 객체에는 kind, namespace, name의 세 가지 값이 있어요. 이 값들을 표시하거나 이를 사용해 API를 호출할 수 있어요.

같은 패키지의 페이지로 연결하므로 라우트 참조를 직접 사용해요. 하지만 다음 섹션에서는 다른 플러그인의 페이지로 연결하는 방법을 볼 거예요.

외부 라우트 참조 (External Route References)

외부 라우트는 외부 플러그인의 페이지에 연결하기 위해 만들어져요. 이 섹션의 예시에서는 Catalog 엔터티 목록 페이지에서 Scaffolder 컴포넌트 생성 페이지로 연결하고 싶다고 가정해 보겠어요.

Scaffolder 플러그인을 직접 참조하고 싶지는 않아요. 불필요한 의존성이 생기기 때문이에요. 또한 앱이 플러그인을 서로 묶을 수 있는 유연성이 거의 없고, 대신 링크가 플러그인 자체에 의해 결정되게 돼요. 이를 해결하기 위해 ExternalRouteRef를 사용해요. 일반 라우트 참조와 매우 유사하게 useRouteRef에 전달해 구체적인 URL을 만들 수 있지만, 페이지 확장에서는 사용할 수 없고 대신 앱의 라우트 바인딩을 사용해 대상 라우트와 연결되어야 해요.

Scaffolder 플러그인 내부에 새 RouteRef를 만들고, 연결할 수 있는 특정 플러그인 페이지가 아니라 플러그인에서의 역할을 설명하는 중립적인 이름을 사용해, 앱이 최종 대상을 결정하도록 해요. 예를 들어 Catalog 엔터티 목록 페이지가 헤더에서 Scaffolder 컴포넌트 생성 페이지로 연결하려고 한다면, 다음과 유사한 ExternalRouteRef를 선언할 수 있어요.

plugins/catalog/src/routes.ts

import { createExternalRouteRef } from '@backstage/frontend-plugin-api';export const createComponentExternalRouteRef = createExternalRouteRef();

외부 라우트도 일반 라우트와 유사한 방식으로 사용돼요.

plugins/catalog/src/components/IndexPage.tsx

import { useRouteRef } from '@backstage/frontend-plugin-api';import { createComponentExternalRouteRef } from '../routes';export const IndexPage = () => {  const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef);  return (    <div>      <h1>Index Page</h1>      {getCreateComponentPath && (        <a href={getCreateComponentPath()}>Create Component</a>      )}    </div>  );};

위 바인딩이 주어지면 Catalog 플러그인 내에서 useRouteRef(createComponentExternalRouteRef)를 사용해 Scaffolder 컴포넌트 생성 페이지가 마운트된 경로가 무엇이든 그곳으로 가는 링크를 만들 수 있어요. Catalog 플러그인과 Scaffolder 사이에 직접 의존성이 없다는 점에 유의하세요. 즉 Scaffolder 패키지에서 createComponentExternalRouteRef를 import하지 않아요.

이제 남은 것은 페이지와 외부 라우트를 플러그인을 통해 제공하는 것뿐이에요.

plugins/catalog/src/plugin.tsx

import {  createFrontendPlugin,  createPageExtension,  useRouteRef,} from '@backstage/frontend-plugin-api';import { indexRouteRef, createComponentExternalRouteRef } from './routes';const catalogIndexPage = createPageExtension({  path: '/entities',  routeRef: indexRouteRef,  loader: () => import('./components').then(m => <m.IndexPage />),});export default createFrontendPlugin({  pluginId: 'catalog',  routes: {    index: indexRouteRef,  },  externalRoutes: {    createComponent: createComponentExternalRouteRef,  },  extensions: [catalogIndexPage],});

외부 라우트도 매개변수를 가질 수 있어요. 예를 들어 Scaffolder에서 엔터티의 상세 페이지로 연결하려면 Catalog 상세 페이지가 기대하는 것과 같은 매개변수를 받는 외부 라우트를 만들어야 해요.

plugins/scaffolder/src/routes.ts

import { createExternalRouteRef } from '@backstage/frontend-plugin-api';export const entityDetailsExternalRouteRef = createExternalRouteRef({  params: ['kind', 'namespace', 'name'],});

이제 앱이 이 외부 라우트를 해석하도록 구성해, Scaffolder가 Catalog 엔터티 페이지로 연결하고 Catalog가 Scaffolder 페이지로 연결하게 해보겠어요.

외부 라우트 참조 바인딩하기

외부 라우트의 연결은 앱이 제어해요. 각 플러그인의 ExternalRouteRef는 보통 다른 플러그인의 실제 RouteRef에 바인딩되어야 해요. 바인딩 과정은 앱 시작 시 한 번 발생하며, 이후 앱 수명 동안 구체적인 라우트 경로를 해석하는 데 사용돼요.

위의 Catalog 엔터티 목록 페이지에서 Scaffolder 컴포넌트 생성 페이지로 연결하는 예시를 사용해, 앱 구성 파일에서 다음과 같이 할 수 있어요.

app-config.yaml

app:  routes:    bindings:      # point to the Scaffolder create component page when the Catalog create component ref is used      catalog.createComponent: scaffolder.index      # point to the Catalog details page when the Scaffolder component details ref is used      scaffolder.componentDetails: catalog.details      # explicitly disable the default route binding from the scaffolder to the catalog import page      scaffolder.registerComponent: false

이를 createApp의 옵션으로 코드로 표현할 수도 있지만, 물론 이 두 방법 중 하나만 사용하면 돼요.

packages/app/src/App.tsx

import { createApp } from '@backstage/frontend-defaults';import catalog from '@backstage/plugin-catalog';import scaffolder from '@backstage/plugin-scaffolder';const app = createApp({  bindRoutes({ bind }) {    bind(catalog.externalRoutes, {      createComponent: scaffolder.routes.createComponent,    });    bind(scaffolder.externalRoutes, {      componentDetails: catalog.routes.details,      registerComponent: false,    });  },});export default app.createRoot();

앱에서 RouteRef를 직접 import하고 사용하지 않고, 플러그인 인스턴스에 의존해 플러그인의 경로에 접근한다는 점에 유의하세요. 이는 경로의 더 나은 네임스페이싱과 발견 가능성을 제공하고, 각 플러그인 패키지의 별도 export 수를 줄여줘요.

또 하나 주의할 점은 라우팅의 이러한 간접성은 통합 방식에 유연성을 제공해야 하는 오픈소스 플러그인에 특히 유용하다는 것이에요. 자신의 Backstage 애플리케이션을 위해 내부적으로 구축하는 플러그인에는 직접 import나 구체적인 라우트 경로 문자열을 선택해 사용할 수 있어요. 내부 플러그인에서도 전체 라우팅 시스템을 사용하는 데 이점이 있을 수 있어요. 라우트를 구조화하는 데 도움이 되고, 아래에서 더 살펴보겠지만 라우트 매개변수를 관리하는 데도 도움이 되기 때문이에요.

외부 라우트 참조의 기본 대상 (Default Targets)

외부 라우트 참조에 대한 기본 대상을 정의할 수 있어서, 앱에서 라우트를 바인딩할 필요가 없어질 수도 있어요. 이는 합리적인 기본값을 제공해 새 플러그인을 설치할 때 구성의 필요성을 줄여줘요. 물론 외부 라우트 참조가 플러그인 인스턴스의 externalRoutes 속성을 통해 export되기만 하면 앱에서 라우트 바인딩을 재정의하는 것도 여전히 가능해요.

기본 대상은 라우트 바인딩 구성과 같은 구문을 사용하며, 대상 플러그인과 라우트가 존재할 때만 사용돼요. 예를 들어 이것이 catalog가 이전 예시의 바인딩 필요성을 제거하는 방식으로 create component 외부 라우트에 대한 기본 대상을 정의하는 방법이에요.

plugins/catalog/src/routes.ts

import { createExternalRouteRef } from '@backstage/frontend-plugin-api';export const createComponentExternalRouteRef = createExternalRouteRef({  defaultTarget: 'scaffolder.createComponent',});

하위 라우트 참조 (Sub Route References)

만들 수 있는 마지막 종류의 라우트 참조는 SubRouteRef예요. 이는 절대 RouteRef나 다른 SubRouteRef에 상대적인 고정 경로로 라우트 참조를 만드는 데 사용할 수 있어요. 페이지가 내부적으로 페이지 확장 컴포넌트의 하위 라우트에 마운트되어 있고, 다른 플러그인이 그 페이지로 라우팅할 수 있기를 원할 때 유용해요. 또한 플러그인 내부의 라우팅을 처리하는 데 유용한 유틸리티가 될 수 있어요.

예를 들어:

import {  createRouteRef,  createSubRouteRef,} from '@backstage/frontend-plugin-api';export const indexRouteRef = createRouteRef();export const detailsSubRouteRef = createSubRouteRef({  parent: indexRouteRef,  path: '/details',});

하위 라우트 만들기와 일반 또는 외부 라우트 만들기 사이에는 상당한 차이가 있어요. 하위 라우트는 일반 라우트와 연결되고 하위 라우트 경로를 지정해야 하기 때문이에요. 경로 문자열은 하위 라우트에 매개변수가 있다면 포함해야 해요.

// Omitting rest of the previous example fileexport const detailsSubRouteRef = createSubRouteRef({  parent: indexRouteRef,  path: '/:name/:namespace/:kind',});

SubRouteRef는 다른 SubRouteRef의 부모가 될 수도 있어요. 이렇게 하면 라우트 참조가 중첩된 앱 경로와 같은 계층 구조를 따를 수 있어요.

plugins/catalog/src/routes.ts

export const revisionSubRouteRef = createSubRouteRef({  parent: indexRouteRef,  path: '/:name/:revision',});export const revisionAttachmentsSubRouteRef = createSubRouteRef({  parent: revisionSubRouteRef,  path: '/attachments',});

createSubRouteRef에 전달된 각 path는 직속 부모에 상대적이에요. 중첩 경로는 조상에서 모든 매개변수를 상속하므로 useRouteRef(revisionAttachmentsSubRouteRef)는 name과 revision을 모두 요구해요. 매개변수 이름은 전체 부모 체인에 걸쳐 고유해야 해요.

페이지 확장에서 하위 라우트를 사용하는 것은 다음과 같이 간단해요.

plugins/catalog/src/components/IndexPage.tsx

import { Routes, Route, useLocation } from 'react-router-dom';import { useRouteRef } from '@backstage/frontend-plugin-api';import { indexRouteRef, detailsSubRouteRef } from '../routes';import { DetailsPage } from './DetailsPage';export const IndexPage = () => {  const { pathname } = useLocation();  const getIndexPath = useRouteRef(indexRouteRef);  const getDetailsPath = useRouteRef(detailsSubRouteRef);  return (    <div>      <h1>Index Page</h1>      {/* Linking to the details sub route */}      {pathname === getIndexPath?.() ? (        <a          {/* Setting the details sub route params */}          href={getDetailsPath?.({            kind: 'component',            namespace: 'default',            name: 'foo',          })}        >          Show details        </a>      ) : (        <a href={getIndexPath?.()}>Hide details</a>      )}      {/* Registering the details sub route */}      <Routes>        <Route path={detailsSubRouteRef.path} element={<DetailsPage />} />      </Routes>    </div>  );};

하위 라우트 URL의 매개변수를 얻는 방법은 다음과 같아요.

plugins/catalog/src/components/DetailsPage.tsx

import { useParams } from 'react-router-dom';export const DetailsPage = () => {  const params = useParams();  return (    <div>      <h1>Details Sub Page</h1>      <ul>        <li>Kind: {params.kind}</li>        <li>Namespace: {params.namespace}</li>        <li>Name: {params.name}</li>      </ul>    </div>  );};

마지막으로 플러그인이 하위 라우트를 제공하는 방법을 살펴볼게요.

plugins/catalog/src/plugin.tsx

import {  createFrontendPlugin,  createPageExtension,} from '@backstage/frontend-plugin-api';import { indexRouteRef, detailsSubRouteRef } from './routes';const catalogIndexPage = createPageExtension({  path: '/entities',  routeRef: indexRouteRef,  loader: () => import('./components').then(m => <m.IndexPage />),});export default createFrontendPlugin({  pluginId: 'catalog',  routes: {    index: indexRouteRef,    details: detailsSubRouteRef,  },  extensions: [catalogIndexPage],});

라우트 별칭 - 모듈에서 라우팅된 확장 재정의하기

모듈을 사용해 플러그인의 확장을 재정의하는 것이 가능해요. 경우에 따라 재정의하려는 확장이 라우트 참조를 요구할 수 있어요. 플러그인 인스턴스를 import하고 routes 속성을 통해 접근할 수 있지만, 이는 플러그인에 대한 직접 의존성을 만들고 라우트 참조도 깨뜨릴 수 있는 패키지 중복 문제로 이어질 위험이 있어요.

라우트 참조에 직접 접근하는 대신, 플러그인의 원본 참조에 대한 별칭 역할을 하는 새 라우트 참조를 만들 수 있어요. 예를 들어 Catalog 인덱스 페이지를 다음과 같이 커스텀 것으로 재정의할 수 있어요.

const indexRouteRef = createRouteRef({ aliasFor: 'catalog.catalogIndex' });export default createFrontendModule({  pluginId: 'catalog',  extensions: [    PageBlueprint.make({      params: {        defaultPath: '/catalog',        routeRef: indexRouteRef,        loader: () =>          import('./CustomCatalogIndexPage').then(m => (            <m.CustomCatalogIndexPage />          )),      },    }),  ],});

별칭은 그것이 정의된 플러그인으로 제한돼요. 이 별칭은 예를 들어 useRouteRef로 평소처럼 import하고 사용할 수도 있지만, 작동하려면 항상 앱에서 확장을 통해 등록되어야 해요. 예를 들어 다음은 작동하지 않아요.

function MyInvalidComponent() {  // This is NOT valid  const link = useRouteRef(    createRouteRef({ aliasFor: 'catalog.catalogIndex' }),  );  // ...}

더 알아보기 (Learn more)