[]
        
(Showing Draft Content)

SpreadJS Sheets Collaboration 기능

실시간 동시 작업 편집에서 SnapshotChangeSet은 여러 사용자가 동시에 동일한 문서를 편집할 때 데이터 일관성, 효율성, 실시간 성능을 보장하기 위해 도입된 두 가지 핵심 개념입니다. 이 개념들은 동시 작업, 데이터 동기화, 성능 최적화와 같은 중요한 과제를 해결합니다. 아래에서는 정의, 목적, 구현 세부 사항을 설명합니다.

설치

import '@mescius/spread-sheets-collaboration-addon';

Collaboration 클래스

동시 작업 기능을 제공하기 위해 Workbook에 Collaboration 클래스를 얻을 수 있는 속성이 추가되며, 이 클래스는 동시 작업과 관련된 기능을 제공합니다.

///* field GC.Spread.Sheets.@Workbook.collaboration: GC.Spread.Sheets.Collaboration.Collaboration
/**
 * Workbook용 동시 작업 관리자
 * @type {GC.Spread.Sheets.Collaboration.Collaboration}
 * //이 예제는 snapshot을 가져오는 방법을 보여줍니다.
 * @example
 * var snapshot = workbook.collaboration.toSnapshot();
 */

Snapshot

Snapshot을 사용하는 이유

  • 다중 사용자 환경에서 모든 사용자는 문서의 현재 상태를 알아야 합니다. Snapshot은 특정 버전 시점의 문서 전체 상태를 나타냅니다.

  • Snapshot은 클라이언트가 최신 문서 버전을 빠르게 가져올 수 있게 하며, 이후 편집을 위한 기준선 역할을 합니다.

동시 작업에서의 가치

  • 일관성: Snapshot은 모든 사용자에게 동일한 시작 지점을 제공하여 동일한 문서 상태를 기반으로 편집하도록 보장합니다.

  • 초기화 및 복구: 신규 사용자나 재연결한 사용자는 Snapshot을 사용해 최신 상태로 동기화할 수 있습니다.

Snapshot 형식

Snapshot은 실시간 동시 작업을 위해 특별히 설계된 새로운 데이터 형식을 사용하며, ssjson이나 sjs와 같은 기존 형식과는 다릅니다.


API

Snapshot은 특정 버전의 Workbook 전체 상태를 캡처합니다. 다음 메서드는 Workbook 인스턴스와 Snapshot 간 변환을 지원합니다.

  • toSnapshot: Workbook 상태를 Snapshot으로 저장합니다.

/**
 * 동시 작업 시나리오에서만 사용되며, Workbook 상태를 snapshot으로 저장합니다.
 * @return {object} snapshot - snapshot 객체
 */
Workbook.collaboration.prototype.toSnapshot = function (): any;
  • fromSnapshot: Snapshot으로부터 Workbook 상태를 복원합니다.

/**
 * 동시 작업 시나리오에서만 사용되며, snapshot을 workbook 상태로 복원합니다.
 * @param {object} snapshot - snapshot 객체
 */
Workbook.collaboration.prototype.fromSnapshot = function (snapshot: any): void;

Op (Operation)

Op란 무엇인가

문서 모델에 대한 모든 변경은 Op(Operation)으로 기록됩니다.


Op를 사용하는 이유

  • 편집할 때마다 전체 문서(새 Snapshot)를 전송하면 과도한 대역폭을 소모합니다. Op는 setValue(값 설정), addRow(행 추가)와 같이 새 Snapshot으로 이어지는 구체적인 변경 사항만을 설명합니다.

  • Op만 전송함으로써 전체 문서를 반복 전송하지 않고도 동기화를 달성할 수 있습니다.

API

각 Op에는 수정 유형을 설명하는 type 필드가 포함됩니다.

export interface IOpComponent{
    type: GC.Spread.Sheets.Collaboration.OpType;
}

