LabHub
배우기 러닝패스 코스

CBA — Backstage 인증 어소시에이트 · 개발 워크플로 — 만들고, 띄우고, 타입 검사하고, 이미지로 굽고, 테마를 입힌다 · 이론

yarn tsc 가 만드는 것과 Dockerfile 이 두 번 tar 를 푸는 이유

LabHub 에서 이어서 보기

한 줄 요약

Backstage 앱은 npx @backstage/create-app@latest 로 만들고 yarn start 로 프론트엔드(3000)와 백엔드(7007)를 함께 띄웁니다. yarn tsc 는 저장소 전체를 한 컴파일 단위로 타입 검사해 dist-types/ 에 결과를 남기고, yarn build:backendpackages/backend/dist/skeleton.tar.gzbundle.tar.gz 두 아카이브를 만듭니다. Docker 이미지는 이 둘을 순서대로 풀어 의존성 설치를 캐시합니다. 테마는 packages/app/src/App.tsxcreateAppthemes 로 등록하고, 플러그인의 React 컴포넌트는 plugins/<id>/src/components/ 에 두고 plugin.ts 가 확장(extension)으로 내보냅니다. 출처는 [시작하기](https://backstage.io/docs/getting-started/), [빌드 시스템](https://backstage.io/docs/tooling/cli/build-system), [Docker 이미지 빌드](https://backstage.io/docs/deployment/docker), [UI 커스터마이즈](https://backstage.io/docs/conf/user-interface), [플러그인 구조](https://backstage.io/docs/plugins/structure-of-a-plugin) 문서입니다.

왜 이게 필요했나

Backstage 는 제품이 아니라 프레임워크라서, 여러분의 조직에 맞춘 앱 저장소가 곧 산출물입니다. 그 저장소는 Yarn 워크스페이스로 묶인 모노레포이고, 프론트엔드·백엔드·플러그인이 각각 패키지입니다. 이 구조 때문에 "빌드" 가 한 가지가 아닙니다 — 타입 검사, 패키지 빌드, 프론트엔드 번들, 백엔드 번들, 그리고 컨테이너 이미지가 각각 다른 도구와 산출물을 가집니다. 시험이 이 워크플로를 한 도메인(24%)으로 묶어 묻는 이유는, 어느 단계가 무엇을 만들고 어느 단계가 그것을 소비하는지를 모르면 CI 파이프라인도 Dockerfile 도 읽을 수 없기 때문입니다.

어떻게 동작하나

만들고 띄우기

npx @backstage/create-app@latest 는 앱 이름을 묻고 그 이름의 디렉터리에 파일을 생성한 뒤 yarn installyarn tsc 까지 실행합니다. 생성물의 뼈대는 이렇습니다.

app├── app-config.yaml      # 앱 설정├── catalog-info.yaml    # 카탈로그 엔티티 기술자├── package.json         # 루트. 여기에 npm 의존성을 넣지 말 것└── packages    ├── app              # 프론트엔드 앱    └── backend          # 백엔드

yarn start 는 프론트엔드와 백엔드를 [0]·[1] 두 프로세스로 한 창에서 띄우고, "Rspack compiled successfully" 가 보이면 http://localhost:3000 에서 앱을 볼 수 있습니다. 시스템이 격리되어 있다면 3000 과 7007 포트를 열어야 합니다. 이 독립 설치는 인메모리 SQLite 와 데모 데이터를 쓰는 평가용이며 운영용이 아닙니다. 요구 사양은 Node.js Active LTS(문서는 22 또는 24를 권함), Yarn 4.4.1(corepack enableyarn set version 4.4.1), 디스크 20GB, 메모리 6GB 입니다.

타입 검사 — 저장소 전체가 한 단위

빌드 시스템 문서가 가장 강조하는 특징은 프로젝트 전체가 하나의 TypeScript 컴파일 단위라는 점입니다. 패키지마다 쪼개면 설정이 복잡해지고 전체 타입 검사가 한 자릿수 배 느려지기 때문입니다. 그래서 각 패키지의 진입점은 TypeScript 소스를 가리킵니다. 로컬에서는 증분(incremental) 검사가 기본이고 결과가 저장소 루트의 dist-types/ 에 쌓입니다. 또 node_modules 안의 라이브러리 타입 검사를 건너뛰어 속도를 얻는데, CI 에서는 이 두 최적화를 끈 yarn tsc:full 을 쓰라고 권합니다. dist-types/ 는 단순한 캐시가 아닙니다 — package build 가 만드는 타입 선언 파일의 진입점이 이 폴더이므로, 타입 선언이 있는 패키지를 빌드하기 전에 반드시 타입 검사를 먼저 돌려야 합니다.

세 가지 빌드와 그 산출물

| 명령 | 도구 | 산출물 | 대상 |
| --- | --- | --- | --- |
| backstage-cli package build | Rollup | 패키지의 dist/ 에 CJS·ESM·타입 선언 | frontend·backend 역할을 뺀 패키지(플러그인·라이브러리) |
| 프론트엔드 번들 | Webpack(문서 기준; 시작 로그에는 Rspack 이 보임) | dist/ 의 일반 자산(짧은 캐시) + dist/static/ 의 해시 자산(긴 캐시) | packages/app |
| yarn build:backend / backend:bundle | 자체 수집 | packages/backend/dist/bundle.tar.gz + skeleton.tar.gz | packages/backend |

백엔드 번들은 Webpack 을 쓰지 않습니다. 백엔드 패키지와 그 로컬 의존성을 모노레포와 같은 디렉터리 배치로 모아 bundle.tar.gz 로 묶고, 루트 package.jsonyarn.lock 도 넣습니다. 그 옆의 skeleton.tar.gz 는 같은 배치인데 package.json 파일만 들어 있습니다. 이 둘로 나눈 이유가 Dockerfile 을 읽는 열쇠입니다 — 스켈레톤만으로도 yarn install 을 할 수 있으므로, 소스가 바뀌어도 의존성이 그대로면 설치 레이어가 캐시됩니다. 번들을 만들기 전에 백엔드 패키지들이 먼저 빌드되어 있어야 하며, --build-dependencies 플래그를 주면 번들 명령이 대신 빌드합니다.

Docker 이미지 — host build 와 multi-stage

Docker 문서는 두 방식을 나누고 첫째를 권합니다.

Host build: 빌드의 대부분을 Docker 밖(호스트나 CI)에서 합니다. 순서는 yarn install --immutableyarn tscyarn build:backend 이고, 그 다음 packages/backend/Dockerfile 로 이미지를 만듭니다. 이 Dockerfile 은 저장소 루트를 빌드 컨텍스트로 실행해야 루트의 yarn.lock·package.json 에 닿습니다.

docker image build . -f packages/backend/Dockerfile --tag backstagedocker run -it -p 7007:7007 backstage

create-app 이 넣어 주는 Dockerfile 의 흐름은 이렇습니다. node:24-trixie-slim 위에서 USER node 로 내려간 뒤, .yarn·.yarnrc.yml·backstage.json 을 복사하고, yarn.lock·package.json·skeleton.tar.gz 를 복사해 풀고 yarn workspaces focus --all --production 으로 운영 의존성만 설치하고, 마지막에 bundle.tar.gzapp-config*.yaml 을 복사해 풉니다. 시작 명령은 node packages/backend --config app-config.yaml --config app-config.production.yaml 입니다. 함께 생성되는 .dockerignorepackages/*/src·plugins·node_modules·*.local.yaml 을 빼서 컨텍스트를 줄입니다 — 소스가 아니라 빌드 산출물을 넣는 방식이기 때문입니다. 문서는 호스트의 Node 버전이 베이스 이미지와 같아야 네이티브 모듈이 런타임에 깨지지 않는다고 경고합니다.

Multi-stage build: 전체 빌드를 Docker 안에서 합니다. 보통 더 느리지만 빌드 환경에 Docker 안의 빌드가 필요하거나 다른 제약이 있을 때 씁니다. 세 단계로 나뉩니다 — 1단계는 findpackage.json 만 남겨 yarn install 캐시용 스켈레톤 레이어를 만들고, 2단계는 yarn install --immutableyarn tscyarn --cwd packages/backend build 로 호스트 빌드와 같은 일을 한 뒤 두 아카이브를 풀어 두고, 3단계가 최종 이미지를 만듭니다. 이 방식의 .dockerignore 는 소스에 접근해야 하므로 host build 의 것과 달리 dist-types·node_modules·packages/*/dist 같은 산출물만 뺍니다.

두 방식 모두 전제가 있습니다 — 기본 Guest 인증 공급자는 컨테이너 환경용이 아니므로 인증 공급자를 먼저 세우고, Postgres 를 준비해야 합니다. 프론트엔드를 따로 서빙하려면 @backstage/plugin-app-backend 를 백엔드에서 빼야 하는데, 그러면 백엔드가 프론트엔드 설정을 주입해 주는 기능을 잃습니다. 빌드가 이상하면 --progress=plain--no-cache 를 붙여 보라는 조언도 있습니다.

테마 — Material UI 와 Backstage UI 두 체계

UI 커스터마이즈 문서는 지금 Backstage 에 두 UI 체계가 공존한다고 설명합니다. 원래의 Material UI(MUI) 는 JS 기반 테마이고 UnifiedThemeProvider 로 적용하며 대부분의 기존 플러그인이 씁니다. 새 Backstage UI(BUI) 는 CSS 변수와 토큰 기반이고 클래스 이름이 bui- 로 시작합니다. 어느 쪽을 고쳐야 할지는 컴포넌트의 클래스 이름을 보고 정합니다.

등록 자리는 하나입니다 — packages/app/src/App.tsxcreateAppthemes 배열을 줍니다. 각 항목은 id, 설정 화면에 보일 title, light 또는 darkvariant(body 에 data-theme-mode 속성으로 들어감), icon, 그리고 MUI 를 위한 Provider 입니다. 이 배열은 기본 테마를 대체하므로 light 와 dark 를 모두 넣어야 하고, 기본값이 필요하면 @backstage/themethemes.light·themes.dark 를 쓰면 됩니다.

import { createBaseThemeOptions, createUnifiedTheme, palettes } from '@backstage/theme';export const lightTheme = createUnifiedTheme({  ...createBaseThemeOptions({ palette: palettes.light }),  fontFamily: 'Comic Sans MS',  defaultPageTheme: 'home',});

MUI 테마는 createUnifiedThemecreateBaseThemeOptions({ palette }) 를 펼쳐 넣어 만듭니다. palettes.light 를 펼친 뒤 primary.main·navigation.background 같은 값을 덮고, pageThemegenPageTheme({ colors, shape: shapes.wave }) 로 페이지 헤더 색과 모양을 정하며, typographydefaultTypography 를 펼쳐 h1 만 바꾸는 식으로 부분 재정의합니다. 사용자 정의 폰트는 componentsMuiCssBaseline styleOverrides@font-face 를 넣습니다. BUI 쪽은 packages/app/src/styles.cssApp.tsx 에서 import 하고, :root[data-theme-mode='light']·[data-theme-mode='dark'] 아래에 --bui-bg-app·--bui-fg-primary 같은 변수를 덮어씁니다.

React 컴포넌트는 플러그인 어디에 들어가나

yarn new 에서 frontend-plugin 을 고르면 플러그인 패키지가 생기고 앱에 자동으로 연결됩니다 — app/package.json 의 의존성과 app/src/App.tsx 의 import 가 함께 추가되어, 앱이 떠 있으면 http://localhost:3000/my-plugin 에서 바로 보입니다. 플러그인은 package.jsonsrc/ 를 가진 별도 패키지라서 npm 으로 배포할 수 있고 앱 전체를 띄우지 않고 dev/ 디렉터리의 설정으로 혼자 띄울 수도 있습니다.

plugins/my-plugin/  dev/index.ts                      # 플러그인만 따로 띄우는 설정  src/    components/ExampleComponent/    # 페이지 컴포넌트 (React)    components/ExampleFetchComponent/  # 외부 API 를 부르고 MUI 표로 그림    plugin.ts                       # createPlugin + createRoutableExtension    routes.ts                       # rootRouteRef    index.ts                        # 폴더 단위 export

plugin.ts 가 배선의 핵심입니다. createPlugin({ id, routes: { root: rootRouteRef } }) 로 플러그인을 만들고, createRoutableExtension({ name, component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), mountPoint: rootRouteRef })plugin.provide() 로 감싸 내보냅니다. 앱은 이 확장을 import 해서 라우트에 붙입니다. 즉 React 컴포넌트 변경은 src/components/ 안에서 하고, 새 페이지를 앱에 노출하려면 plugin.ts 에서 확장으로 내보내야 합니다. 이 문서는 레거시 프론트엔드 시스템 기준이며, 새 프론트엔드 시스템에서는 plugin.ts 의 배선이 크게 다르다는 안내가 붙어 있습니다.

현장에서 만나는 모습

CI 에서 이미지 빌드가 "packages/backend/dist/skeleton.tar.gz not found" 로 실패한 팀이 있었습니다. Dockerfile 이 packages/backend/ 를 컨텍스트로 실행되고 있었고, 그 전에 yarn build:backend 도 빠져 있었습니다. host build 는 호스트에서 만든 산출물을 이미지에 넣는 방식이라, 빌드 순서와 컨텍스트 루트가 곧 계약입니다.

다른 팀은 로컬에서 yarn tsc 가 통과하는데 CI 만 실패했습니다. 로컬은 증분 검사와 라이브러리 타입 건너뛰기가 켜져 있고 CI 는 tsc:full 이었습니다. 문서가 CI 에 tsc:full 을 권하는 이유가 바로 이 차이입니다.

다음 퀴즈에서 확인할 것

퀴즈에서는 yarn start 가 띄우는 것과 포트, yarn tsc 의 산출물과 dist-types/ 가 필요한 이유, skeleton.tar.gzbundle.tar.gz 의 차이, host build 와 multi-stage build 의 차이, createAppthemes 항목, 그리고 plugin.ts 의 역할을 묻습니다.