LabHub

CBA — Backstage 인증 어소시에이트 · 앱 저장소와 플러그인 만들기 · 이론

저장소 구조가 곧 커스터마이즈 능력인 이유

LabHub 에서 이어서 보기

한 줄 요약

npx @backstage/create-app 이 만들어 주는 것은 Backstage 의 사본이 아니라 여러분 소유의 모노레포입니다. 커스터마이즈는 설정 한 줄을 켜는 일이 아니라 그 저장소의 코드를 고치고 빌드해 배포하는 일이고, 그래서 어디에 무엇이 있는지를 아는 것이 곧 커스터마이즈 능력입니다.

왜 이게 필요했나

포털을 도입한 팀이 처음 부딪히는 질문은 대개 이런 것들입니다.

이 질문들에는 공통점이 있습니다. 전부 저장소 구조를 알면 답이 나오고, 모르면 답이 안 나옵니다. Backstage 는 "설정으로 켜고 끄는 제품" 이 아니기 때문에, 화면에서 찾을 수 있는 답이 없습니다.

어떻게 동작하나

저장소는 워크스페이스 하나다

create-app 이 만든 트리는 yarn 워크스페이스를 쓰는 모노레포입니다.

package.json                루트. private: true, workspaces 에 packages/* 와 plugins/*packages/app/               프런트엔드 애플리케이션 (React)packages/backend/           백엔드 애플리케이션 (Node.js)plugins/<이름>/             사내에서 만든 플러그인app-config.yaml             설정

루트를 private: true 로 두는 이유는 이 패키지 자체를 배포하지 않기 때문입니다. 그리고 workspacesplugins/* 를 넣어야 사내 플러그인을 npm 레지스트리에 올리지 않고도 @internal/plugin-oncall 같은 이름으로 앱에서 가져다 쓸 수 있습니다.

package.json 의 backstage.role 이 열쇠다

각 패키지의 package.json 에는 backstage.role 이 있습니다. backstage-cli 는 이 값 하나를 보고 그 패키지를 어떻게 빌드하고 테스트할지 정합니다.

| role | 무엇인가 |
| --- | --- |
| frontend | packages/app 하나뿐인 프런트엔드 앱 |
| backend | packages/backend 하나뿐인 백엔드 앱 |
| frontend-plugin | 브라우저에서 도는 플러그인 |
| backend-plugin | 서버에서 도는 플러그인 |
| common-library · node-library · web-library | 플러그인이 아닌 공용 코드 |

역할을 잘못 적으면 빌드는 되는데 산출물이 이상해집니다. 백엔드 패키지의 main 이 빌드 산출물(dist/...)을 가리켜야 하는 것도 같은 이유입니다.

새 백엔드 시스템은 등록만 한다

packages/backend/src/index.ts 가 백엔드의 전부입니다.

const backend = createBackend();backend.add(import('@backstage/plugin-catalog-backend'));backend.add(import('@backstage/plugin-catalog-backend-module-github'));backend.start();

예전 백엔드는 플러그인마다 라우터를 손으로 만들고 로거·설정·데이터베이스를 직접 넘겨야 했습니다. 새 백엔드 시스템에서는 등록만 하면 필요한 것을 주입받습니다. 여기서 플러그인과 모듈의 차이 가 시험에 나옵니다.

이름에 -module- 이 들어 있으면 그것은 단독으로 동작하지 않고 짝이 되는 플러그인이 함께 등록되어 있어야 합니다.

프런트엔드 플러그인은 세 파일에서 시작한다

src/routes.ts   createRouteRef 로 라우트 참조를 만들어 내보낸다src/plugin.ts   createPlugin 으로 플러그인을 만들고, 페이지를 확장으로 제공한다src/index.ts    바깥에 공개할 것만 다시 내보낸다

라우트 참조를 굳이 별도 파일에 두는 데는 이유가 있습니다. 페이지 컴포넌트가 플러그인을 참조하고 플러그인이 다시 컴포넌트를 참조하면 순환 임포트가 생깁니다. 참조만 담은 파일을 따로 두면 그 고리가 끊깁니다.

그리고 라우트를 가진 페이지는 createRoutableExtension 으로 만들고 mountPoint 에 라우트 참조를 겁니다. 컴포넌트를 지연 임포트로 넘기기 때문에 그 플러그인을 실제로 열기 전까지는 코드가 내려오지 않습니다.

index.ts 만 공개 진입점으로 두는 것도 규율입니다. 앱이 플러그인 내부 파일을 직접 가져가기 시작하면 플러그인 내부 구조를 다시는 바꿀 수 없게 됩니다.

엔티티 페이지에 탭을 붙이려면 두 곳을 고친다

<EntitySwitch>  <EntitySwitch.Case if={isKind('component')}>    <EntityLayout>      <EntityLayout.Route path="/oncall" title="On-call">        <OncallPage />      </EntityLayout.Route>

EntitySwitchisKind 로 kind 별로 다른 페이지를 보여 주고, EntityLayout.Route 로 탭을 답니다. 흔한 실수가 여기서 나옵니다. 탭만 붙이고 App.tsx 의 라우트를 등록하지 않으면 탭은 보이는데 눌렀을 때 빈 화면이 뜹니다. 화면에는 오류가 없어서 원인을 찾기 어렵습니다.

개발 워크플로

yarn install --immutable 은 잠금 파일을 고치지 않겠다는 뜻입니다. CI 에서 이 플래그를 빼면 의존성이 조용히 올라가서, 어제 통과한 커밋이 오늘 다르게 빌드됩니다. 그다음은 타입 검사, 빌드, 테스트 순서입니다. 이 순서가 중요한 이유는 타입 오류 하나가 다른 모든 출력을 가리기 때문입니다.

현장에서 만나는 모습

이 저장소에도 같은 경계가 있습니다. logos.js, i18n.js, backend/app/grader_paths.py 는 생성물이라 손으로 고치면 다음 생성 때 사라집니다. 고쳐야 할 것은 생성기 쪽입니다. Backstage 앱도 정확히 같은 성격의 경계를 갖습니다. packages/app/src 는 내 코드이고 node_modules 안의 플러그인은 남의 코드입니다. 경계를 흐려 남의 코드를 직접 고치기 시작하면 업그레이드가 불가능해집니다.

또 하나. 이 저장소에서 여럿이 병렬로 고치던 날, 타입스크립트 문법 오류 하나 때문에 검사기가 나머지를 아예 보지 않고 끝난 적이 있습니다. "검사기가 아무것도 못 찾았다" 가 사실은 "아무것도 검사되지 않았다" 였습니다. 포털 저장소도 규모가 커지면 같은 일이 생깁니다. CI 에서 타입 검사를 빌드보다 먼저 세워 두는 이유가 이것입니다.

다음 실습에서 할 것

/root/cba-app/ 에 create-app 이 만들어 낼 저장소 구조를 직접 세웁니다. 워크스페이스 루트와 앱 두 개, 사내 플러그인 두 개를 만들고 각 패키지에 역할을 붙입니다. 그다음 새 백엔드 시스템 방식으로 플러그인과 모듈을 등록하고, 프런트엔드 플러그인의 세 파일을 작성하고, 엔티티 페이지에 탭을 답니다. 마지막으로 패키지 인벤토리를 뽑고 CI 게이트를 세웁니다. 노드 의존성을 받을 수 없는 환경이므로 빌드는 하지 않고 구조와 배선만 다룹니다.