# 사용자 지정 컴포넌트

> Web SDK는 사용자 지정 컴포넌트를 통합하는 여러 방법을 제공하므로 기능을 확장하고 원하는 대로 조정할 수 있습니다. 이 가이드에서는 자주 사용하는 몇 가지 방법을 소개합니다.

- Human documentation: [https://docs.univer.ai/ko-KR/guides/sheets/ui/components](https://docs.univer.ai/ko-KR/guides/sheets/ui/components)

- Agent Markdown: [https://docs.univer.ai/ko-KR/guides/sheets/ui/components.md](https://docs.univer.ai/ko-KR/guides/sheets/ui/components.md)

- Requested language: `ko-KR`

- Content language: `ko-KR`

- Documentation version: `1.0.4`

- Source: [sheets/ui/components.ko-KR.mdx](https://github.com/dream-num/documentation/blob/dev/content/guides/sheets/ui/components.ko-KR.mdx)

---

Univer는 컴포넌트를 렌더링 함수에 매개변수로 직접 전달하지 않습니다. 여러 마운트 지점에서 컴포넌트를 사용하려면 먼저 Facade API를 통해 Univer에 등록해야 합니다.

Facade API 인스턴스를 가져오는 방법이 익숙하지 않다면 [Facade API](https://docs.univer.ai/ko-KR/guides/sheets/getting-started/facade.md)를 참고하세요.

## 사용자 지정 컴포넌트 등록

```typescript
// [!code word:componentKey]
// [!code word:CustomComponent]
// [!code word:options]
univerAPI.registerComponent(componentKey, CustomComponent, options)
```

`univerAPI.registerComponent` 메서드를 사용해 사용자 지정 컴포넌트를 등록할 수 있습니다. 이 메서드는 세 가지 매개변수를 받습니다.

* `componentKey`: Univer에서 컴포넌트를 참조할 때 사용하는 고유 식별자입니다.
* `CustomComponent`: 컴포넌트 구현입니다. React, Vue 또는 Web Component를 사용할 수 있습니다.
* `options`: 컴포넌트가 의존하는 프레임워크나 그 밖의 관련 설정을 지정하는 선택적 구성 옵션입니다.

### React 컴포넌트

React 컴포넌트를 등록할 때는 별도의 구성이 필요하지 않습니다. 유효한 React 컴포넌트인지 확인하기만 하면 됩니다. 다음은 간단한 예입니다.

```tsx
function ReactComponent(props: Record<string, any>) {
  return <div>Hello Univer!</div>
}

univerAPI.registerComponent('MyReactComponent', ReactComponent)
```

### Vue 컴포넌트

#### Vue 3.x

Vue 컴포넌트를 등록하려면 `UniverVue3AdapterPlugin`을 `@univerjs/ui-adapter-vue3` 패키지에서 설치하고 등록해야 합니다.

#### npm

```bash
npm install @univerjs/ui-adapter-vue3
```

#### pnpm

```bash
pnpm add @univerjs/ui-adapter-vue3
```

#### yarn

```bash
yarn add @univerjs/ui-adapter-vue3
```

#### bun

```bash
bun add @univerjs/ui-adapter-vue3
```

```typescript
import { UniverVue3AdapterPlugin } from '@univerjs/ui-adapter-vue3'

univer.registerPlugin(UniverVue3AdapterPlugin)
```

Vue 컴포넌트를 등록할 때는 `framework` 옵션을 `'vue3'`으로 지정해야 합니다.

```tsx
const Vue3Component = defineComponent({
  setup(props) {
    return () => <div>Hello Univer!</div>
  },
})

univerAPI.registerComponent('MyVue3Component', Vue3Component, {
  // [!code word:vue3]
  framework: 'vue3',
})
```

#### Vue 2.x

Vue 2.x는 지원 종료(EOL)되어 더 이상 유지 관리되지 않으므로 Univer는 현재 Vue 2.x용 UI 어댑터 플러그인을 제공하지 않습니다. 하지만 사용자 지정 플러그인을 만들어 Vue 2.x 어댑터를 구현할 수 있습니다. 다음은 Vue 2.x 어댑터 플러그인을 만드는 예입니다.

```typescript title="ui-adapter-vue2.ts"
import { DependentOn, Inject, Injector, Plugin } from '@univerjs/core'
import { ComponentManager, UniverUIPlugin } from '@univerjs/ui'
import Vue from 'vue'

/**
 * The plugin that allows Univer to use Vue 2 components as UI components.
 */
@DependentOn(UniverUIPlugin)
export class UniverVue2AdapterPlugin extends Plugin {
  static override pluginName = 'UNIVER_UI_VUE2_ADAPTER_PLUGIN'

  constructor(
    private readonly _config = {},
    @Inject(Injector) protected readonly _injector: Injector,
    @Inject(ComponentManager) protected readonly _componentManager: ComponentManager,
  ) {
    super()
  }

  override onStarting(): void {
    const { createElement, useEffect, useRef } = this._componentManager.reactUtils

    this._componentManager.setHandler('vue2', (component: any) => {
      return (props: Record<string, any>) =>
        createElement(VueComponentWrapper, {
          component,
          props: Object.keys(props).reduce<Record<string, any>>((acc, key) => {
            if (key !== 'key') {
              acc[key] = props[key]
            }
            return acc
          }, {}),
          reactUtils: { createElement, useEffect, useRef },
        })
    })
  }
}

export function VueComponentWrapper(options: {
  component: any
  props: Record<string, any>
  reactUtils: typeof ComponentManager.prototype.reactUtils
}) {
  const { component, props, reactUtils } = options
  const { createElement, useEffect, useRef } = reactUtils

  const domRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!domRef.current) return

    const Constructor = Vue.extend(component)

    const instance = new Constructor({
      data: props,
    })
    instance.$mount()

    domRef.current.appendChild(instance.$el)

    return () => {
      instance.$destroy()
    }
  }, [props])

  return createElement('div', { ref: domRef })
}
```

그런 다음 플러그인을 등록합니다.

```typescript
import { UniverVue2AdapterPlugin } from './ui-adapter-vue2' // [!code ++]

univer.registerPlugin(UniverUIPlugin)
univer.registerPlugin(UniverVue2AdapterPlugin) // [!code ++]
```

Vue 2.x 컴포넌트를 등록할 때는 `framework` 옵션을 `'vue2'`로 지정해야 합니다.

```tsx
const Vue2Component = Vue.component('MyVue2Component', {
  template: '<div>Hello, Univer!</div>',
})

univerAPI.registerComponent('MyVue2Component', Vue2Component, {
  // [!code word:vue2]
  framework: 'vue2',
})
```

### Web Components

Web Components를 등록하려면 컴포넌트가 Web Components 표준을 준수하는지 확인하고, `UniverWebComponentAdapterPlugin`을 `@univerjs/ui-adapter-web-component` 패키지에서 설치하고 등록해야 합니다.

#### npm

```bash
npm install @univerjs/ui-adapter-web-component
```

#### pnpm

```bash
pnpm add @univerjs/ui-adapter-web-component
```

#### yarn

```bash
yarn add @univerjs/ui-adapter-web-component
```

#### bun

```bash
bun add @univerjs/ui-adapter-web-component
```

```typescript
import { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component'

univer.registerPlugin(UniverWebComponentAdapterPlugin)
```

Web Components를 등록할 때는 `framework` 옵션을 `'web-component'`로 지정해야 합니다.

```tsx
class WebComponent extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })
    const div = document.createElement('div')
    div.textContent = 'Hello Univer!'
    shadow.appendChild(div)
  }
}

univerAPI.registerComponent('my-web-component', WebComponent, {
  // [!code word:web-component]
  framework: 'web-component',
})
```

### Angular

`@angular/elements`의 `createCustomElement`로 Angular 컴포넌트를 사용자 정의 요소로 변환한 후 Univer의 Web Component 어댑터에 등록하세요. Angular 컴포넌트를 Univer 사이드바나 대화 상자에서 사용할 수 있습니다.

`@angular/core`와 같은 버전의 `@angular/elements`, 다른 Univer 패키지와 같은 버전의 `@univerjs/ui-adapter-web-component`를 추가하세요.

Univer 초기화 후 아래 함수를 호출하세요. 호스트 컴포넌트에서 `private readonly injector = inject(Injector)`로 Angular 주입기를 가져와 `registerAngularPanel(univer, univerAPI, this.injector)`에 전달합니다. 호스트의 `inject`와 `Injector`는 `@angular/core`에서 가져오세요.

```typescript title="angular-panel.ts"
import type { Injector } from '@angular/core'
import type { FUniver, Univer } from '@univerjs/presets'
import { Component } from '@angular/core'
import { createCustomElement } from '@angular/elements'
import { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component'

@Component({
  selector: 'app-angular-panel',
  standalone: true,
  template: '<p>Hello from Angular!</p>',
})
export class AngularPanelComponent {}

export function registerAngularPanel(univer: Univer, univerAPI: FUniver, injector: Injector) {
  const element = createCustomElement(AngularPanelComponent, { injector })

  univer.registerPlugin(UniverWebComponentAdapterPlugin)
  return univerAPI.registerComponent('univer-angular-panel', element, {
    framework: 'web-component',
  })
}
```

편집기 렌더링 후 등록한 키를 사이드바 콘텐츠로 사용하세요.

```typescript
const sidebar = univerAPI.openSidebar({
  header: { title: 'Angular' },
  children: { label: 'univer-angular-panel' },
  width: 360,
})
```

반환된 등록 핸들과 사이드바 핸들을 보관하고 더 이상 필요하지 않으면 각각 `dispose()`를 호출하세요. 사용자 정의 요소는 애플리케이션에서 한 번만 등록합니다. 이름에 하이픈이 있어야 하며 Angular 컴포넌트의 selector와 달라야 합니다.

Angular Elements는 입력을 요소 속성으로, 출력을 DOM `CustomEvent`로 변환합니다. Univer 어댑터는 속성을 전달하지만 Angular 출력을 자동 구독하지 않습니다. 필요한 이벤트를 명시적으로 연결하세요.

## 사용자 지정 컴포넌트 사용

다음 메서드를 사용하면 다양한 사용자 지정 컴포넌트를 Univer에 유연하게 통합하여 기능을 확장하고 맞춤 설정할 수 있습니다.

> [!WARNING: 주의]
> 1. 이 메서드를 사용하기 전에 Univer의 렌더링이 완료되었는지 확인하세요. 2. 등록이 필요한 컴포넌트는 사용하기 전에
>    올바르게 등록되었는지 확인하세요. 3. 메모리 누수를 방지하려면 `dispose()` 메서드로 추가한 컴포넌트를 정리하고
>    제거하세요.

### 사용자 지정 메뉴 항목 추가

상단 메뉴 모음(Ribbon)과 오른쪽 클릭 메뉴(Context Menu) 모두에 사용자 지정 컴포넌트를 넣을 수 있습니다. 메뉴 항목에 사용자 지정 컴포넌트를 추가하려면 사용자 지정 플러그인을 만들어야 합니다. 빠르게 시작할 수 있도록 [사용자 지정 메뉴 항목 추가 모범 사례](/blog/custom-plugin)를 준비했습니다.

### 기본 제공 컴포넌트 교체

> [!ERROR: 경고]
> 기본 제공 컴포넌트를 교체하면 일부 기능이 제대로 작동하지 않을 수 있습니다. 컴포넌트를 교체하기 전에 소스 코드와
> 문서를 충분히 읽고 신중하게 진행하세요.

`univerAPI.registerComponent`는 기본적으로 재정의 컴포넌트를 등록합니다(`override: true`). 플러그인이 기본 컴포넌트를 등록하는 순서와 관계없이 재정의가 우선합니다. 각 `componentKey`에는 하나의 재정의만 활성화할 수 있으므로 다시 등록하기 전에 이전 등록을 해제하세요. 반환값의 `dispose()`를 호출하면 등록된 기본 컴포넌트로 돌아갑니다. `{ override: false }`는 기본 컴포넌트를 등록합니다.

해당 뷰를 처음 열기 전에 등록하세요. 등록을 바꿔도 이미 마운트된 뷰가 자동으로 다시 렌더링되지는 않습니다.

Sheets 패키지의 공개 진입점에서 다음 키를 가져와 해당 기본 컴포넌트를 재정의할 수 있습니다.

| 컴포넌트       | 공개 키                       | 패키지                                          |
| ---------- | -------------------------- | -------------------------------------------- |
| 정의된 이름     | `DEFINED_NAME_CONTAINER`   | `@univerjs/sheets-ui`                        |
| 추가 함수      | `MORE_FUNCTIONS_COMPONENT` | `@univerjs/sheets-formula-ui`                |
| 데이터 유효성 검사 | `DATA_VALIDATION_PANEL`    | `@univerjs/sheets-data-validation-ui`        |
| 조건부 서식     | `CF_PANEL_KEY`             | `@univerjs/sheets-conditional-formatting-ui` |

예를 들어 기본 제공 ColorPicker 컴포넌트는 다음과 같이 간단히 교체할 수 있습니다.

```tsx
// Following code only replaces the UI of the built-in ColorPicker component, not its functionality
univerAPI.registerComponent('UI_COLOR_PICKER_COMPONENT', () => <input type="color" />)
```

### 콘텐츠 컴포넌트로 사용

#### 사이드바에서 사용

`univerAPI.openSidebar` 메서드를 사용하면 Univer 인터페이스에서 사용자 지정 컴포넌트가 포함된 사이드바를 열 수 있습니다.

```tsx
// [!code word:MyCustomSidebarComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent('MyCustomSidebarComponent', () => <div>Hello Univer!</div>)

const sidebar = univerAPI.openSidebar({
  header: { title: 'My Sidebar' },
  children: { label: 'MyCustomSidebarComponent' },
  onClose: () => {
    console.log('close')
  },
  width: 360,
})

// Later close the sidebar
sidebar.dispose()
```

참조: [`univerAPI.openSidebar`](https://docs.univer.ai/ko-KR/reference/facade/univer.md#opensidebar)

#### 대화 상자에서 사용

`univerAPI.openDialog` 메서드를 사용하면 사용자 지정 컴포넌트가 포함된 대화 상자를 열 수 있습니다.

```tsx
// [!code word:MyCustomDialogComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent('MyCustomDialogComponent', () => <div>Hello Univer!</div>)

const dialog = univerAPI.openDialog({
  id: 'unique-dialog-id', // The unique identifier for the dialog
  draggable: true,
  width: 300,
  title: { title: 'My Dialog' },
  children: {
    label: 'MyCustomDialogComponent',
  },
  destroyOnClose: true,
  preservePositionOnDestroy: true,
  onClose: () => {},
})

// Later close the dialog
dialog.dispose()
```

참조: [`univerAPI.openDialog`](https://docs.univer.ai/ko-KR/reference/facade/univer.md#opendialog)

#### 셀에 팝업 연결

`FRange.attachPopup` 메서드를 사용하면 특정 셀 범위에 사용자 지정 팝업을 연결할 수 있습니다.

* 팝업은 셀에 연결되는 임시 DOM 요소입니다. 일반적으로 일시적인 상태 정보를 표시하는 데 사용하며 영구 저장은 지원하지 않습니다.
* [`FRange.attachPopup(popup: IFCanvasPopup)`](https://docs.univer.ai/ko-KR/reference/facade/range.md#attachpopup) 메서드를 사용하면 특정 셀 범위에 사용자 지정 팝업을 연결할 수 있습니다.
* 팝업은 셀 가장자리에 붙으며, 가려질 경우 위치와 방향을 자동으로 조정합니다.

```tsx
// [!code word:MyCustomPopupComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent(
  'MyCustomPopupComponent',
  () => <div>Hello Univer!</div>,
)

const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()
const fRange = fWorksheet.getRange('A1:J10')

// Attach the popup to the first cell of the range
// If `disposable` is null, it indicates that the popup was not added successfully
const disposable = fRange.attachPopup({
  componentKey: 'MyCustomPopupComponent',
})

// Remove the popup
disposable.dispose()
```

#### 경고 팝업 연결

`FRange.attachAlertPopup` 메서드를 사용하면 지정한 범위의 시작 셀에 경고 팝업을 연결할 수 있습니다.

```typescript
const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()
const fRange = fWorksheet.getRange('A1:B2')

const disposable = fRange.attachAlertPopup({
  key: 'unique-alert-key', // Unique identifier
  title: '这是一个警告！',
  message: '这是一个警告！',
  width: 300,
  height: 200,
  // 0: Information
  // 1: Warning
  // 2: Error
  type: 0,
})

// Remove the alert later
disposable.dispose()
```

#### 워크시트에 플로팅 DOM 추가

`FWorksheet.addFloatDomToPosition` 메서드를 사용하면 워크시트에 플로팅 DOM을 추가할 수 있습니다.

* 이 메서드를 사용하기 전에 `@univerjs/sheets-drawing-ui` 플러그인 또는 `@univerjs/preset-sheets-drawing` 프리셋을 설치해야 합니다.
* 플로팅 DOM은 워크시트 위에 떠 있는 드래그 가능한 컴포넌트이며 영구 저장을 지원합니다.
* Univer의 렌더링이 끝난 뒤 이 메서드를 호출해야 합니다.
* `componentKey`: Univer 내부에서 컴포넌트를 참조하는 데 사용하는 고유 식별자입니다.
* [전체 매개변수 정의](https://github.com/dream-num/univer/blob/dev/packages/sheets-drawing-ui/src/facade/f-worksheet.ts#L32)

```tsx
// [!code word:MyCustomFloatDOMComponent]
// You should register the component at an appropriate time (e.g., after Univer has loaded)
univerAPI.registerComponent(
  'MyCustomFloatDOMComponent',
  ({ data }) => (
    <div>
      Hello
      {data?.label}
      !
    </div>
  ),
)
const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()

// Add a floating DOM to the worksheet
// If `disposable` is null, it indicates that the floating DOM was not added successfully
const disposable = fWorksheet.addFloatDomToPosition({
  componentKey: 'MyCustomFloatDOMComponent',
  initPosition: {
    startX: 100,
    endX: 200,
    startY: 100,
    endY: 200,
  },

  // The data for the component
  data: {
    label: 'Univer',
  },
})

// Remove the floating DOM
disposable.dispose()
```
