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

커스텀 필드 확장 작성

원문 보기 위키 갱신

info

출처: 문서

본문

info

이 문서는 새 Backstage 앱에서 기본인 새 프론트엔드 시스템을 위해 작성되었습니다. 여러분의 Backstage 앱이 여전히 이전 프론트엔드 시스템을 사용한다면, 이 가이드의 이전 프론트엔드 시스템 버전을 대신 읽으세요.

사용자로부터 입력을 수집하는 것은 스캐폴딩 과정과 소프트웨어 템플릿 전체에서 아주 큰 부분입니다. 때로는 내장 컴포넌트와 필드가 충분하지 않을 때가 있고, 때로는 더 잘 맞는 더 나은 입력으로 사용자가 보는 폼을 풍부하게 만들고 싶을 때가 있습니다.

여기서 Custom Field Extensions(커스텀 필드 확장)이 등장합니다.

이것들을 사용하면 자신만의 React 컴포넌트를 보여주고, 그것을 사용해 JSON 스키마의 상태를 제어할 수 있으며, 데이터를 검증하기 위한 자신만의 검증 함수도 제공할 수 있습니다.

필드 확장 만들기

필드 확장은 ID, React 컴포넌트, validation 함수를 모듈식으로 결합하는 방법으로, 그런 다음 Backstage 앱에서 확장으로 등록할 수 있습니다.

@backstage/plugin-scaffolder-react/alpha의 FormFieldBlueprint를 createFormField와 함께 사용해 나만의 필드 확장을 만들 수 있습니다. createFormField는 컴포넌트, 검증, 선택적 스키마를 타입화합니다.

예시로, 문자열이 Kebab-case 패턴에 있는지 검증하는 컴포넌트를 만들겠습니다.

먼저 컴포넌트와 검증 함수를 만듭니다.

// packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsximport { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';import type { FieldValidation } from '@rjsf/utils';import FormControl from '@material-ui/core/FormControl';import FormHelperText from '@material-ui/core/FormHelperText';import Input from '@material-ui/core/Input';import InputLabel from '@material-ui/core/InputLabel';export const ValidateKebabCase = ({  onChange,  rawErrors,  required,  formData,}: FieldExtensionComponentProps<string>) => {  return (    <FormControl      margin="normal"      required={required}      error={rawErrors?.length > 0 && !formData}    >      <InputLabel htmlFor="validateName">Name</InputLabel>      <Input        id="validateName"        aria-describedby="entityName"        onChange={e => onChange(e.target?.value)}      />      <FormHelperText id="entityName">        Use only letters, numbers, hyphens and underscores      </FormHelperText>    </FormControl>  );};export const validateKebabCaseValidation = (  value: string,  validation: FieldValidation,) => {  const kebabCase = /^[a-z0-9-_]+$/g.test(value);  if (kebabCase === false) {    validation.addError(      `Only use letters, numbers, hyphen ("-") and underscore ("_").`,    );  }};

그런 다음 FormFieldBlueprint와 createFormField를 사용해 확장을 만듭니다.

// packages/app/src/scaffolder/ValidateKebabCase/extensions.tsimport {  FormFieldBlueprint,  createFormField,} from '@backstage/plugin-scaffolder-react/alpha';import {  ValidateKebabCase,  validateKebabCaseValidation,} from './ValidateKebabCaseExtension';export const ValidateKebabCaseFieldExtension = FormFieldBlueprint.make({  name: 'validate-kebab-case',  params: {    field: async () =>      createFormField({        name: 'ValidateKebabCase',        component: ValidateKebabCase,        validation: validateKebabCaseValidation,      }),  },});
// packages/app/src/scaffolder/ValidateKebabCase/index.tsexport { ValidateKebabCaseFieldExtension } from './extensions';

확장이 생성되면, 프론트엔드 모듈로 감싸고 createApp에 전달해 앱에 설치합니다.

packages/app/src/scaffolder/scaffolderModule.ts

import { createFrontendModule } from '@backstage/frontend-plugin-api';import { ValidateKebabCaseFieldExtension } from './ValidateKebabCase';export const scaffolderCustomizations = createFrontendModule({  pluginId: 'scaffolder',  extensions: [ValidateKebabCaseFieldExtension],});

packages/app/src/App.tsx

import { createApp } from '@backstage/frontend-defaults';import { scaffolderCustomizations } from './scaffolder/scaffolderModule';const app = createApp({  features: [scaffolderCustomizations],});export default app.createRoot();

비동기 검증 함수

검증 함수는 비동기일 수 있고, 필드 검증 컨텍스트의 ApiHolder를 통해 Utility API를 사용할 수 있습니다. 아래 예시는 catalogApiRef를 사용해 제출된 값(이 시나리오에서는 엔티티 참조)이 카탈로그에 존재하는지 확인합니다.

