Headless Univer

Headless Univer는 대화형 편집기 UI가 없는 Univer 콘텐츠 런타임입니다. 브라우저 편집기와 콘텐츠 모델, 명령, 계산, Facade API를 공유합니다. Node.js에서 콘텐츠를 처리하거나 브라우저에서 UI 없는 작업을 실행할 수 있으며, 사용 가능한 기능은 등록한 제품·기능 플러그인에 따라 결정됩니다.

Node.js에서 통합 문서를 생성·로드하고 셀과 형식, 행과 열을 수정하며 수식을 계산하고 Univer 스냅샷을 저장할 수 있습니다. 필터, 정렬, 유효성 검사, 피벗, 차트는 해당 콘텐츠 플러그인이 필요합니다. 차트 데이터 수정은 차트 렌더링과 별개입니다.

아래 예제는 플러그인 모드를 사용합니다. @univerjs/core와 제품 콘텐츠 플러그인은 Headless 계층에 속하지만 @univerjs/ui, 제품 *-ui, 기본 브라우저 코어 프리셋은 포함하지 않습니다. @univerjs/engine-render는 공유 의존성으로 등록하며 Canvas나 편집기를 마운트하지 않습니다. 패키지 경계에서 분류와 예외를 확인하세요.

로컬 스냅샷 작업에는 협업 서비스가 필요하지 않습니다. 협업 예제에는 클라이언트, 서버, 인증이 추가로 필요합니다. Office 변환과 스크린샷에는 각각 Server SDK와 렌더링 런타임이 필요합니다. /facade 가져오기만으로 기능 플러그인이 등록되거나 UI API가 Headless API로 바뀌지는 않습니다.

설치

Shell
pnpm add @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docs @univerjs/sheets @univerjs/sheets-formula @univerjs/sheets-numfmt

사용법

TypeScript
import { LocaleType, mergeLocales, Univer } from '@univerjs/core'import { FUniver } from '@univerjs/core/facade'import { UniverDocsPlugin } from '@univerjs/docs'import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'import { UniverRenderEnginePlugin } from '@univerjs/engine-render'import { UniverSheetsPlugin } from '@univerjs/sheets'import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'import SheetsKoKR from '@univerjs/sheets/locale/ko-KR'import '@univerjs/engine-formula/facade'import '@univerjs/sheets/facade'import '@univerjs/sheets-formula/facade'import '@univerjs/sheets-numfmt/facade'const univer = new Univer({  locale: LocaleType.KO_KR,  locales: {    [LocaleType.KO_KR]: mergeLocales(      SheetsKoKR,    ),  },})univer.registerPlugin(UniverRenderEnginePlugin)univer.registerPlugin(UniverFormulaEnginePlugin)univer.registerPlugin(UniverDocsPlugin)univer.registerPlugin(UniverSheetsPlugin)univer.registerPlugin(UniverSheetsFormulaPlugin)univer.registerPlugin(UniverSheetsNumfmtPlugin)const univerAPI = FUniver.newAPI(univer)univerAPI.createWorkbook({})

Node.js 환경의 공동 편집 예제

Node.js를 공동 편집 클라이언트로 사용하여 서버에 있는 기존 통합 문서를 불러오고 Facade API로 내용을 수정한 뒤, 같은 문서를 연 다른 클라이언트에 변경 사항을 동기화할 수 있습니다. 실행 전에 공동 편집 서비스, 클라이언트 라이선스, 접근 가능한 문서 ID를 준비하세요.

예제 파일

파일을 다음 구조로 배치합니다.

text
src/sheets/node/├── main.ts├── univer.ts└── worker.ts
  • univer.ts: Univer를 만들고 전체 기능 플러그인, 공동 편집 클라이언트, 선택적인 RPC 메인 프로세스 플러그인을 등록합니다.
  • worker.ts: 계산 측 수식 엔진, 원격 수식, 필터, 피벗 테이블 플러그인을 등록하고 RPC로 메인 프로세스와 통신합니다.
  • main.ts: 공동 편집 문서를 불러오고 권한, 기본 시트 작업, 조건부 서식, 데이터 유효성 검사, 테이블, 피벗 테이블, 스파크라인, 이미지, 차트, 도형, 그룹화, 하이퍼링크 API를 차례로 보여 줍니다.

