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

플러그인에 검색 통합

원문 보기 위키 갱신

레거시 문서

출처: 문서

본문

레거시 문서

이 섹션은 레거시 플러그인 문서의 일부예요. 여기 설명된 백엔드 검색 콜레이터 패턴은 새 백엔드 시스템을 사용하며 여전히 유효해요. 프론트엔드 검색 경험 예시는 이전 프론트엔드 시스템 API를 사용해요.

Backstage Search Platform은 플러그인 개발자에게 플러그인 안에서 검색 경험을 제공하는 데 필요한 API와 인터페이스를 제공하면서, 특정 기본 검색 기술은 추상화하고(대신 실행하는 애플리케이션 통합자가 선택하도록) 설계되었어요.

이 페이지에서는 Backstage Search Platform을 플러그인에서 활용하기 위한 개념과 튜토리얼을 찾을 수 있어요.

검색 플랫폼에 데이터 제공

콜레이터 만들기

콜레이터(collator)가 무엇인지 알면 그것을 구축할 때 도움이 돼요.

데이터베이스에 FAQ 스니펫을 저장하는 일을 담당하는 플러그인이 있다고 상상해 보세요. 다른 엔지니어가 질문과 답변을 쉽게 찾을 수 있기를 원해요. 즉 그것들이 검색 플랫폼에 인덱싱되기를 원한다는 뜻이에요. FAQ 스니펫이 backstage.example.biz/faq-snippets 같은 URL에서 볼 수 있다고 가정해 보세요.

검색 플랫폼은 정확히 그렇게 할 수 있게 하는 인터페이스(@backstage/plugin-search-common 패키지의 DocumentCollatorFactory)를 제공해요. 각 항목을 나중에 각각 하나의 검색 결과를 나타내는 "문서(document)"로 등록해 작동해요.

확실하지 않거나 모범 사례를 따르고 싶다면 StackOverflowQuestionsCollatorFactory 같은 작동 예시를 항상 볼 수 있어요.

1. 콜레이터 모듈 패키지 만들기

Backstage 인덱스 레지스트리에 FAQ 콜레이터를 추가하려면 플러그인 모듈을 만들어야 하며, 이를 별도 패키지(예: plugins/search-backend-module-faq-snippets-collator)에 만드는 것이 가장 좋아요. yarn new 명령을 사용해서요.

  • Backstage 프로젝트 루트 디렉터리에 접근해 yarn new를 실행하세요.
  • 무엇을 만들 것인지 물어보면 backend-module을 선택하고 엔터를 누르세요.
  • 플러그인 ID로 search를, 모듈 ID로 faq-snippets-collator를 입력하세요.
  • 프로젝트의 "plugins" 디렉터리에 search-backend-module-faq-snippets-collator 폴더가 만들어져 있어야 해요.

2. 콜레이터 의존성 설치

모듈에서 일부 라이브러리를 사용할 거므로, 플러그인 모듈 의존성에 추가하세요.

# Create a new branch using Git command-linegit checkout -b tutorials/new-faq-snippets-collator# Install the package containing the interfaceyarn workspace @internal/backstage-plugin-search-backend-module-faq-snippets-collator add @backstage/plugin-search-common @backstage/plugin-search-backend-node

3. Backstage 앱 구성 사용

새 콜레이터는 프로젝트의 루트 폴더에 있는 Backstage app-config.yaml 파일의 구성에서 직접 이점을 얻을 수 있어요.

faq:  baseUrl: https://backstage.example.biz/faq-snippets

콜레이터에 대한 일정을 정의하는 것은 선택적이며, 아니면 콜레이터 팩토리 코드의 값으로 기본 설정돼요(5. 콜레이터 팩토리 구현 참고).

faq:  baseUrl: https://backstage.example.biz/faq-snippets  schedule:    # supports cron, ISO duration, "human duration" as used in code    frequency: { minutes: 30 }    # supports ISO duration, "human duration" as used in code    timeout: { minutes: 3 }

4. 콜레이터 문서 타입 정의

FAQ 항목에서 문서를 생성하기 시작하기 전에, 나중에 항목을 검색 결과로 표시하는 데 필요한 모든 정보를 포함하는 문서 타입을 먼저 정의해야 해요. 아까 설치한 @backstage/plugin-search-common 패키지에는 확장할 수 있는 IndexableDocument 타입이 있어요.

