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

앱 인스턴스

원문 보기 위키 갱신

앱 인스턴스 (App Instances)

앱 인스턴스는 프론트엔드 앱을 만드는 주요 진입점이에요. 자체적으로 많은 일을 하지는 않지만, 시스템의 다른 부분에서 기능(features)으로 제공된 것들을 서로 연결(wiring)하는 책임을 져요.

출처: 문서

본문

앱 인스턴스 (The App Instance)

앱 인스턴스는 프론트엔드 앱을 만드는 주요 진입점이에요. 자체적으로 많은 일을 하지는 않지만, 시스템의 다른 부분에서 기능으로 제공된 것들을 서로 연결하는 책임을 져요.

아래는 앱 인스턴스를 만들고 렌더링하는 간단한 예시예요.

import ReactDOM from 'react-dom/client';import { createApp } from '@backstage/frontend-defaults';// Create your app instanceconst app = createApp({  // Features such as plugins can be installed explicitly, but we will explore other options later on  features: [catalogPlugin],});// This creates a React element that renders the entire appconst root = app.createRoot();// Just like any other React we need a root element. No server side rendering is used.const rootEl = document.getElementById('root')!;ReactDOM.createRoot(rootEl).render(root);

createApp을 호출해 새 앱 인스턴스를 만드는데, 이는 앱에 제공하는 모든 기능을 서로 연결하는 책임을 져요. 또한 앱의 기반을 구축하는 데 도움을 주는 내장 확장(Extensions) 세트와 함께 Utility API 구현, 컴포넌트, 아이콘, 테마, 구성 로드 방법 같은 다른 많은 시스템의 기본값도 제공해요. 앱을 만드는 시점에는 실제 작업이 수행되지 않아요. 모든 작업은 app.createRoot()가 반환한 요소를 렌더링할 때로 지연돼요.

앱을 만들 때 기능을 명시적으로 설치할 수도 있어요. 하지만 일반적으로 이들은 나중에 살펴볼 자동 검색(discovery)으로 발견될 거예요. 그럼에도 이 기능들이 확장을 제공해 앱의 실제 기능을 구축해요. 이 확장들은 앱에 의해 앱 확장 트리(app extension tree)라고 하는 트리 구조로 연결돼요. 이 트리의 각 노드는 자식 노드에서 데이터를 받고 부모에게 데이터를 전달해요. 아래 다이어그램은 작은 앱 확장 트리의 형태를 보여줘요.

이 트리의 각 노드는 부모 노드와 자식을 가진 확장이에요. 색칠된 모양은 확장 데이터 입력과 출력을 나타내며, 각 색은 고유한 데이터 유형이에요. 부모가 무시하는 데이터를 출력하는 확장도 있고, 입력을 받지만 자식이 없는 확장도 있다는 것을 알 수 있어요. 확장을 만들 때 입력과 출력에 대한 다양한 요구 사항을 정의할 수 있게 해주는 몇 가지 도구가 있는데, 이에 대해서는 확장 섹션에서 더 자세히 다룰 거예요.

확장 간에 공유되는 흔한 데이터 유형은 React 요소와 컴포넌트예요. 이들은 각자의 React 컴포넌트에서 서로 렌더링될 수 있으며, 결과적으로 앱 확장 트리와 형태가 유사한 React 컴포넌트의 평행 트리를 형성해요. 앱 확장 트리의 맨 위에는 그 중에서도 React 요소를 출력하는 내장 루트 확장이 있어요. 이 요소는 평행 React 트리의 루트가 되며, app.createRoot()가 반환한 React 요소에 의해 렌더링돼요.

기능 검색 (Feature Discovery)

앱 기능 검색을 사용하면 앱의 의존성이 제공하는 기능을 자동으로 발견하고 설치할 수 있어요. 실제로는 코드에서 기능을 수동으로 import할 필요가 없고, package.json에 의존성으로 추가하는 즉시 설치된다는 뜻이에요.