import { FieldValidation } from '@rjsf/utils';import { ApiHolder } from '@backstage/core-plugin-api';import { catalogApiRef } from '@backstage/plugin-catalog-react';export const customFieldExtensionValidator = async (  value: string,  validation: FieldValidation,  context: { apiHolder: ApiHolder },) => {  const catalogApi = context.apiHolder.get(catalogApiRef);  if ((await catalogApi?.getEntityByRef(value)) === undefined) {    validation.addError('Entity not found');  }};

커스텀 필드 확장 사용

등록되면, 템플릿에서 ui:field 속성을 사용해 커스텀 필드 확장의 이름을 참조할 수 있습니다.

apiVersion: scaffolder.backstage.io/v1beta3kind: Templatemetadata:  name: Test template  title: Test template with custom extension  description: Test templatespec:  parameters:    - title: Fill in some steps      required:        - name      properties:        name:          title: Name          type: string          description: My custom name for the component          ui:field: ValidateKebabCase  steps:  [...]

다른 필드의 데이터 접근

커스텀 필드 확장은 폼 컨텍스트를 통해 폼의 다른 필드에서 데이터를 읽을 수 있습니다. 이것은 결합을 만든다는 점에서 권장되지 않지만, 때로는 여전히 가장 합리적인 해결책입니다.

import {  FormFieldBlueprint,  createFormField,} from '@backstage/plugin-scaffolder-react/alpha';import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';const CustomFieldExtensionComponent = (props: FieldExtensionComponentProps<string[]>) => {  const { formData } = props.formContext;  ...};const CustomFieldExtension = FormFieldBlueprint.make({  name: 'custom-field',  params: {    field: async () =>      createFormField({        name: 'custom-field',        component: CustomFieldExtensionComponent,        validation: ...,      }),  },});

커스텀 필드 확장 미리보기

Custom Field Explorer(기본적으로 /create/edit 라우트로 접근)를 사용해 Backstage UI에서 작성한 커스텀 필드 확장을 미리 볼 수 있습니다.

탐색기에서 새 커스텀 필드 확장을 사용 가능하게 하려면, 필드의 입력/출력 타입을 설명하는 JSON 스키마를 다음과 같은 예시처럼 정의해야 합니다.

// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsximport FormControl from '@material-ui/core/FormControl';import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';export const MyCustomExtensionWithOptionsSchema = {  uiOptions: {    type: 'object',    properties: {      focused: {        description: 'Whether to focus this field',        type: 'boolean',      },    },  },  returnValue: { type: 'string' },};export const MyCustomExtensionWithOptions = ({  onChange,  rawErrors,  required,  formData,  uiSchema,}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {  const focused = uiSchema['ui:options']?.focused;  return (    <FormControl      margin="normal"      required={required}      error={rawErrors?.length > 0 && !formData}      onChange={onChange}      focused={focused}    />  );};
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.tsimport {  FormFieldBlueprint,  createFormField,} from '@backstage/plugin-scaffolder-react/alpha';import {  MyCustomExtensionWithOptions,  MyCustomExtensionWithOptionsSchema,} from './MyCustomExtensionWithOptions';export const MyCustomFieldWithOptionsExtension = FormFieldBlueprint.make({  name: 'MyCustomExtensionWithOptions',  params: {    field: async () =>      createFormField({        name: 'MyCustomExtensionWithOptions',        component: MyCustomExtensionWithOptions,        schema: MyCustomExtensionWithOptionsSchema,      }),  },});

스키마를 정의하고 제공된 makeFieldSchemaFromZod 헬퍼 유틸리티 함수를 사용해 필드 props에 대한 JSON 스키마와 타입을 모두 생성해 정의를 중복하지 않도록, zod 같은 라이브러리를 사용할 것을 권장합니다.

// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsximport FormControl from '@material-ui/core/FormControl';import { z } from 'zod/v3';import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(  z.string(),  z.object({    focused: z.boolean().optional().describe('Whether to focus this field'),  }),);export const MyCustomExtensionWithOptionsSchema =  MyCustomExtensionWithOptionsFieldSchema.schema;type MyCustomExtensionWithOptionsProps =  typeof MyCustomExtensionWithOptionsFieldSchema.type;export const MyCustomExtensionWithOptions = ({  onChange,  rawErrors,  required,  formData,  uiSchema,}: MyCustomExtensionWithOptionsProps) => {  const focused = uiSchema['ui:options']?.focused;  return (    <FormControl      margin="normal"      required={required}      error={rawErrors?.length > 0 && !formData}      onChange={onChange}      focused={focused}    />  );};

더 알아보기 (Learn more)