Search How-To 가이드
Search How-To 가이드 (이전 프론트엔드 시스템)
이전 프론트엔드 시스템을 사용하는 Backstage 앱에서 Search를 구현·사용자 지정하는 방법들을 정리한 가이드예요.
출처: 문서
본문
:::info 이 문서는 여전히 이전 프론트엔드 시스템을 사용하는 Backstage 앱을 위한 것이에요. 앱이 새 프론트엔드 시스템을 사용한다면 현재 가이드를 읽으세요. :::
나만의 Search API 구현하는 방법
Search 플러그인은 기본적으로 하나의 주요 API 구현을 제공해요. 바로 검색 결과를 조회하기 위해 search-backend와 통신하는 SearchApi예요.
이 API를 직접 구현해야 하는 경우가 있을 수 있어요. 예를 들어 통신하고 싶은 나만의 검색 백엔드가 있을 때, API를 자신의 요구에 맞게 사용자 지정해야 할 수 있어요. 이 가이드의 목적은 두 단계로 이 과정을 안내하는 것이에요.
- 필요에 따라
SearchApi인터페이스를 구현해요.
export class SearchClient implements SearchApi { // your implementation}
App.tsx에서ApiFactories를 사용해 API ref인searchApiRef를 새로 구현한 API로 재정의해요. App API에 대해 자세히 읽어보세요.
const app = createApp({ apis: [ // SearchApi createApiFactory({ api: searchApiRef, deps: { discovery: discoveryApiRef }, factory({ discovery }) { return new SearchClient({ discoveryApi: discovery }); }, }), ],});
Software Catalog 또는 TechDocs 인덱스의 필드를 사용자 지정하는 방법
때로는 catalog 콜레이터에서 어떤 데이터가 검색 인덱스로 들어가는지 제어하거나, 특정 kind에 대해 데이터를 사용자 지정하고 싶을 수 있어요. DefaultCatalogCollatorFactory에 entityTransformer 콜백을 전달하면 쉽게 처리할 수 있어요. 이 동작은 DefaultTechDocsCollatorFactory에서도 가능해요. 기본 동작을 단순히 수정하거나 완전히 새로운 문서(여전히 필요한 기본 구조를 따라야 함)를 작성할 수도 있어요.
authorization과 location은 entityTransformer로 수정할 수 없으며, location은 locationTemplate을 통해서만 수정할 수 있어요.
packages/backend/src/plugins/search.ts
const catalogEntityTransformer: CatalogCollatorEntityTransformer = ( entity: Entity,) => { if (entity.kind === 'SomeKind') { return { // customize here output for 'SomeKind' kind }; } return { // and customize default output ...defaultCatalogCollatorEntityTransformer(entity), text: 'my super cool text', };};indexBuilder.addCollator({ collator: DefaultCatalogCollatorFactory.fromConfig(env.config, { discovery: env.discovery, tokenManager: env.tokenManager, entityTransformer: catalogEntityTransformer, }),});const techDocsEntityTransformer: TechDocsCollatorEntityTransformer = ( entity: Entity,) => { return { // add more fields to the index tags: entity.metadata.tags, };};const techDocsDocumentTransformer: TechDocsCollatorDocumentTransformer = ( doc: MkSearchIndexDoc,) => { return { // add more fields to the index bost: doc.boost, };};indexBuilder.addCollator({ collator: DefaultTechDocsCollatorFactory.fromConfig(env.config, { discovery: env.discovery, tokenManager: env.tokenManager, entityTransformer: techDocsEntityTransformer, documentTransformer: techDocsDocumentTransformer, }),});
검색 결과 하이라이팅 스타일을 사용자 지정하는 방법
검색 결과에서 일치하는 용어의 기본 하이라이팅 스타일은 <mark> HTML 태그에 대한 브라우저 기본 스타일이에요. 하이라이트된 용어가 어떻게 보이는지 사용자 지정하려면 Backstage의 '앱 UI 사용자 지정' 가이드를 따라 원하는 스타일로 오버라이드를 만들 수 있어요.
예를 들어 새 MUI V4+V5 통합 테마(unified theming) 방법을 사용하면 다음 결과로 하이라이트된 단어가 굵게(bold) 표시되고 밑줄이 그어져요.
import { createBaseThemeOptions, createUnifiedTheme, palettes, UnifiedTheme,} from '@backstage/theme';export const myLightTheme: UnifiedTheme = createUnifiedTheme({ ...createBaseThemeOptions({ palette: palettes.light, }), defaultPageTheme: 'home', components: { /** @ts-ignore This is temporarily necessary until MUI V5 transition is completed. */ BackstageHighlightedSearchResultText: { styleOverrides: { highlight: { color: 'inherit', backgroundColor: 'inherit', fontWeight: 'bold', textDecoration: 'underline', }, }, }, },});
const app : BackstageApp = createApp({ ... themes: [{ id: 'my-light-theme', title: 'Light Theme', variant: 'light', icon: <LightIcon />, Provider: ({ children }) => (<UnifiedThemeProvider theme={myLightTheme} children={children } />) }]});
물론 다크 테마를 원한다면 그것도 제공해야 해요.
확장을 사용해 검색 결과를 렌더링하는 방법
검색 결과용 확장은 검색 결과 항목을 렌더링하는 데 사용되는 컴포넌트를 사용자 지정할 수 있게 해줘요. 나만의 검색 결과 항목 확장을 제공하거나 플러그인 패키지가 제공하는 것을 사용할 수 있어요.
1. 플러그인 패키지에서 확장 제공하기
참고: 검색 항목 렌더러를 사용 가능하게 만들려면 plugin.provide() 함수를 사용해야 해요. 표준 MUI 테이블 등에서 목록을 렌더링하는 것과 달리, 단순히 <SearchResult /> 컴포넌트에 렌더링 함수를 제공할 수는 없어요.
아래 예시를 사용해 검색 결과 항목으로 사용될 확장을 제공할 수 있어요.
plugins/your-plugin/src/plugin.ts
import { createPlugin } from '@backstage/core-plugin-api';import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });export const YourSearchResultListItemExtension = plugin.provide( createSearchResultListItemExtension({ name: 'YourSearchResultListItem', component: () => import('./components').then(m => m.YourSearchResultListItem), }),);
목록 항목이 props를 받는다면 SearchResultListItemExtensionProps를 컴포넌트 특정 props로 확장할 수 있어요.
export const YourSearchResultListItemExtension: ( props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>,) => JSX.Element | null = plugin.provide( createSearchResultListItemExtension({ name: 'YourSearchResultListItem', component: () => import('./components').then(m => m.YourSearchResultListItem), }),);
추가로, 결과를 받아 자신의 확장이 그 결과를 렌더링하는 데 사용되어야 하는지 여부를 반환하는 predicate 함수를 정의할 수 있어요.
plugins/your-plugin/src/plugin.ts
import { createPlugin } from '@backstage/core-plugin-api';import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });export const YourSearchResultListItemExtension = plugin.provide( createSearchResultListItemExtension({ name: 'YourSearchResultListItem', component: () => import('./components').then(m => m.YourSearchResultListItem), // Only results matching your type will be rendered by this extension predicate: result => result.type === 'YOUR_RESULT_TYPE', }),);
앱 안에서 사용할 수 있도록 플러그인의 index.ts를 통해 새 확장을 내보내는 것을 잊지 마세요.
plugins/your-plugin/src/index.ts
export { YourSearchResultListItem } from './plugin.ts';
자세한 내용은 createSearchResultListItemExtension API reference를 참고하세요.
2. SearchPage에서 사용자 지정 검색 결과 확장
plugin.provide() 함수로 항목 렌더러를 노출했다면, 이제 기본 검색 항목 렌더러를 재정의하고 <SearchResult> 컴포넌트에 어떤 렌더러를 사용할지 알려줄 수 있어요. 렌더러의 순서가 중요하다는 점에 주의하세요! predicate 함수를 통해 일치하는 첫 번째 것이 사용돼요.
SearchPage를 사용자 지정하는 예시는 다음과 같아요.
packages/app/src/components/searchPage.tsx
import { Grid, Paper } from '@material-ui/core';import BuildIcon from '@material-ui/icons/Build';import { Page, Header, Content, DocsIcon, CatalogIcon,} from '@backstage/core-components';import { SearchBar, SearchResult } from '@backstage/plugin-search-react';// Your search result item extensionimport { YourSearchResultListItem } from '@backstage/your-plugin';// Extensions provided by other plugin developersimport { ToolSearchResultListItem } from '@backstage/plugin-explore';import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';// This example omits other components, like filter and paginationconst SearchPage = () => ( <Page themeId="home"> <Header title="Search" /> <Content> <Grid container direction="row"> <Grid item xs={12}> <Paper> <SearchBar /> </Paper> </Grid> <Grid item xs={12}> <SearchResult> <YourSearchResultListItem /> <CatalogSearchResultListItem icon={<CatalogIcon />} /> <TechDocsSearchResultListItem icon={<DocsIcon />} /> <ToolSearchResultListItem icon={<BuildIcon />} /> </SearchResult> </Grid> </Grid> </Content> </Page>);export const searchPage = <SearchPage />;
중요: 기본 결과 항목 확장(predicate가 없는 것)은 마지막 자식으로 배치해야 다른 확장이 렌더링하는 결과와 일치하지 않을 때만 사용될 수 있어요. 기본이 아닌 확장이 지정되면 DefaultResultListItem 컴포넌트가 사용돼요.
2. SidebarSearchModal에서 사용자 지정 검색 결과 확장
SidebarSearchModal 컴포넌트를 사용하고 있을 수 있어요. 이 경우 이 컴포넌트에서 검색 항목을 다음과 같이 사용자 지정할 수 있어요.
packages/app/src/components/Root/Root.tsx
import { SidebarSearchModal } from '@backstage/plugin-search';...export const Root = ({ children }: PropsWithChildren<{}>) => { const styles = useStyles(); return <SidebarPage> <Sidebar> ... <SidebarSearchModal resultItemComponents={[ /* Provide a custom Extension search item renderer */ <CustomSearchResultListItem icon={<CatalogIcon />} />, /* Provide an existing search item renderer */ <TechDocsSearchResultListItem icon={<DocsIcon />} /> ]} /> ... </Sidebar> {children} </SidebarPage>;};
3. 사용자 지정 SearchModal에서 사용자 지정 검색 결과 확장
SearchModal을 완전히 사용자 지정했다고 가정하고, 확장으로 결과를 렌더링하는 예시는 다음과 같아요.
packages/app/src/components/searchModal.tsx
import { DialogContent, DialogTitle, Paper } from '@material-ui/core';import BuildIcon from '@material-ui/icons/Build';import { DocsIcon, CatalogIcon } from '@backstage/core-components';import { SearchBar, SearchResult } from '@backstage/plugin-search-react';// Your search result item extensionimport { YourSearchResultListItem } from '@backstage/your-plugin';// Extensions provided by other plugin developersimport { ToolSearchResultListItem } from '@backstage/plugin-explore';import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => ( <> <DialogTitle> <Paper> <SearchBar /> </Paper> </DialogTitle> <DialogContent> <SearchResult onClick={toggleModal}> <CatalogSearchResultListItem icon={<CatalogIcon />} /> <TechDocsSearchResultListItem icon={<DocsIcon />} /> <ToolSearchResultListItem icon={<BuildIcon />} /> {/* As a "default" extension, it does not define a predicate function, so it must be the last child to render results that do not match the above extensions */} <YourSearchResultListItem /> </SearchResult> </DialogContent> </>);
결과 항목 확장을 받아들이는 더 구체적인 검색 결과 레이아웃 컴포넌트도 있어요. 해당 문서를 확인하세요: SearchResultList와 SearchResultGroup.