기능 검색은 컴파일 과정과 상호작용해야 하므로 @backstage/cli로 앱을 빌드할 때만 사용할 수 있어요. 앱 패키지에서 호환 가능한 의존성을 스캔해 WebPack 컴파일 과정에 연결하며, 이 의존성들은 앱 컴파일 번들에 포함돼요.

기능 검색 구성과 다른 설치 옵션에 대한 정보는 플러그인 설치 문서를 참고하세요.

단계적으로 앱 준비하기 (Preparing an App in Phases)

대부분의 앱은 모든 앱 준비를 내부적으로 처리하는 @backstage/frontend-defaults의 createApp을 사용해야 해요. 더 고급 사용 사례에는 @backstage/frontend-app-api에 더 저수준의 prepareSpecializedApp API도 있어요.

이 API는 전체 앱이 확정(finalized)되기 전에 부트스트랩 트리를 렌더링해야 할 때 유용해요. 예를 들어 로그인이나 다른 세션 종속 상태를 기다리는 동안 말이죠. 부트스트랩 앱 트리에 즉시 접근할 수 있게 해주고, onFinalized()로 확정을 구독하거나 finalize()로 동기적으로 확정할 수 있게 해주며, 준비된 세션을 이후 앱 인스턴스에서 재사용할 수 있게 해줘요.

import {  FinalizedSpecializedApp,  prepareSpecializedApp,} from '@backstage/frontend-app-api';const preparedApp = prepareSpecializedApp({  config,  features: [appPlugin, ...features],});const bootstrapApp = preparedApp.getBootstrapApp();const unsubscribe = preparedApp.onFinalized(  (finalizedApp: FinalizedSpecializedApp) => {    console.log(finalizedApp.sessionState);  },);

getBootstrapApp() 메서드는 부트스트랩 동안 사용할 수 있는 부분 앱 트리를 노출해요. onFinalized()를 호출하면 부트스트랩 소유의 확정 흐름을 구독하게 돼요. 로그인 경우에 로그인 페이지는 onSignInSuccess 콜백을 받고, 그 콜백을 통해 신원(identity)을 제공하면 전체 앱이 확정되고 onFinalized() 구독자들에게 알림이 가요.

대신 finalize()를 호출하면 확정을 직접 소유하게 돼요. 이는 앱을 동기적으로 확정할 수 있을 때만 작동해요. 예를 들어 모든 predicate 컨텍스트가 이미 사용 가능하거나, 처음부터 재사용 가능한 세션 상태를 prepareSpecializedApp()에 전달한 경우예요.

const preparedApp = prepareSpecializedApp({  config,  features: [appPlugin, ...features],  advanced: {    sessionState,  },});const app = preparedApp.finalize();

단계별 앱 준비를 사용할 때 app/root.children이 주요 세션 경계 역할을 해요. 그 경계 뒤의 조건부 확장은 확정 중에 평가돼요. 조건부 app/root.elements와 API 분기도 확정까지 지연되는 반면, 다른 부트스트랩에서 보이는 predicate는 무시되고 경고로 보고돼요.

부트스트랩 중에 처음으로 구체화(materialized)된 Utility API는 해당 앱 인스턴스의 수명 동안 고정(frozen)돼요. 확정은 여전히 새 API를 추가할 수 있고 부트스트랩 중에 구체화되지 않은 기존 API refs를 재정의할 수 있지만, 이미 구체화된 부트스트랩 API의 지연된 재정의는 무시되고 앱 오류로 보고돼요.

플러그인 정보 해석 (Plugin Info Resolution)

앱에 플러그인이 설치되면 최종 사용자와 관리자에게 유용할 수 있는 플러그인 정보 소스를 제공할 수 있어요. 여기에는 어떤 버전의 플러그인이 실행 중인지, 어떤 팀이 플러그인을 소유하는지, 지원을 위해 누구에게 연락해야 하는지 같은 것이 포함돼요. 플러그인이 이 정보를 제공하는 방법에 대해선 플러그인의 info 옵션 섹션에서 더 읽어 보세요.