새 파일 plugins/search-backend-module-faq-snippets-collator/src/types.ts를 만들고 다음을 붙여넣으세요.

import { IndexableDocument } from '@backstage/plugin-search-common';export interface FaqSnippetDocument extends IndexableDocument {  answered_by: string;}

5. 콜레이터 팩토리 구현

FAQ가 URL https://backstage.example.biz/faq-snippets에서 다음 JSON 응답 형식으로 검색될 수 있다고 상상해 보세요.

{  "items": [    {      "id": 42,      "question": "What is The Answer to the Ultimate Question of Life, the Universe, and Everything?",      "answer": "Forty-two",      "user": "Deep Thought"    }  ]}

아래는 새 문서 타입을 사용한 FAQ 콜레이터 팩토리가 어떻게 보일 수 있는지에 대한 예시 구현이며, plugins/search-backend-module-faq-snippets-collator/src/factory.ts 파일에 배치돼요.

import { Readable } from 'stream';import {  LoggerService,  RootConfigService,} from '@backstage/backend-plugin-api';import { DocumentCollatorFactory } from '@backstage/plugin-search-common';import { FaqSnippetDocument } from './types';const DEFAULT_BASE_URL = 'https://backstage.example.biz/faq-snippets';export class FaqSnippetsCollatorFactory implements DocumentCollatorFactory {  public readonly type: string = 'faq-snippets';  private readonly baseUrl: string;  private readonly logger: LoggerService;  private constructor(options: { logger: LoggerService; baseUrl: string }) {    this.baseUrl = options.baseUrl;    this.logger = options.logger;  }  static fromConfig(    config: RootConfigService,    options: {      logger: LoggerService;    },  ) {    const baseUrl = config.getOptionalString('faq.baseUrl') ?? DEFAULT_BASE_URL;    return new FaqSnippetsCollatorFactory({ ...options, baseUrl });  }  async getCollator() {    return Readable.from(this.execute());  }  async *execute(): AsyncGenerator<FaqSnippetDocument> {    this.logger.info(`Fetching faq snippets from ${this.baseUrl}`);    const response = await fetch(this.baseUrl);    const data = await response.json();    for (const faq of data.items) {      yield {        title: faq.question,        location: `/faq-snippets/${faq.id}`,        text: faq.answer,        answered_by: faq.user,      };    }  }}

6. 콜레이터 플러그인 모듈 구현

이제 검색 백엔드 플러그인을 FAQ Snippets 콜레이터 팩토리와 연결해야 하므로, module.ts 파일을 아래 내용으로 교체하세요.

plugins/search-backend-module-faq-snippets-collator/src/module.ts

import {  coreServices,  createBackendModule,  readSchedulerServiceTaskScheduleDefinitionFromConfig,} from '@backstage/backend-plugin-api';import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';import { FaqSnippetsCollatorFactory } from './factory';export const searchFaqSnippetsCollatorModule = createBackendModule({  pluginId: 'search',  moduleId: 'faq-snippets-collator',  register(env) {    env.registerInit({      deps: {        config: coreServices.rootConfig,        logger: coreServices.logger,        scheduler: coreServices.scheduler,        indexRegistry: searchIndexRegistryExtensionPoint,      },      async init({ config, logger, scheduler, indexRegistry }) {        const defaultSchedule = {          frequency: { minutes: 10 },          timeout: { minutes: 15 },          initialDelay: { seconds: 3 },        };        const schedule = config.has('faq.schedule')          ? readSchedulerServiceTaskScheduleDefinitionFromConfig(              config.getConfig('faq.schedule'),            )          : defaultSchedule;        indexRegistry.addCollator({          schedule: scheduler.createScheduledTaskRunner(schedule),          factory: FaqSnippetsCollatorFactory.fromConfig(config, { logger }),        });      },    });  },});

위 조각에서 모듈이 등록되고, Backstage 백엔드가 초기화할 때 FAQ Snippets 콜레이터를 검색 인덱스 레지스트리에 추가해요. 이제 index.ts 파일에서 모듈을 기본으로 내보내겠어요.

plugins/search-backend-module-faq-snippets-collator/src/index.ts

export { searchFaqSnippetsCollatorModule as default } from './module';

7. 콜레이터 모듈 설치

새로 만든 모듈을 백엔드 패키지 의존성에 다음과 같이 추가해야 해요.