두 파일의 your-license를 동일한 라이선스로 바꾸고 서버 주소, Cookie, 문서 ID와 사용자 ID도 실제 값으로 변경하세요. main.ts의 권한 예제에는 공동 작업자 및 보호 범위 설정도 포함되어 있으므로 실제 업무 사용자와 필요한 관리 권한을 가진 인증 정보를 사용해야 합니다.

TypeScript
import { UniverCollaborationPlugin } from '@univerjs-pro/collaboration'import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'import {  NodeCollaborationSocketService,  UniverCollaborationClientNodePlugin,} from '@univerjs-pro/collaboration-client-node'import { UniverProFormulaEnginePlugin } from '@univerjs-pro/engine-formula'import { UniverLicensePlugin } from '@univerjs-pro/license'import { UniverSheetsChartPlugin } from '@univerjs-pro/sheets-chart'import { UniverSheetsOutlinePlugin } from '@univerjs-pro/sheets-outline'import { UniverSheetsPivotTablePlugin } from '@univerjs-pro/sheets-pivot'import { UniverSheetsShapePlugin } from '@univerjs-pro/sheets-shape'import { UniverSheetSparklinePlugin } from '@univerjs-pro/sheets-sparkline'import { IAuthzIoService, IImageIoService, IUndoRedoService, LocaleType, Univer } from '@univerjs/core'import { UniverDataValidationPlugin } from '@univerjs/data-validation'import { UniverDocsPlugin } from '@univerjs/docs'import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing'import { UniverDrawingPlugin } from '@univerjs/drawing'import { UniverRenderEnginePlugin } from '@univerjs/engine-render'import { UniverNetworkPlugin } from '@univerjs/network'import { UniverRPCNodeMainPlugin } from '@univerjs/rpc-node'import { UniverSheetsPlugin } from '@univerjs/sheets'import { UniverSheetsConditionalFormattingPlugin } from '@univerjs/sheets-conditional-formatting'import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation'import { UniverSheetsDrawingPlugin } from '@univerjs/sheets-drawing'import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter'import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link'import { UniverSheetsNotePlugin } from '@univerjs/sheets-note'import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'import { UniverSheetsTablePlugin } from '@univerjs/sheets-table'import { UniverThreadCommentPlugin } from '@univerjs/thread-comment'import path from 'node:path'// 여기서 사용하는 Facade API는 packages/facade/src/apis/everything.ts에 포함됩니다// 일부 플러그인은 구현이 불완전하여 해당 API를 여기에 포함하지 않았습니다.import '@univerjs/engine-formula/facade'import '@univerjs/sheets/facade'import '@univerjs/sheets-formula/facade'import '@univerjs/sheets-numfmt/facade'import '@univerjs/sheets-conditional-formatting/facade'import '@univerjs/sheets-data-validation/facade'import '@univerjs/sheets-filter/facade'import '@univerjs/sheets-hyper-link/facade'import '@univerjs/sheets-note/facade'import '@univerjs/sheets-table/facade'import '@univerjs/sheets-drawing/facade'import '@univerjs/sheets-thread-comment/facade'import '@univerjs-pro/engine-shape/facade'import '@univerjs-pro/sheets-pivot/facade'import '@univerjs-pro/sheets-sparkline/facade'import '@univerjs-pro/sheets-chart/facade'import '@univerjs-pro/sheets-shape/facade'import '@univerjs-pro/sheets-outline/facade'import '@univerjs-pro/collaboration-client/facade'export interface ICreateUniverOnNodeOptions {  useComputingWorker?: boolean}export function createUniverOnNode(options: ICreateUniverOnNodeOptions = {}): Univer {  const { useComputingWorker = false } = options  const univer = new Univer({    locale: LocaleType.KO_KR,    locales: {},    override: [      [IAuthzIoService, null],      [IUndoRedoService, null],    ],  })  univer.registerPlugin(UniverLicensePlugin, {    license: 'your-license',  })  registerBasicPlugins(univer, useComputingWorker)  registerSharedPlugins(univer)  if (useComputingWorker) {    registerRPCPlugin(univer)  }  registerDocPlugins(univer)  registerSheetPlugins(univer, useComputingWorker)  registerCollaborationPlugins(univer)  return univer}function registerBasicPlugins(univer: Univer, useComputingWorker: boolean): void {  univer.registerPlugin(UniverProFormulaEnginePlugin, { notExecuteFormula: useComputingWorker })}function registerSharedPlugins(univer: Univer): void {  univer.registerPlugin(UniverRenderEnginePlugin)  univer.registerPlugin(UniverThreadCommentPlugin)  univer.registerPlugin(UniverDrawingPlugin, {    override: [[IImageIoService, null]],  })}function registerDocPlugins(univer: Univer): void {  univer.registerPlugin(UniverDocsPlugin)  univer.registerPlugin(UniverDocsDrawingPlugin)}function registerSheetPlugins(univer: Univer, useComputingWorker: boolean): void {  univer.registerPlugin(UniverSheetsPlugin, { notExecuteFormula: useComputingWorker })  univer.registerPlugin(UniverSheetsFormulaPlugin, { notExecuteFormula: useComputingWorker })  univer.registerPlugin(UniverSheetsNumfmtPlugin)  univer.registerPlugin(UniverSheetsConditionalFormattingPlugin)  univer.registerPlugin(UniverDataValidationPlugin)  univer.registerPlugin(UniverSheetsDataValidationPlugin)  univer.registerPlugin(UniverSheetsFilterPlugin)  univer.registerPlugin(UniverSheetsHyperLinkPlugin)  univer.registerPlugin(UniverSheetsNotePlugin)  univer.registerPlugin(UniverSheetsDrawingPlugin)  univer.registerPlugin(UniverSheetsSortPlugin)  univer.registerPlugin(UniverSheetsTablePlugin)  univer.registerPlugin(UniverSheetsChartPlugin)  univer.registerPlugin(UniverSheetsShapePlugin)  univer.registerPlugin(UniverSheetsPivotTablePlugin, {    notExecuteFormula: useComputingWorker ? true : undefined,  })  univer.registerPlugin(UniverSheetSparklinePlugin)  univer.registerPlugin(UniverSheetsOutlinePlugin)}function registerRPCPlugin(univer: Univer): void {  // 빌드 결과물의 실제 위치에 맞게 worker 경로를 설정합니다.  const childPath = path.join(__dirname, 'worker.js')  univer.registerPlugin(UniverRPCNodeMainPlugin, { workerSrc: childPath })}function registerCollaborationPlugins(univer: Univer): void {  const serverUrl = 'https://your-univer-server.example.com'  univer.registerPlugin(UniverNetworkPlugin)  univer.registerPlugin(UniverCollaborationPlugin)  univer.registerPlugin(UniverCollaborationClientPlugin, {    socketService: NodeCollaborationSocketService,    enableOfflineEditing: false,    enableSingleActiveInstanceLock: false,    snapshotServerUrl: `${serverUrl}/universer-api/snapshot`,    collabSubmitChangesetUrl: `${serverUrl}/universer-api/comb`,    collabWebSocketUrl: `${serverUrl}/universer-api/comb/connect`,    wsSessionTicketUrl: `${serverUrl}/universer-api/user/session-ticket`,    uploadFileServerUrl: `${serverUrl}/universer-api/stream/file/upload`,    signUrlServerUrl: `${serverUrl}/universer-api/file/{fileID}/sign-url`,    downloadEndpointUrl: `${serverUrl}`,    authzUrl: `${serverUrl}/universer-api/authz`,    sendChangesetTimeout: 200,    retryConnectingInterval: 1000,    customHeaders: {      // 예: Univer 서버에서 Cookie 인증이 필요한 경우 여기에 'cookie' 헤더를 설정할 수 있습니다.      cookie: 'your-cookie',      // ... 추가 헤더    },  })  univer.registerPlugin(UniverCollaborationClientNodePlugin)}