OpType에 대한 자세한 정의는 다음 문서를 참고하세요: OpType.

ChangeSet

ChangeSet이란 무엇인가

  • ChangeSet은 서로 관련된 Op들의 집합으로, 일반적으로 하나의 논리적 수정 단위를 나타냅니다. 문서가 한 버전에서 다음 버전으로 전환되는 과정을 설명합니다.

  • SpreadJS에서는

    • 단일 모드(기본값): 하나의 명령으로 생성된 Ops가 하나의 ChangeSet으로 그룹화됩니다.

    • 배치 모드: 개발자가 여러 변경을 하나의 ChangeSet으로 병합하여 서버 부하와 네트워크 사용량을 줄입니다(동기 실행 필요). 자세한 내용은 “Batch Operations” 섹션을 참고하세요.

ChangeSet을 사용하는 이유

  • 논리적 그룹화: ChangeSet은 관련된 Ops를 묶어 수정 내용을 더 명확하게 표현합니다. 예를 들어 하나의 동작이 여러 모델에 영향을 미칠 수 있습니다.

  • 원자성: ChangeSet은 Ops 그룹이 전부 적용되거나 전혀 적용되지 않도록 보장하여 부분 적용 상태를 방지합니다.

  • 충돌 해결: 다중 사용자 환경에서 발생하는 충돌을 보다 상위 수준에서 비교·병합할 수 있습니다.

  • 이력 관리: 전체 수정 이력을 기록하여 버전 롤백, 디버깅, 감사에 활용할 수 있습니다.

  • 성능 최적화: 여러 Op를 하나의 ChangeSet으로 묶어 네트워크 전송과 동기화 효율을 높입니다.

동시 작업에서의 가치

  • 효율성: ChangeSet은 작고 빠르게 전송되어 실시간 동시 작업에 적합합니다.

  • 동시성 처리: OT(Operational Transformation)를 통해 동시에 발생한 사용자 작업을 일관되게 처리합니다.

ChangeSet 제한 사항

동일한 Snapshot에서 시작된 Workbook만 서로의 Op를 적용할 수 있습니다.


API

동시 작업은 ChangeSet을 적용하고 감지하기 위한 두 가지 API를 제공합니다.

interface IChangeSet {
    ops: IOpComponent[];
}
/**
 * 동시 작업 시나리오에서만 사용되며, changeSet을 문서에 적용합니다.
 * @param {changeSet: GC.Spread.Sheets.Collaboration.IChangeSet} changeSet - change set
 */
Workbook.collaboration.prototype.applyChangeSet = (changeSet: GC.Spread.Sheets.Collaboration.IChangeSet) => void;
  • onChangeSet: ChangeSet 생성 이벤트를 수신합니다.

/**
 * 동시 작업 시나리오에서만 사용되며, changeset을 감시합니다.
 * @param {changeSetHandler: GC.Spread.Sheets.Collaboration.IChangeSetHandler} onOpHandler - change set 콜백
 */
Workbook.collaboration.prototype.onChangeSet = (onChangeSetHandler: GC.Spread.Sheets.Collaboration.IChangeSetHandler) => void;

Batch Operations

Batch Operations란 무엇인가

Batch Operations는 여러 개의 동기식 Workbook 작업(예: 대량 설정 변경, 반복적인 데이터 삽입/삭제)을 하나의 ChangeSet으로 묶을 수 있게 합니다. startBatchOp()endBatchOp()로 감싸면 중간에 생성된 모든 Op가 하나의 원자적 변경으로 병합됩니다.


Batch Operations를 사용하는 이유

  • 원자성: 그룹화된 작업이 단일 변경으로 적용되어 일관성을 보장합니다.

  • 효율성: ChangeSet 수를 줄여 네트워크 사용량과 서버 부하를 감소시킵니다.

  • Undo/Redo 단순화: 대량 업데이트를 하나의 단계로 되돌릴 수 있습니다.

  • 실용적 활용: Workbook 초기화나 대규모 업데이트에 적합합니다.