yarn --cwd packages/backend add @internal/backstage-plugin-search-backend-module-faq-snippets-collator

그 후 Backstage 백엔드 인스턴스에 모듈을 설치하세요.

packages/backend/src/index.ts

import { createBackend } from '@backstage/backend-defaults';//...const backend = createBackend();// Installing the search backend pluginbackend.add(import('@backstage/plugin-search-backend'));// Installing the newly created faq snippets collator modulebackend.add(  import(    '@internal/backstage-plugin-search-backend-module-faq-snippets-collator'  ),);//...backend.start();

8. 콜레이터 코드 테스트

구현이 예상대로 작동하는지 검증하려면 테스트를 추가해야 해요. 편의를 위해 사용자 지정 콜레이터를 통합할 수 있는 파이프라인을 에뮬레이트하는 TestPipeline 유틸리티가 있어요.

예시는 DefaultTechDocsCollatorFactory 테스트를 참고하세요.

Backstage 플러그인 모듈을 테스트하는 방법에 대한 문서도 확인할 수 있어요.

9. 로컬에서 콜레이터 실행

Backstage 프로젝트의 루트 폴더에서 yarn start를 실행하고 다음과 같은 로그를 찾으세요.

[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Registered scheduled task: search_index_faq_snippets, {"version":2,"cadence":"PT10M","initialDelayDuration":"PT3S","timeoutAfterDuration":"PT15M"} task=search_index_faq_snippets[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets via FaqSnippetsCollatorFactory documentType=faq-snippets[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Fetching faq snippets from https://backstage.example.biz/faq-snippets[backend]: YYYY-MM-DDTHH:MM:SS.000Z search info Collating documents for faq-snippets succeeded documentType=faq-snippets

콜레이터 작업이 시작되어 성공적으로 완료되었음을 의미해요. http://localhost:3000을 방문해 로그인하고 'All' 탭을 선택한 다음 검색 상자에 스니펫 제목 중 하나를 입력하세요.

스니펫에 대한 결과가 나타나야 해요.

10. 다른 사람이 플러그인 콜레이터를 발견할 수 있게 하기

콜레이터를 다른 도입자가 발견할 수 있게 하려면 검색에 통합된 플러그인 목록에 추가하세요.

플러그인에 검색 경험 구축

핵심 Search 플러그인은 앱 통합자가 전역 검색 경험을 구성할 수 있게 하는 컴포넌트와 확장을 제공하지만, 플러그인 안에서만 더 좁은 검색 경험을 원할 수도 있어요. 이것은 플러그인이 제공하는 문서에 초점을 맞춘 자동완성 스타일 검색 바(예: TechDocsSearch 컴포넌트)만큼 literal할 수도 있고, 페이지의 다른 것과 맥락적으로 관련된 링크 목록을 제시하는 위젯만큼 추상적일 수도 있어요.

검색 경험 개념

이러한 높은 수준의 개념을 알면 플러그인 내 검색 경험을 만드는 데 도움이 돼요.

  • 모든 검색 경험은 @backstage/plugin-search-react가 제공하는 <SearchContextProvider>로 감싸져야 해요. 이 컨텍스트는 검색 쿼리를 수행하고 결과를 표시하는 데 필요한 상태를 추적해요. 쿼리에 대한 입력이 갱신되면(예: term 또는 filter 값), 갱신된 쿼리가 실행되고 results가 새로고침돼요. 자세한 내용은 SearchContextValue를 확인하세요.
  • 위에서 언급한 상태는 @backstage/plugin-search-react도 내보내는 useSearch() 훅을 통해 수정 및/또는 소비될 수 있어요.
  • 더 literal한 검색 경험을 위해 재사용 가능한 컴포넌트를 플러그인에서 응집력 있는 경험으로 import하고 구성할 수 있어요(예: <SearchBar /> 또는 <SearchFilter.Checkbox />). Backstage의 storybook에서 그러한 모든 컴포넌트를 볼 수 있어요.

검색 경험 튜토리얼

다음 튜토리얼은 아직 플러그인의 의존성으로 갖고 있지 않을 수 있는 패키지와 플러그인을 사용해요. 사용하기 전에 추가해야 해요!

  • @backstage/plugin-search-react - 모든 프론트엔드 플러그인(여러분의 플러그인처럼!)에 걸쳐 공유되는 컴포넌트, 훅, 타입을 담은 패키지.
  • @backstage/plugin-search - 앱 통합자가 전역 검색 경험을 구성하는 데 사용하는 주요 검색 플러그인.
  • @backstage/core-components - Backstage에서 만든 다양한 경험에 유용한 일반 컴포넌트를 담은 패키지.

개선된 "404" 페이지 경험

사용자가 위젯을 관리할 수 있는 플러그인이 있다고 상상해 보세요. 아마 backstage.example.biz/widgets/{widgetName} 같은 URL에서 볼 수 있을 거예요. 어느 시점에 위젯 이름이 변경되고, 채팅 시스템, 위키, 브라우저 북마크에서 그 위젯 페이지로의 링크가 낡아져 오류나 404가 발생할 수 있어요.

깨진 페이지나 일반적인 "looks like someone dropped the mic" 404 페이지를 보여 주는 대신 관련 가능성이 있는 위젯 목록을 보여 주는 것은 어떨까요?

import { Link } from '@backstage/core-components';import { SearchResult } from '@backstage/plugin-search';import { SearchContextProvider } from '@backstage/plugin-search-react';export const Widget404Page = ({ widgetName }) => {  // Supplying this to <SearchContextProvider> runs a pre-filtered search with  // the given widgetName as the search term, focused on search result of type  // "widget" with no other filters.  const preFiltered = {    term: widgetName,    types: ['widget'],    filters: {},  };  return (    <SearchContextProvider initialState={preFiltered}>      {/* The <SearchResult> component allows us to iterate through results and          display them in whatever way fits best! */}      <SearchResult>        {({ results }) => (          {results.map(({ document }) => (            <Link to={document.location} key={document.location}>              {document.title}            </Link>          ))}        )}      <SearchResult>    </SearchContextProvider>  ););

모든 검색 경험이 사용자 입력을 요구하는 것은 아니에요! 보다시피, 사용자에게 입력 컨트롤을 반드시 주지 않고도 Backstage Search Platform의 프론트엔드 프레임워크를 활용하는 것이 가능해요.

간단한 검색 페이지

물론 플러그인에서 더 완전한 기능의 검색 경험을 제공하는 것도 가능해요. 가장 간단한 방법은 @backstage/plugin-search 패키지가 제공하는 재사용 가능한 컴포넌트를 활용하는 것이에요. 이렇게요.

import { useProfile } from '@internal/api';import {  Content,  ContentHeader,  PageWithHeader,} from '@backstage/core-components';import { SearchBar, SearchResult } from '@backstage/plugin-search';import { SearchContextProvider } from '@backstage/plugin-search-react';export const ManageMyWidgets = () => {  const { primaryTeam } = useProfile();  // In this example, note how we are pre-filtering results down to a specific  // owner field value (the currently logged-in user's team), but allowing the  // search term to be controlled by the user via the <SearchBar /> component.  const preFiltered = {    types: ['widget'],    term: '',    filters: {      owner: primaryTeam,    },  };  return (    <PageWithHeader title="Widgets Home">      <Content>        <ContentHeader title="All your Widgets and More" />        <SearchContextProvider initialState={preFiltered}>          <SearchBar />          <SearchResult>            {/* Render results here, just like above */}          </SearchResult>        </SearchContextProvider>      </Content>    </PageWithHeader>  );};

사용자 지정 검색 컨트롤 표면

@backstage/plugin-search가 제공하는 재사용 가능한 검색 컴포넌트가 충분하지 않다면 괜찮아요! 검색 컨텍스트의 다양한 부분을 제어하기 위해 자체 컴포넌트를 작성하는 데 사용할 수 있는 API가 마련되어 있어요.

import { useSearch } from '@backstage/plugin-search-react';import ChipInput from 'material-ui-chip-input';export const CustomChipFilter = ({ name }) => {  const { filters, setFilters } = useSearch();  const chipValues = filters[name] || [];  // When a chip value is changed, update the filters value by calling the  // setFilters function from the search context.  const handleChipChange = (chip, index) => {    // There may be filters set for other fields. Be sure to maintain them.    setFilters(prevState => {      const { [name]: filter = [], ...others } = prevState;      if (index === undefined) {        filter.push(chip);      } else {        filter.splice(index, 1);      }      return { ...others, [name]: filter };    });  };  return (    <ChipInput      value={chipValues}      onAdd={handleChipChange}      onDelete={handleChipChange}    />  );};

검색 컨텍스트를 조작하고 읽는 데 사용할 수 있는 메서드와 값에 대한 자세한 내용은 SearchContextValue 타입을 확인하세요.

일반적이고 재사용 가능한 것을 만든다면, Backstage Search Platform의 모든 사용자가 이점을 얻을 수 있도록 컴포넌트를 업스트림으로 기여하는 것을 고려하세요. 이슈와 pull request를 환영해요.

사용자 지정 검색 결과

Backstage 전반의 검색 결과는 목록으로 렌더링되어 목록 항목을 쉽게 커스터마이즈할 수 있어요. 기본 결과 목록 항목을 사용할 수 있지만, 플러그인만 알 수 있는 관련 정보를 표면화하는 사용자 지정 결과 목록 항목을 제공하는 데 가장 적합한 위치는 플러그인이에요.

아래 예시는 YourCustomSearchResult를 제목/텍스트 아래에 칩으로 렌더링할 수 있는 관련 tags를 포함하는 검색 결과 유형으로 상상해요.

import { Link } from '@backstage/core-components';import { useAnalytics } from '@backstage/core-plugin-api';import { ResultHighlight } from '@backstage/plugin-search-common';import { HighlightedSearchResultText } from '@backstage/plugin-search-react';type CustomSearchResultListItemProps = {  result: YourCustomSearchResult;  rank?: number;  highlight?: ResultHighlight;};export const CustomSearchResultListItem = (  props: CustomSearchResultListItemProps,) => {  const { title, text, location, tags } = props.result;  const analytics = useAnalytics();  const handleClick = () => {    analytics.captureEvent('discover', title, {      attributes: { to: location },      value: props.rank,    });  };  return (    <Link noTrack to={location} onClick={handleClick}>      <ListItem alignItems="center">        <Box flexWrap="wrap">          <ListItemText            primaryTypographyProps={{ variant: 'h6' }}            primary={              highlight?.fields?.title ? (                <HighlightedSearchResultText                  text={highlight.fields.title}                  preTag={highlight.preTag}                  postTag={highlight.postTag}                />              ) : (                title              )            }            secondary={              highlight?.fields?.text ? (                <HighlightedSearchResultText                  text={highlight.fields.text}                  preTag={highlight.preTag}                  postTag={highlight.postTag}                />              ) : (                text              )            }          />          {tags &&            tags.map((tag: string) => (              <Chip key={tag} label={`Tag: ${tag}`} size="small" />            ))}        </Box>      </ListItem>      <Divider />    </Link>  );};

<HighlightedSearchResultText> 컴포넌트의 선택적 사용으로 사용자의 검색 쿼리에 기반해 결과의 관련 부분을 강조할 수 있어요.

애널리틱스 참고: 앱 통합자가 Backstage 전반의 검색 경험을 추적하고 개선할 수 있으려면, 사용자가 언제 무엇을 검색하는지, 그리고 검색 후 무엇을 클릭하는지 이해하는 것이 중요해요. 사용자 지정 결과 컴포넌트를 제공할 때는 검색 애널리틱스 관례에 따라 그것을 계측하는 것이 플러그인 개발자로서의 책임이에요. 특히 다음을 지켜야 해요.

  • useAnalytics() 훅의 analytics.captureEvent 메서드를 사용해야 해요 (자세한 플러그인 애널리틱스 문서는 여기).
  • 검색 결과 항목 클릭을 나타내는 이벤트의 액션이 discover이고, subject가 클릭한 결과의 title이도록 보장해야 해요. 또한 to 속성은 결과의 location으로 설정되고, 이벤트의 value는 rank(props로 전달된)로 설정되어야 해요.
  • 위에서 언급한 captureEvent 메서드가 사용자가 링크를 클릭할 때 호출되도록 보장해야 해요. 또한 링크에 noTrack prop이 추가되도록 보장해야 해요 (이것은 이 사용자 지정 계측을 위해 기본 링크 클릭 추적을 비활성화해요).

사용자 지정 결과 목록 항목에 대한 다른 예시와 영감은 <StackOverflowSearchResultListItem> 또는 <CatalogSearchResultListItem> 컴포넌트를 확인하세요.

더 알아보기 (Learn more)