기본적으로 앱은 package.json 파일에서 몇 가지 공통 필드를 선택하고, 불투명한 매니페스트가 일부 정보를 수집할 수 있는 catalog-info.yaml 파일이라고 가정해요. 이 정보는 플러그인 인스턴스의 info() 메서드를 통해 사용할 수 있으며, FrontendPluginInfo 유형의 구조를 반환해요.

플러그인 정보 확장하기 (Extending Plugin Info)

기본 플러그인 정보는 이를 바탕으로 구축하기 위한 기반으로 의도됐어요. 앱을 설정하는 과정의 일부로 플러그인 정보가 해석되는 방식을 커스터마이즈할 수 있을 뿐만 아니라, FrontendPluginInfo 유형을 더 많은 정보를 포함하도록 확장할 수도 있어요.

FrontendPluginInfo 유형을 확장하려면 TypeScript 모듈 증강(module augmentation)을 사용해요. 이렇게 하면 추가 필드로 FrontendPluginInfo 인터페이스를 확장할 수 있으며, 그 필드에 커스텀 해석 로직을 추가하고 앱 내에서 접근할 수도 있어요. 예를 들어 slackChannel 필드를 다음과 같이 추가할 수 있어요.

declare module '@backstage/frontend-plugin-api' {  interface FrontendPluginInfo {    /**     * The slack channel to use for support requests for this plugin.     */    slackChannel?: string;  }}

플러그인 정보 해석 커스터마이즈하기 (Customizing Plugin Info Resolution)

새 slackChannel 필드가 준비됐으니, 이 정보를 플러그인 정보 소스에서 추출하는 방법을 아는 커스텀 해석기(resolver)를 제공해야 해요. 이는 createApp에 커스텀 pluginInfoResolver를 전달해 수행하며, 우리 예시에서는 다음과 같이 선언돼요.

pluginInfoResolver.ts

import { createPluginInfoResolver } from '@backstage/frontend-plugin-api';// It is recommended to keep the above module augmentation in this file tooexport const pluginInfoResolver: FrontendPluginInfoResolver = async ctx => {  // In our particular example app we assume that all plugin manifests are catalog-info.yaml files  const manifest = (await ctx.manifest?.()) as Entity | undefined;  // Call the default resolver to populate common fields  const { info } = await ctx.defaultResolver({    packageJson: await ctx.packageJson(),    manifest: manifest,  });  // In this example the catalog model has been extended with a metadata.slackChannel field  const slackChannel = manifest?.metadata?.slackChannel?.toString();  if (slackChannel) {    info.slackChannel = slackChannel;    info.links = [      ...(info.links ?? []),      {        title: 'Slack Channel',        url: `https://our-workspace.enterprise.slack.com/archives/${slackChannel}`,      },    ];  }  return { info };};

그리고 앱에 다음과 같이 포함해요.

App.tsx

import { pluginInfoResolver } from './pluginInfoResolver';const app = createApp({  pluginInfoResolver,  // ... other options});

플러그인 정보 재정의하기 (Overriding Plugin Info)

플러그인 정보를 커스터마이즈하는 또 다른 방법은 app.pluginOverrides 정적 구성 키를 사용하는 것이에요. 이 재정의는 플러그인 정보가 해석된 뒤 사용자에게 제공되기 전의 마지막 단계로 적용돼요. 특히 서드파티 플러그인의 정보를 재정의하는 데 유용해요. 예를 들어 조직에 Software Catalog의 유지 관리를 담당하는 개별 팀이 있다면 다음 재정의를 구성할 수 있어요.

app:  pluginOverrides:    - match:        pluginId: catalog      info:        ownerEntityRefs: [catalog-owners]

플러그인의 pluginId 및/또는 packageName으로 일치시킬 수 있어요. 다만 packageName은 플러그인이 package.json 파일용 로더를 제공할 때만 지원돼요. /패턴/을 사용하면 이 일치에 정규식 패턴도 사용할 수 있어요. 예를 들어 @acme 네임스페이스의 모든 플러그인 소유자를 재정의하려면 다음과 같이 할 수 있어요.

app:  pluginOverrides:    - match:        packageName: /@acme/.*/      info:        ownerEntityRefs: [acme-owners]

더 알아보기 (Learn more)