API

Workbook.collaboration.prototype.startBatchOp(): void
Workbook.collaboration.prototype.endBatchOp(): void;

주의 사항

  • startBatchOp()과 endBatchOp()은 반드시 쌍으로 올바른 순서로 사용해야 합니다.

  • 두 함수 사이의 작업은 반드시 동기식이어야 합니다.

예제

spread.collaboration.startBatchOp();
sheet.setValue(0, 0, "Hello");
sheet.setFormula(0, 2, "=SUM(A1:B1)");
spread.collaboration.endBatchOp(); 
// 위 모든 작업은 하나의 ChangeSet으로 병합됩니다.

기타

타입 등록

동시 작업 시나리오에서는 OT_Type을 등록해야 비정상적인 Undo 동작 등을 방지할 수 있습니다.

export interface IOT_Type{
    uri?: string;
    transform?: (op1: GC.Spread.Sheets.Collaboration.IChangeSet, op2: GC.Spread.Sheets.Collaboration.IChangeSet, side: 'left' | 'right') => GC.Spread.Sheets.Collaboration.IChangeSet;
    transformX?: (op1: GC.Spread.Sheets.Collaboration.IChangeSet, op2: GC.Spread.Sheets.Collaboration.IChangeSet, side: 'left' | 'right') => GC.Spread.Sheets.Collaboration.IChangeSet[];
}
/**
 * 동시 작업 시나리오에서만 사용되며, 동시 작업 타입을 등록합니다.

예제

import * as GC from '@mescius/spread-sheets'
import { type } from '@mescius/spread-sheets-collaboration-client';

const workbook = new GC.Spread.Sheets.Workbook('ss');
workbook.collaboration.registerCollaborationType(type);

자세한 내용은 다음을 참고하세요: SpreadJS Sheets Collaboration packages

User

사용자 바인딩, 권한 설정, 동시 작업 상태 표시를 지원하기 위해 User 개념과 관련 API가 추가되었습니다. 참고: User.

구현 예제

예제 1: 페이지 내 두 Spread 인스턴스 동기화

var spread = new GC.Spread.Sheets.Workbook(document.getElementById('ss'));
var spread2 = new GC.Spread.Sheets.Workbook(document.getElementById('ss2'));
   
spread2.collaboration.fromSnapshot(spread.collaboration.toSnapshot());
spread.collaboration.onChangeSet((changeSet) => {
  spread2.collaboration.applyChangeSet(changeSet);
});
spread2.collaboration.onChangeSet((changeSet) => {
  spread.collaboration.applyChangeSet(changeSet);
});

예제 2: 상태 저장 및 복원

var spread = new GC.Spread.Sheets.Workbook(document.getElementById('ss'));

// 초기 Snapshot 저장
const initialSnapshot = spread.collaboration.toSnapshot();
if (!localStorage.getItem('snapshot')) {
   localStorage.setItem('snapshot', JSON.stringify(initialSnapshot)); 
}
applyChangeSet(spread);

// localStorage에 저장된 snapshot과 changeSet 적용
function applyChangeSet (spread) {
    const storedSnapshot = JSON.parse(localStorage.getItem('snapshot'));
    const storedChanges = JSON.parse(localStorage.getItem('changes') || '[]');

    if (storedSnapshot && spread) {
        spread.collaboration.fromSnapshot(storedSnapshot);

        spread.suspendPaint();
        storedChanges.forEach((changeSet) => {
            spread.collaboration.applyChangeSet(changeSet);
        });
        spread.resumePaint();
    }
}

// ChangeSet 수신 및 저장
spread.collaboration.onChangeSet((changeSet) => {
    let changes = JSON.parse(localStorage.getItem('changes') || '[]');
    changes.push(changeSet);
    localStorage.setItem('changes', JSON.stringify(changes));
});