플러그인에 검색 통합
레거시 문서
출처: 문서
본문
레거시 문서
이 섹션은 레거시 플러그인 문서의 일부예요. 여기 설명된 백엔드 검색 콜레이터 패턴은 새 백엔드 시스템을 사용하며 여전히 유효해요. 프론트엔드 검색 경험 예시는 이전 프론트엔드 시스템 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메서드가 사용자가 링크를 클릭할 때 호출되도록 보장해야 해요. 또한 링크에noTrackprop이 추가되도록 보장해야 해요 (이것은 이 사용자 지정 계측을 위해 기본 링크 클릭 추적을 비활성화해요).
사용자 지정 결과 목록 항목에 대한 다른 예시와 영감은 <StackOverflowSearchResultListItem> 또는 <CatalogSearchResultListItem> 컴포넌트를 확인하세요.