# 플러그인과 Facade 확장

> 필요한 기능을 조합하고 공개 API로 콘텐츠와 업무 UI를 확장합니다.

- Human documentation: [https://docs.univer.ai/ko-KR/overview/features/shared/customization](https://docs.univer.ai/ko-KR/overview/features/shared/customization)

- Agent Markdown: [https://docs.univer.ai/ko-KR/overview/features/shared/customization.md](https://docs.univer.ai/ko-KR/overview/features/shared/customization.md)

- Requested language: `ko-KR`

- Content language: `ko-KR`

- Documentation version: `1.3.0`

- Source: [features/shared/customization.ko-KR.mdx](https://github.com/dream-num/documentation/blob/dev/content/docs/features/shared/customization.ko-KR.mdx)

---

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` | 일시적인 상호작용 상태를 변경합니다. | 선택 범위, 스크롤 위치, 패널 변경. |

```mermaid
flowchart TD
    U["도구 모음 / 단축키"] --> C["Command<br/>검증과 업무 구성"]
    A["앱 / Agent 도구"] --> F["Facade API"]
    F --> C
    C --> M["Mutation<br/>콘텐츠 변경"]
    M --> D["콘텐츠 모델"]
    D --> R["렌더링 / 계산"]
    M -. "해당 연동 메커니즘" .-> H["실행 취소·다시 실행 / 협업 / 기록"]
```

예를 들어 `FRange.setValues()`는 `SetRangeValuesCommand`를 호출하고 이 명령은 `SetRangeValuesMutation`과 실행 취소 데이터를 준비합니다. 여러 진입점이 같은 실행 경로를 사용하므로 기존 검증, 인터셉터, 변경 처리를 재사용할 수 있습니다. 그림의 실행 취소, 협업, 기록에는 각각의 메커니즘이 필요합니다. Mutation 등록만으로 자동 제공되지는 않습니다.

## 플러그인 기능을 Facade API로 공개하기

Facade는 콘텐츠 객체 중심의 공개 인터페이스를 제공하며 내부 서비스 조회와 명령 매개변수 구성을 감춥니다. 기존 Facade 메서드를 조합하거나 플러그인의 업무 명령을 호출해 확장할 수 있습니다.

다음 예제는 `FUniver`에 `generateSalesReport()`를 추가합니다. 명령은 다음 절에서 구현합니다. 세 부분은 런타임 메서드, 런타임 등록, TypeScript 타입을 담당합니다.

### sales-report.facade.ts

```typescript
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을 관리합니다. 고정 데이터는 예시이며 실제 앱에서는 검증한 업무 입력으로 바꿀 수 있습니다.

![매출 보고서 플러그인 출력: 지역별 매출과 계산된 합계](https://raw.githubusercontent.com/dream-num/documentation/dev/public/assets/features/customization/sales-report.png)

예제 출력입니다. 읽기 쉽도록 배경색, 숫자 표시 형식, 행 높이와 열 너비를 조정했습니다.

### sales-report.plugin.ts

```typescript
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 설치 가이드](https://docs.univer.ai/ko-KR/guides/sheets/getting-started/installation.md)에 따라 제품과 수식 기능을 설정한 후 업무 플러그인과 Facade 확장을 등록하고 호출합니다. 여기서 `univer`는 해당 설정이 완료된 인스턴스입니다. Node.js 설정은 [Headless Web SDK](https://docs.univer.ai/ko-KR/guides/sheets/getting-started/node.md)를 참고하세요.

```typescript
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 아키텍처](/blog/univer) · [Facade 설정](https://docs.univer.ai/ko-KR/guides/sheets/getting-started/facade.md) · [Facade API 참조](https://docs.univer.ai/ko-KR/reference/facade.md) · [UI 사용자 지정](https://docs.univer.ai/ko-KR/guides/sheets/ui/overview.md) · [Agent와 자동화](https://docs.univer.ai/ko-KR/overview/features/shared/agents.md)

[기능 개요로 돌아가기](https://docs.univer.ai/ko-KR/overview/features.md)
