프론트엔드 확장
프론트엔드 확장 (Frontend Extensions)
이전 섹션에서 언급했듯이 Backstage 앱은 확장의 트리로 구성돼요. 이 섹션에서는 확장이 무엇인지, 확장을 만들고 사용하는 방법, 그리고 자체 확장성 패턴을 만드는 방법에 대해 더 자세히 다룰 거예요.
출처: 문서
본문
이전 섹션에서 언급했듯이 Backstage 앱은 확장의 트리로 구성돼요. 이 섹션에서는 확장이 무엇인지, 확장을 만들고 사용하는 방법, 그리고 자체 확장성 패턴을 만드는 방법에 대해 더 자세히 다룰 거예요.
확장 구조 (Extension Structure)
각 확장에는 동작 방식과 다른 확장 및 앱의 나머지 부분과 상호작용하는 방식을 정의하는 여러 속성이 있어요. 이 중 일부 속성은 고정되어 있고, 다른 일부는 통합자(integrator)가 커스터마이즈할 수 있어요. 아래 다이어그램은 확장의 구조를 보여줘요.
ID
확장의 ID는 확장을 고유하게 식별하는 데 사용되며, 이상적으로는 전체 Backstage 생태계에서 고유해야 해요. 각 프론트엔드 앱 인스턴스에는 주어진 ID에 대해 단일 확장만 있을 수 있어요. 같은 ID로 여러 확장을 설치하면 오류가 발생하거나 확장 중 하나가 다른 확장을 재정의하게 돼요. 이 ID는 다른 확장에서 확장을 참조하고, 구성에서, 그리고 개발자 도구와 분석 같은 다른 곳에서도 사용돼요.
확장을 만들 때 ID를 직접 제공하지 않아요. 대신 ID를 구성하는 kind, namespace, name 부분을 간접적 또는 직접적으로 제공해요. kind는 항상 확장 블루프린트가 제공하며, 유일한 예외는 createExtension을 직접 사용할 때예요. 플러그인이 제공하는 확장은 기본적으로 namespace가 플러그인 ID로 설정되므로, 기존 확장을 재정의하려는 경우가 아니면 일반적으로 명시적 namespace를 제공할 필요가 없어요. name도 선택 사항이며 주로 같은 kind와 namespace의 여러 확장을 구분하는 데 사용돼요. 플러그인이 같은 kind의 다른 확장을 구분할 필요가 없다면 name은 생략할 수 있어요.
확장 ID는 [<kind>:][<namespace>][/][<name>] 패턴으로 구성되며, namespace와 name이 모두 정의된 경우에만 구분 /가 존재해요.
출력 (Output)
확장의 출력은 부모 확장에 제공하는 데이터이며, 궁극적으로 앱에 대한 기여예요. 출력 자체는 TypeScript 유형으로 표현할 수 있는 모든 것, 임의 값의 모음 형태로 제공돼요. 하지만 각 개별 출력 값은 확장 데이터 참조(extension data reference)라고 하는 공유 참조와 연결되어야 해요. 확장의 개별 출력 값에 접근하려면 이 동일한 참조를 사용해야 해요.
입력 (Inputs)
확장의 입력은 자식에게서 받는 데이터를 정의해요. 각 확장은 입력 이름으로 식별되는 여러 다른 입력을 가질 수 있어요. 이 각 입력에는 고유한 예상 데이터 세트가 있으며, 이는 확장 데이터 참조의 모음으로 정의돼요. 확장은 각 입력에서 명시적으로 요청한 데이터에만 접근할 수 있어요.
부착 지점 (Attachment point)
확장의 부착 지점은 앱 확장 트리에서 어디에 위치할지 결정해요. 이는 부모 확장의 ID와 부착할 입력의 이름으로 정의돼요. 부착 지점을 통해 확장은 자신의 출력을 부모 확장에 입력으로 공유해요. 확장은 자신의 출력과 일치하는 입력에만 부착할 수 있으며, 확장이 출력에서 제공하지 않는 데이터가 필요한 입력에 확장을 부착하려고 하면 오류예요.
부착 지점은 확장의 구성 가능한 속성 중 하나이며 통합자가 재정의할 수 있어요. 그렇게 할 때는 확장을 호환되지 않는 입력에 부착하지 않도록 주의해야 해요. 확장은 또한 한 번에 단일 입력과 부모에만 부착할 수 있어요. 즉 앱 확장 트리는 순환(cycle)을 포함할 수 없어요. 확장의 조상은 루트에서 끝나거나 루트에서 분리되기 때문이에요.
비활성화 (Disabled)
앱의 각 확장은 비활성화될 수 있으며, 이 경우 인스턴스화되지 않고 부모가 입력에서 이를 보지 못해요. 확장을 만들 때 확장이 기본적으로 비활성화되어야 하는지 지정할 수도 있어요. 이렇게 하면 예를 들어 앱에 여러 확장을 설치하되 환경에 따라 그중 하나 또는 몇 개만 활성화하도록 선택할 수 있어요.
확장의 순서는 때로 매우 중요해요. 예를 들어 UI에 표시되는 순서에 영향을 줄 수 있기 때문이에요. 구성으로 확장이 비활성화에서 활성화로 전환되면 확장의 순서가 재설정되어 목록 끝으로 밀려나요. 확장의 순서가 중요하다면 일반적으로 기본적으로 비활성화로 두는 것을 권장하며, 구성을 통해 활성화되는 순서가 앱에서의 순서를 결정하도록 해요.
조건 (Conditions)
if predicate를 제공해 확장을 조건부로 활성화할 수도 있어요. 이는 createExtension(...)에서 직접 사용할 수 있고 블루프린트에서 확장을 만들 때도 사용할 수 있어요.
predicate는 Backstage의 다른 곳과 같은 FilterPredicate 구문을 사용하지만, 프론트엔드 시스템에서는 featureFlags와 permissions 같은 앱 수준 데이터에 대해 평가돼요. 예를 들어 다음 페이지는 experimental-features 플래그가 활성화된 경우에만 설치돼요.
const examplePage = PageBlueprint.make({ params: { path: '/example', loader: () => import('./ExamplePage').then(m => <m.ExamplePage />), }, if: { featureFlags: { $contains: 'experimental-features' } },});
$all, $any, $not 같은 논리 연산자로 조건을 결합할 수도 있어요.
const guardedCard = CardBlueprint.make({ params: { title: 'Guarded Card', loader: () => import('./GuardedCard').then(m => <m.GuardedCard />), }, if: { $all: [ { featureFlags: { $contains: 'experimental-features' } }, { permissions: { $contains: 'catalog.entity.create' } }, ], },});
권한 이름 형식 (Permission name format)
if predicate의 권한 이름은 권한 검사에서 action 속성을 지정하는 선택적 #action 접미사를 지원해요. 형식은 permissionName#action이에요.
const actionGatedPage = PageBlueprint.make({ params: { path: '/edit-catalog', loader: () => import('./EditCatalogPage').then(m => <m.EditCatalogPage />), }, // Only shown when the user can perform the 'update' action on catalog.entity.refresh if: { permissions: { $contains: 'catalog.entity.refresh#update' } },});
접미사 없이(예: catalog.entity.create) 권한은 빈 attributes 객체로 검사되며, 이는 액션을 담지 않는 기본 권한과 일치해요. 접미사와 함께(예: catalog.entity.refresh#update) # 앞부분은 권한 이름이고 뒷부분은 attributes.action으로 권한 API에 전달돼요.
조건은 앱이 실행되는 동안 연속적으로가 아니라 앱 트리가 준비될 때 평가돼요. 기반 feature flags나 권한이 바뀌면 확장 트리가 바뀌려면 앱을 다시 준비해야 하며, 실제로는 일반적으로 앱을 다시 로드한다는 뜻이에요.
플러그인이나 모듈도 if predicate를 제공하면 확장 수준 predicate와 논리적 AND로 결합돼요. 자세한 내용은 플러그인 if 옵션과 프론트엔드 모듈 섹션을 참고하세요.
구성 및 구성 스키마 (Configuration & configuration schema)
각 확장은 수용하는 구성을 설명하는 구성 스키마를 정의할 수 있어요. 이 스키마는 통합자가 제공하는 구성을 검증하는 데 사용되고, 기본 구성 값을 채우는 데도 사용돼요. 구성 자체는 확장을 커스터마이즈하기 위해 통합자가 제공해요. 확장의 기본 구성을 제공하는 것은 불가능하며, 이는 구성 스키마의 기본값을 통해 수행해야 해요. 이렇게 하면 같은 확장의 여러 구성이 병합되는 대신 서로 완전히 교체하는 더 간단한 구성 로직이 가능해져요.
Factory
확장 factory는 확장 자체의 구현이에요. 확장이 받은 입력과 구성이 제공되는 함수이며, 정의된 출력을 생성해야 해요. 앱 인스턴스가 시작되면 앱의 일부인 각 확장의 factory 함수를 리프 노드에서 시작해 앱 확장 트리의 루트까지 올라가며 호출해요. factory는 활성 확장에 대해서만 호출되는데, 활성 확장은 비활성화되지 않고 활성 부모가 있는 확장이에요.
확장 factory는 가볍게 유지하고 무거운 작업이나 비동기 작업을 하지 않아야 해요. 앱 초기화 중에 호출되기 때문이에요. 예를 들어 출력을 생성하기 위해 비용이 많이 드는 계산이 필요하다면, 계산을 수행하는 콜백을 출력하는 것을 선호해요. 이렇게 하면 부모 확장이 계산을 나중으로 지연시킬 수 있어 앱 시작을 막지 않아요.
확장 만들기 (Creating an extension)
확장은 @backstage/frontend-plugin-api의 createExtension 함수를 사용해 만들어요. 최소한 ID, 부착 지점, 출력 정의, factory 함수를 제공해야 해요. 다음 예시는 최소 확장의 생성을 보여줘요.
const extension = createExtension({ name: 'my-extension', // This is the attachment point, `id` is the ID of the parent extension, // while `input` is the name of the input to attach to. attachTo: { id: 'my-parent', input: 'content' }, // The output option defines the allowed and required outputs of the extension factory. output: [coreExtensionData.reactElement], // This factory is called to instantiate the extensions and produce its output. factory() { return [coreExtensionData.reactElement(<div>Hello World</div>)]; },});
createExtension 함수는 공개 API이고 많은 곳에서 사용되지만, 플러그인과 기능을 구축할 때 일반적으로 사용하는 것은 아니라는 점에 유의하세요. 대신 핵심 API와 플러그인 모두가 export하는 많은 확장 블루프린트가 있어 더 구체적인 용도에 맞는 확장을 더 쉽게 만들 수 있어요.
확장 데이터 (Extension data)
확장 간 통신은 한 방향으로, 한 자식 확장에서 부착 지점을 통해 부모로 진행돼요. 자식 확장은 데이터를 출력하며 이는 부모 확장에 입력으로 전달돼요. 이 데이터를 확장 데이터(Extension Data)라고 하며, 각 개별 데이터 조각의 형태는 확장 데이터 참조(Extension Data Reference)로 설명돼요. 이 참조는 확장 자체와 별개로 만들어지며 여러 다른 종류의 확장 간에 공유될 수 있어요. 각 참조는 ID와 데이터가 준수해야 하는 TypeScript 유형으로 구성되며, 확장 간 공유할 수 있는 한 유형의 데이터를 나타내요.
확장 데이터 참조 (Extension data references)
공유 확장 데이터 유형을 나타내는 새 확장 데이터 참조를 만들려면 createExtensionDataRef 함수를 사용해요. 새 참조를 정의할 때 ID와 TypeScript 유형을 제공해야 해요. 예를 들어:
export const reactElementExtensionDataRef = createExtensionDataRef<React.JSX.Element>().with({ id: 'my-plugin.reactElement', });
ExtensionDataRef는 확장의 출력 속성을 설명하는 데 사용할 수 있어요. 이는 확장 factory의 반환 값에 유형을 강제해요.
const extension = createExtension({ // ... output: [reactElementExtensionDataRef], factory() { return [reactElementExtensionDataRef(<div>Hello World</div>)]; },});
확장 데이터 고유성 (Extension data uniqueness)
출력에서 동일한 데이터 참조를 반복하거나 같은 참조에 대해 여러 값을 반환하는 것은 허용되지 않는다는 점에 유의하세요. 같은 참조에 대한 여러 출력은 서로 충돌해 오류를 일으켜요. 같은 TypeScript 유형의 여러 값을 출력하려면 각 값에 대해 별도의 참조를 만들어야 해요. 이는 지나치게 일반적인 확장 데이터 참조(예: 일반적인 "string" 유형)가 좋지 않은 생각이라는 뜻이에요. 대신 공유하려는 각 데이터 유형에 대해 별도의 참조를 만드세요.
const extension = createExtension({ // ... output: [ // ❌ Bad example - duplicate output declaration reactElementExtensionDataRef, reactElementExtensionDataRef, ], factory() { return [ // ❌ Bad example - duplicate output values reactElementExtensionDataRef(<div>Hello</div>), reactElementExtensionDataRef(<div>World</div>), ]; },});
핵심 확장 데이터 (Core extension data)
우리는 React.JSX.Element, RouteRef 같은 자주 사용되는 ExtensionDataRef를 제공하는 기본 coreExtensionData를 제공해요. 이들은 자체 확장을 만들 때 사용할 수 있어요. 예를 들어 위에서 정의한 React Element 확장 데이터는 이미 coreExtensionData.reactElement로 제공돼요.
모든 유형의 핵심 확장 데이터의 전체 목록과 설명은 핵심 확장 데이터 참조를 참고하세요.
선택적 확장 데이터 (Optional extension data)
기본적으로 모든 확장 데이터는 필수이며, 확장 factory가 각 출력에 대해 값을 제공해야 해요. 하지만 .optional() 메서드를 호출해 확장 데이터를 선택적으로 만들 수 있어요. 이렇게 하면 factory 함수가 출력의 일부로 값을 반환하는 것이 선택 사항이 돼요. .optional() 메서드를 호출하면 확장 데이터 참조의 새 사본을 만들며 기존 참조는 변경하지 않아요.
const extension = createExtension({ // ... output: [coreExtensionData.reactElement.optional()], factory() { return Math.random() > 0.5 ? [coreExtensionData.reactElement(<div>Hello World</div>)] : []; },});
확장 입력 (Extension Inputs)
확장 데이터는 확장 입력을 통해 다른 확장으로 전달될 수 있어요. 앞서 본 출력과 유사하게, 확장 입력이 있는 확장의 예시를 만들어 보겠어요.
const navigationExtension = createExtension({ // ... inputs: { // [1]: Input logo: createExtensionInput([coreExtensionData.reactElement], { singleton: true, optional: true, }), }, factory({ inputs }) { return { element: ( <nav> {inputs.logo?.get(coreExtensionData.reactElement) ?? ( <span>Backstage</span> )} </nav> ), }; }, // ...});
입력(위 [1] 참고)은 createExtensionInput으로 만드는 객체예요. 첫 번째 인자는 이 입력을 통해 수용하는 확장 데이터 세트이며 output 옵션처럼 작동해요. 두 번째 인자는 선택 사항이며, 우리 입력에 부착된 확장에 제약을 두는 것을 허용해요. singleton: true 옵션이 설정되면 한 번에 단일 확장만 부착될 수 있고, optional: true 옵션이 설정되지 않는 한 정확히 하나의 부착 확장이 있어야 하는 것도 요구돼요.
확장 입력을 만들 때 사용할 수 있는 또 다른 옵션은 internal: true 옵션이에요. 이는 입력을 정의하는 확장과 같은 플러그인의 확장만 입력이 수용하도록 제한해요. 내부 입력에 부착하려고 시도하는 다른 플러그인의 확장은 무시되고 경고가 보고돼요. 이는 확장성을 플러그인의 재정의와 모듈로 제한하고 싶을 때 유용하며, 어느 플러그인에나 열어 두고 싶지 않을 때 사용해요.
그럼 이제 자신의 출력을 부모 확장의 입력에 어떻게 부착할 수 있을까요? Backstage의 Sidebar 같은 탐색 컴포넌트를 생각해 보면, 플러그인이 이 탐색 컴포넌트에 자체 플러그인으로 가는 링크를 부착하고 싶을 수 있어요. 이 경우 플러그인은 factory가 반환한 확장 output을 지정된 확장에 부착하려면 확장 id와 확장 input의 이름만 알면 돼요.
const navigationItemExtension = createExtension({ // ... attachTo: { id: 'app/nav', input: 'items' }, output: [coreExtensionData.reactElement], factory() { return [coreExtensionData.reactElement(<Link to="/home">Home</Link>)]; },});const navigationExtension = createExtension({ // ... // [2]: Extension `id` will be `app/nav` following the extension naming pattern namespace: 'app', name: 'nav', output: [coreExtensionData.reactElement], inputs: { items: createExtensionInput([coreExtensionData.reactElement]), }, factory({ inputs }) { return [ coreExtensionData.reactElement( <nav> <ul> {inputs.items.map(item => { return <li>{item.get(coreExtensionData.reactElement)}</li>; })} </ul> </nav>, ), ]; }, // ...});
이 경우 확장 입력 items는 배열이며, 각 개별 항목은 이 id의 확장 입력에 자체를 부착한 확장이에요.
입력을 통해 전달된 데이터에 접근할 수 있을 뿐만 아니라 기본 앱 node에도 접근할 수 있어요. 예를 들어 부착된 확장의 ID를 얻고 싶다면 유용해요. 하지만 필요하지 않는 한 node 사용을 피하세요. 일반적으로 제공된 데이터만 소비하는 것이 더 좋아요. 위 예시의 factory 함수를 보면 node에 다음과 같이 접근할 수 있어요.
// ... factory({ inputs }) { return [ coreExtensionData.reactElement( <nav> <ul> {inputs.items.map((item) => { const _node: AppNode = item.node; return <li>{item.get(coreExtensionData.reactElement)}</li>; })} </ul> </nav> ), ]; },
확장 구성 (Extension configuration)
app-config.yaml로 이미 플러그인이나 앱에 앱의 baseURL을 정의하는 것 같은 구성을 전달하는 옵션이 있어요. 확장의 경우 확장이 플러그인과 독립적이고 여러 번 초기화될 수 있으므로 이 개념은 제한적일 수 있어요. 그래서 우리는 각 확장을 구성(confng)을 통해 개별적으로 구성할 수 있는 가능성을 만들었어요. 확장 구성 스키마는 JSON Schema 지원이 있는 Standard Schema 인터페이스를 구현하는 모든 스키마 라이브러리(예: zod v4, 즉 zod@^4.0.0)로 만들어져요. TypeScript 유형 검사 외에도 스키마는 런타임 검증과 강제 변환(coercion)도 제공해요. navigationExtension 예시를 이어서 이제 구성 가능한 제목을 갖고 싶다면 다음과 같이 사용할 수 있게 할 수 있어요.
import { z } from 'zod';const navigationExtension = createExtension({ // ... namespace: 'app', name: 'nav', // [3]: Extension `id` will be `app/nav` following the extension naming pattern configSchema: { title: z.string().default('Sidebar Title'), }, factory({ config }) { return [ coreExtensionData.reactElement( <nav> <span>{config.title}</span> <ul>{/* ... */}</ul> </nav>, ), ]; }, // ...});
이제 제목 텍스트를 "Sidebar Title"에서 "Backstage"로 바꾸려면 확장의 id를 보고 app-config.yaml에 다음을 추가하면 돼요.
app: # ... extensions: # ... - app/nav: config: title: 'Backstage'
제너레이터 함수로서의 확장 factory
지금까지 모든 예시에서 확장 factory를 출력을 배열로 반환하는 일반 함수로 정의했어요. 하지만 factory 함수가 확장 데이터 값의 iterable을 반환하기만 하면 된다는 것이 유일한 요구 사항이에요. 즉 factory 함수를 제너레이터 함수로 정의할 수도 있으며, 이렇게 하면 값을 하나씩 생성(yield)할 수 있어요. 조건부로 값을 출력하고 싶을 때 특히 유용해요.
예를 들어 출력이 구성에 따라 달라지는 확장을 다음과 같이 정의할 수 있어요.
import { z } from 'zod';const exampleExtension = createExtension({ // ... configSchema: { disableIcon: z.boolean().default(false), }, output: [coreExtensionData.reactElement, iconDataRef.optional()], *factory({ config }) { yield coreExtensionData.reactElement(<div>Hello World</div>); if (!config.disableIcon) { yield iconDataRef(<ExampleIcon />); } },});
확장 경계 (Extension boundary)
ExtensionBoundary는 다양한 목적을 위해 여러 React 컨텍스트로 확장을 감싸요.
Suspense
확장이 렌더링하는 대부분의 React 요소는 확장 경계로 감싸야 해요. Suspense를 사용하면 확장이 로딩 폴백으로 리소스를 비동기적으로 로드할 수 있어요. 또한 Backstage에서 플러그인이 현재 지연 로드되는 것과 유사하게 전체 확장을 지연 로드할 수도 있어요.
Error boundary
플러그인과 유사하게 확장용 ErrorBoundary는 컴포넌트 내부에서 잡히지 않은 오류가 있을 때 폴백 컴포넌트를 전달할 수 있게 해줘요. 이렇게 하면 오류를 격리할 수 있고 나머지 플러그인이 충돌하는 것을 막을 수 있어요.
Analytics
분석 정보는 AnalyticsContext를 통해 제공되며, 확장 내부에서 발생한 분석 이벤트에 extensionId와 pluginId를 컨텍스트로 제공해요. 추가로 RouteTracker는 라우팅 가능한 확장에 대한 분석 이벤트를 캡처해, 탐색된 경로가 수집된 mountPoint일 때 어떤 확장 메타데이터가 탐색 이벤트와 연결되는지 알려줘요. 확장이 라우팅 가능한지는 출력에서 추론되지만, ExtensionBoundary에 routable prop을 전달해 이 동작을 명시적으로 제어할 수도 있어요.
ExtensionBoundary는 확장에서 다음과 같이 사용할 수 있어요.
const routableExtension = createExtension({ // ... factory({ config, inputs, node }) { return [ coreExtensionData.path(config.path), coreExtensionData.routeRef(myRouteRef), coreExtensionData.reactElement( <ExtensionBoundary node={node}> <MyExtensionComponent /> </ExtensionBoundary>, ), ]; },});
여러 위치에 확장 공유하기 (Sharing extensions across multiple locations)
앱 전체의 여러 위치에서 확장을 사용할 수 있게 해야 한다면, 확장을 수집하고 여러 부모 확장이 이를 소비할 수 있게 하는 Utility API를 사용하세요. 이 패턴은 더 나은 관심사 분리를 제공하고 데이터 흐름을 더 명확하게 만들어요.
이 패턴에 대한 완전한 설명과 상세 예시는 여러 위치에 확장 공유하기 가이드를 참고하세요.
상대 부착 지점 (Relative attachment points)
확장이나 확장 블루프린트를 만들 때 현재 플러그인에 상대적인 부착 지점을 지정할 수 있어요. 이는 공통 계층 구조의 일부인 블루프린트 그룹, 즉 한 블루프린트의 확장이 다른 블루프린트의 확장에 부착되는 경우에 특히 유용해요. 예를 들어 다음 확장 정의 쌍은 서로 다른 플러그인에 여러 번 설치될 수 있으며, 각각 자체 계층 구조를 만들어요.
// Parent extension with a fixed attachment pointconst parentExtension = createExtension({ kind: 'section', attachTo: { id: 'app/some-fixed-extension', input: 'children' }, inputs: { content: createExtensionInput([coreExtensionData.reactElement], { singleton: true, }), }, output: [coreExtensionData.reactElement], factory({ inputs }) { return [ coreExtensionData.reactElement( <section> <h1>Section Title</h1> {inputs.content.get(coreExtensionData.reactElement)} </section>, ), ]; },});// Child extension with a relative attachment pointconst childExtension = createExtension({ kind: 'section-content', attachTo: { relative: { kind: 'section' }, input: 'content' }, output: [coreExtensionData.reactElement], factory() { return [coreExtensionData.reactElement(<p>Section Content</p>)]; },});
확장 입력 참조 (Extension input references)
상대 부착 지점 개념을 바탕으로, 확장 정의의 inputs 속성을 통해 확장 입력을 직접 참조할 수도 있어요. 이는 자식 확장을 부착하는 더 편리하고 유형 안전한 방법을 제공하며, 특히 중첩된 확장 계층을 제공하는 블루프린트를 사용할 때 유용해요.
확장 입력 참조는 항상 상대적이에요. 즉 같은 플러그인 내의 확장을 참조하는 데만 사용할 수 있어요.
각 확장 정의는 정의된 모든 입력에 대한 참조를 포함하는 inputs 속성을 노출해요. 이 참조는 자식 확장을 만들 때 attachTo 옵션에 직접 전달할 수 있어요.
const parent = createExtension({ inputs: { children: createExtensionInput([coreExtensionData.reactElement]), }, // other options...});// Create a child extension that attaches to the parent's inputconst child = createExtension({ attachTo: parent.inputs.children, // Direct reference to the input output: [coreExtensionData.reactElement], // Outputs are verified against the parent input // other options...});
이 참조는 자식 확장을 부착하는 유형 안전한 방법이에요. 부모 입력이 존재하는지와 자식이 부모에 필요한 데이터를 제공하는지 모두 보장해요.
내부적으로 입력 참조는 상대 부착 지점과 같은 방식으로 해석되며, 확장의 kind, namespace, name을 사용해 최종 부착 대상을 구성해요.