플러그인과 Facade 확장
필요한 기능을 조합하고 공개 API로 콘텐츠와 업무 UI를 확장합니다.
Univer는 플러그인으로 제품과 기능을 구성하고 Facade API로 콘텐츠 작업을 애플리케이션에 공개합니다. 필요한 엔진, 기능, UI를 선택하거나 업무 로직을 플러그인으로 묶고 타입이 제공되는 메서드로 앱과 Agent 도구에서 호출할 수 있습니다.
필요한 콘텐츠 기능 조합하기
Preset은 빠른 도입을 위해 미리 조합한 플러그인 모음입니다. Plugin 모드에서는 호스트가 제품, 기능, 설정을 명시적으로 선택합니다. 두 방식은 같은 플러그인 메커니즘을 사용합니다.
| 계층 | 역할 | 주요 구성 요소 |
|---|---|---|
| 제품과 콘텐츠 모델 | Unit, 콘텐츠 구조, 기본 작업을 관리합니다. | Sheet, Doc, Slide, Relational Table, Canvases, PDF 제품 플러그인. |
| 기능 플러그인 | 특정 분야의 기능을 추가합니다. | 수식, 필터, 차트, 댓글 등. |
| UI와 렌더링 플러그인 | 화면과 상호작용 진입점을 제공합니다. | Canvas 렌더링, 메뉴, 도구 모음, 단축키, 컴포넌트. |
| Facade 확장 | 설치된 기능의 공개 호출 인터페이스를 제공합니다. | 통합 문서, 범위, 문서, 객체 작업과 의미 기반 이벤트. |
제품 로직과 UI를 분리하면 지원되는 콘텐츠 기능을 Node.js, Worker, 브라우저에서 재사용할 수 있습니다. Headless 호스트는 필요한 콘텐츠 플러그인을 등록합니다. 메뉴, DOM, 상호작용 API에는 해당 UI 환경이 필요합니다.
의존성, 생명 주기, 리소스 소유권
플러그인은 @DependentOn(...)으로 직접 의존성을 선언하고 런타임은 이를 해석해 생성 순서를 조정합니다. 누락된 의존 플러그인은 기본 설정으로 등록됩니다. 사용자 설정이 필요하다면 의존 플러그인을 먼저 설정하고 등록해야 합니다. 이 과정이 패키지를 설치하거나 스타일, 로케일, Facade 진입점, Worker를 가져오지는 않습니다.
플러그인 내부에서는 의존성 주입(DI)으로 Service, Controller, 모델을 공개 서비스 식별자를 통해 연결합니다. 지원되는 설정 또는 주입 경계에서 플랫폼 구현을 교체할 수 있으므로 업무 로직이 모든 의존성을 직접 생성하고 관리할 필요가 없습니다.
| 생명 주기 | 적합한 작업 |
|---|---|
onStarting() | 의존성, 명령, 컨트롤러를 등록합니다. 아직 생성되지 않은 Unit이나 DOM을 읽지 않습니다. |
onReady() | 해당 콘텐츠가 준비된 후 모델에 의존하는 로직을 초기화합니다. |
onRendered() | 렌더러, Canvas, DOM이 필요한 확장을 초기화합니다. |
onSteady() | 첫 화면에 필수적이지 않은 작업을 지연합니다. |
명령 등록, 이벤트 구독, 단축키, 렌더링 모듈은 소유자가 관리합니다. disposable을 반환하는 등록은 disposeWithMe()로 관리해 플러그인이나 컨트롤러와 함께 해제합니다. 현재 mergeMenu()는 disposable이 아닌 void를 반환합니다.
서로 다른 진입점에서 콘텐츠 변경 메커니즘 공유하기
도구 모음, 단축키, 업무 API는 같은 업무 명령을 호출할 수 있습니다. Agent 호스트도 필요한 기능을 설치한 후 같은 진입점을 재사용합니다. 명령 시스템은 업무 의도, 영구 콘텐츠 변경, 일시적 상호작용을 구분합니다.
| 유형 | 역할 | 예시 |
|---|---|---|
COMMAND | 대상을 검증하고 업무 단계, Mutation, Operation을 구성합니다. | 매출 보고서 작성, 행 삭제. |
MUTATION | 명시적이고 직렬화 가능한 매개변수로 모델을 변경하며 재생과 해당 협업 처리에 사용합니다. | 셀 내용 변경, 행 삽입. |
OPERATION | 일시적인 상호작용 상태를 변경합니다. | 선택 범위, 스크롤 위치, 패널 변경. |
예를 들어 FRange.setValues()는 SetRangeValuesCommand를 호출하고 이 명령은 SetRangeValuesMutation과 실행 취소 데이터를 준비합니다. 여러 진입점이 같은 실행 경로를 사용하므로 기존 검증, 인터셉터, 변경 처리를 재사용할 수 있습니다. 그림의 실행 취소, 협업, 기록에는 각각의 메커니즘이 필요합니다. Mutation 등록만으로 자동 제공되지는 않습니다.
플러그인 기능을 Facade API로 공개하기
Facade는 콘텐츠 객체 중심의 공개 인터페이스를 제공하며 내부 서비스 조회와 명령 매개변수 구성을 감춥니다. 기존 Facade 메서드를 조합하거나 플러그인의 업무 명령을 호출해 확장할 수 있습니다.
다음 예제는 FUniver에 generateSalesReport()를 추가합니다. 명령은 다음 절에서 구현합니다. 세 부분은 런타임 메서드, 런타임 등록, TypeScript 타입을 담당합니다.
sales-report.facade.ts
import { FUniver } from '@univerjs/core/facade';import type { ISalesReportParams } from './sales-report.plugin';import { GenerateSalesReportCommand } from './sales-report.plugin';export interface IFUniverSalesReport { generateSalesReport(params: ISalesReportParams): Promise<boolean>;}export class FUniverSalesReport extends FUniver implements IFUniverSalesReport { override generateSalesReport(params: ISalesReportParams): Promise<boolean> { return this._commandService.executeCommand(GenerateSalesReportCommand.id, params); }}FUniver.extend(FUniverSalesReport);declare module '@univerjs/core/facade' { interface FUniver extends IFUniverSalesReport {}}FUniver.extend()가 런타임에 메서드를 설치하며 모듈 보강은 타입만 추가합니다. 사용자는 side-effect import로 확장 파일을 실행해야 합니다. 클래스를 내보내거나 플러그인을 등록하는 것만으로 별도 Facade 진입점이 실행되지는 않습니다. 배포 패키지는 독립 /facade 하위 경로를 공개하고 package.json#sideEffects로 등록 부수 효과를 보존할 수 있습니다.
이 방식은 extend()를 명시적으로 지원하는 Facade 클래스에만 사용합니다. 내보낸 Facade 클래스가 모두 mixin 대상인 것은 아닙니다. 메서드는 명확한 업무 이름으로 지정하고 기존 메서드나 다른 확장과의 충돌을 피합니다.
예제: 앱과 Agent가 같은 매출 보고서 생성하기
이 최소 업무 플러그인은 지정한 Sheet의 A1:D5를 영어 매출 데이터, 합계 수식, 머리글 스타일로 덮어씁니다. 통합 문서와 워크시트 ID를 명시적으로 받아 현재 선택이나 포커스에 의존하지 않습니다.
대상과 크기를 확인한 후 기존 SetRangeValuesCommand를 호출해 콘텐츠 변경 및 실행 취소/다시 실행 로직을 재사용합니다. 플러그인은 onStarting()에서 명령을 등록하고 disposable을 관리합니다. 고정 데이터는 예시이며 실제 앱에서는 검증한 업무 입력으로 바꿀 수 있습니다.
예제 출력입니다. 읽기 쉽도록 배경색, 숫자 표시 형식, 행 높이와 열 너비를 조정했습니다.
sales-report.plugin.ts
import type { ICellData, ICommand, Workbook } from '@univerjs/core';import { CommandType, DependentOn, ICommandService, Inject, Injector, IUniverInstanceService, Plugin, UniverInstanceType,} from '@univerjs/core';import { SetRangeValuesCommand, UniverSheetsPlugin } from '@univerjs/sheets';export interface ISalesReportParams { unitId: string; subUnitId: string;}export const GenerateSalesReportCommand: ICommand<ISalesReportParams> = { id: 'sales-report.command.generate', type: CommandType.COMMAND, handler: (accessor, params) => { if (!params) return false; const sheet = accessor.get(IUniverInstanceService) .getUnit<Workbook>(params.unitId, UniverInstanceType.UNIVER_SHEET) ?.getSheetBySheetId(params.subUnitId); if (!sheet || sheet.getRowCount() < 5 || sheet.getColumnCount() < 4) return false; const value: ICellData[][] = [ ['Region', 'Q1', 'Q2', 'H1 Total'].map((v) => ({ v, t: 1, s: { bg: { rgb: '#17324D' }, cl: { rgb: '#FFFFFF' }, bl: 1 }, })), [{ v: 'East', t: 1 }, { v: 128000, t: 2 }, { v: 142000, t: 2 }, { f: '=SUM(B2:C2)' }], [{ v: 'West', t: 1 }, { v: 96000, t: 2 }, { v: 118000, t: 2 }, { f: '=SUM(B3:C3)' }], [{ v: 'North', t: 1 }, { v: 110000, t: 2 }, { v: 125000, t: 2 }, { f: '=SUM(B4:C4)' }], [{ v: 'Total', t: 1 }, { f: '=SUM(B2:B4)' }, { f: '=SUM(C2:C4)' }, { f: '=SUM(D2:D4)' }], ]; return accessor.get(ICommandService).executeCommand(SetRangeValuesCommand.id, { ...params, range: { startRow: 0, endRow: 4, startColumn: 0, endColumn: 3 }, value, }); },};@DependentOn(UniverSheetsPlugin)export class SalesReportPlugin extends Plugin { static override pluginName = 'SALES_REPORT_PLUGIN'; static override type = UniverInstanceType.UNIVER_SHEET; constructor( _config: undefined, @Inject(Injector) protected override readonly _injector: Injector, @ICommandService private readonly _commands: ICommandService, ) { super(); } override onStarting(): void { this.disposeWithMe(this._commands.registerCommand(GenerateSalesReportCommand)); }}Sheet 설치 가이드에 따라 제품과 수식 기능을 설정한 후 업무 플러그인과 Facade 확장을 등록하고 호출합니다. 여기서 univer는 해당 설정이 완료된 인스턴스입니다. Node.js 설정은 Headless Web SDK를 참고하세요.
import { FUniver } from '@univerjs/core/facade';import '@univerjs/sheets/facade';import '@univerjs/engine-formula/facade';import { SalesReportPlugin } from './sales-report.plugin';import './sales-report.facade';univer.registerPlugin(SalesReportPlugin);const univerAPI = FUniver.newAPI(univer);const workbook = univerAPI.createWorkbook({ name: 'Quarterly Sales' });const worksheet = workbook.getActiveSheet();const target = { unitId: workbook.getId(), subUnitId: worksheet.getSheetId() };const generated = await univerAPI.generateSalesReport(target);if (!generated) throw new Error('Sales report could not be generated');await univerAPI.getFormula().onCalculationResultApplied();console.log(worksheet.getRange('D5').getValue()); // 719000명령 성공은 콘텐츠 쓰기가 완료되었다는 뜻입니다. 수식 값을 읽기 전에 계산 결과가 적용되기를 기다려야 합니다. 예제의 D5는 719000입니다.
앱 버튼은 클릭 처리기에서 generateSalesReport(target)을 호출할 수 있습니다. Agent 도구 처리기도 Headless 호스트에 같은 플러그인과 Facade 확장을 설치한 후 이 메서드를 호출할 수 있습니다. 업무 동작과 대상은 같으며 UI는 진입점과 피드백을 담당합니다. 사용자 플러그인 설치가 Agent 도구를 자동으로 공개하지는 않습니다. 도구 등록과 Unit 권한 부여는 앱이 담당합니다.
구현 경계와 관련 문서
- 기존 콘텐츠 명령 재사용: 예제는
SetRangeValuesCommand를 사용합니다. 새 Mutation에는 역방향 작업과 적절한 협업 연동이 필요합니다. 새로운 영구 리소스를 추가하면 직렬화도 구현해야 하며 모델을 직접 수정하지 않아야 합니다. - 실행·계산·제출 구분: 여러 Facade 호출은 자동 원자 트랜잭션이 아닙니다. 예제는 한 번의 범위 쓰기를 사용하며 협업 호스트는 Runtime 계약에 따라 변경을 제출하고 확인해야 합니다.
- 환경에 맞게 구성: 콘텐츠와 UI 플러그인을 별도로 연결하고 Univer의 같은 릴리스 계열 패키지를 동일 버전으로 유지합니다. UI 문구는 로케일 리소스를 사용하며 Agent 진입점에는 앱의 권한 부여와 매개변수 검증이 필요합니다.
관련 문서: Univer 아키텍처 · Facade 설정 · Facade API 참조 · UI 사용자 지정 · Agent와 자동화
이 가이드는 어떠셨나요?