계산 worker 활성화

createUniverOnNode()의 기본값은 useComputingWorker: false이며 현재 프로세스에서 수식을 계산합니다. 위의 main.ts는 useComputingWorker: true를 명시적으로 전달하여 계산 worker를 활성화합니다.

  1. 메인 프로세스의 UniverProFormulaEnginePlugin, UniverSheetsPlugin, UniverSheetsFormulaPlugin, UniverSheetsPivotTablePlugin에 모두 notExecuteFormula: true를 설정하여 RPC를 통해 자식 프로세스에 계산을 맡깁니다.
  2. UniverRPCNodeMainPlugin이 workerSrc에 지정된 빌드된 worker.js를 실행합니다.
  3. 자식 프로세스는 UniverRPCNodeWorkerPlugin과 UniverRemoteSheetsFormulaPlugin을 통해 데이터를 수신하고 계산합니다.

UniverSheetsPivotTablePlugin 설정은 세 가지 모드를 구분합니다. worker를 사용하는 메인 프로세스에서는 true, worker에서는 false를 설정합니다. worker를 사용하지 않을 때는 생략하거나 undefined를 지정하여 현재 프로세스에 피벗 테이블 서비스와 계산 로직을 함께 등록합니다. 따라서 메인 프로세스에서는 useComputingWorker를 직접 전달하지 않고 useComputingWorker ? true : undefined를 사용합니다.

이 worker는 Node.js의 child_process.fork()로 실행됩니다. 프로젝트에서 사용하는 빌드 도구로 main.ts와 worker.ts를 각각 독립적인 진입점으로 빌드하고 workerSrc가 빌드된 worker 파일을 가리키도록 설정하세요. 예제는 worker.js가 메인 진입점 결과물과 같은 디렉터리에 있다고 가정합니다. 경로, 확장자, 모듈 형식은 실제 빌드 결과에 맞춰야 합니다. 예제는 CommonJS의 __dirname으로 파일을 찾으므로 ESM 프로젝트에서는 해당 모듈 방식에 맞는 경로 해석을 사용하세요.

실행 안내

loadSheetAsync()는 서버의 기존 문서를 불러오므로 createWorkbook()을 호출할 필요가 없습니다. 공동 편집 서비스는 customHeaders에 포함된 Cookie 등의 인증 정보로 사용자를 식별하고 권한을 확인하며, 연결의 memberID는 서버가 반환합니다.

이 예제는 브라우저 오프라인 캐시와 여러 탭 간 인스턴스 잠금을 비활성화하므로 계정별 캐시 격리를 위해 setCurrentUser()를 호출할 필요가 없습니다. 나중에 댓글처럼 로컬 현재 사용자에 의존하는 기능을 사용할 경우 UserManagerService로 업무 사용자를 설정할 수 있습니다. 이 설정은 서버 인증을 대체하지 않습니다.

예제는 실행 후에도 공동 편집 연결을 유지합니다. 브라우저에서 같은 문서를 열면 변경 사항을 확인할 수 있습니다. flush()는 대기 중인 공동 편집 변경 사항에 대한 서버 확인을 기다립니다. 일회성 스크립트로 변경한다면 모든 비동기 작업과 동기화가 완료된 뒤 univer.dispose()를 호출하여 인스턴스를 해제하세요.

이 가이드는 어떠셨나요?

© 2026 DreamNum Co., Ltd.