CrescentCrescent
PostsTagsAbout

> OSSCA Yorkie(2) - 15분 만에 동시편집 에디터 구현하기

Crescent
#210718

00. 서론

# 15분 만에 동시편집 에디터 구현하기

01. CodeMirror 에디터 만들기

CodeMirror

/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title></title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.6/codemirror.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.6/codemirror.min.css" />
  </head>
  <body>
    <textarea id="codemirror" name="" cols="30" rows="10"></textarea>
    <script src="./main.js"></script>
  </body>
</html>
  1. 디렉토리에 index.html 만들기
  2. https://cdnjs.com/libraries/codemirror 들어가서 js, css코드 복사
  3. <textarea/><body/>에 추가 → id="codemirror"로 만들어준다
  4. main.js만들기
main.js
console.log('hit');
const editor = CodeMirror.fromTextArea(document.getElementById('codemirror'), {
  lineNumbers: true,
});

02. Yorkie Client, Document 생성

  1. Yorkie JS SDK받고 서버 실행
bash
git clone https://github.com/yorkie-team/yorkie-js-sdk.git
docker-compose.yml
docker-compose -f docker/docker-compose.yml up --build -d
  1. Yorkie JS SDK 빌드 → 빌드 안해도 됩니다! yorkie-js-sdk가 CDN에 올라왔다는 댓글 https://cdnjs.com/libraries/yorkie-js-sdk
/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title></title>
    <!-- 여기 밑에다가 추가합시다 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/yorkie-js-sdk/0.2.11/yorkie-js-sdk.js"></script>
  </head>
  <body>
    <textarea id="codemirror"></textarea>
    <script src="./main.js"></script>
  </body>
</html>
  1. Yorkie Client 생성, 활성화 & Yorkie Document 생성 & Text 생성
/main.js
async function main() {
  console.log('hit');
  const editor = CodeMirror.fromTextArea(document.getElementById('codemirror'), {
    lineNumbers: true,
  });
  const client = new yorkie.Client('http://localhost:8080');
  await client.activate();
 
  const doc = new yorkie.Document('docs', 'doc1');
  await client.attach(doc);
 
  doc.update((root) => {
    if (!root.content) {
      root.content = new yorkie.Text();
    }
  });
}
main();
#210719

03. CodeMirror와 Yorkie 서로 연동(바인드)

전체적인 에디터 구조
전체적인 에디터 구조

CodeMirror to Yorkie / Yorkie to CodeMirror

> phase 1

/main.js
// doc.update((root) => {});
// (1) CodeMirror
editor.on('beforeChange', (cm, change) => {
  console.log(change);
});
// (2) Yorkie
doc.getRoot().content.text.onChanges((changes) => {
  console.log(changes);
});
  1. CodeMirror 에디터 쪽에 Handler를 추가한다.
  1. Yorkie 쪽에서도 Handler를 추가한다
  1. Handler가 제대로 붙었는지 확인하려면 index.html파일 열어서 편집을 했을 때 콘솔이 찍히는지 확인한다

> phase 2

/main.js
// doc.update((root) => {});
 
editor.on('beforeChange', (cm, change) => {
  // (1)
  if (change.origin === 'yorkie' || change.origin === 'setValue') {
    return;
  }
  console.log(change);
 
  // (2)
  const from = editor.indexFromPos(change.from);
  const to = editor.indexFromPos(change.to);
  // (3)
  const content = change.text.join('\n');
  // (4)
  doc.update((root) => {
    root.content.edit(from, to, content);
  });
});
  1. 변경사항들이 Yorkie 변경 사항과 CodeMirror 변경 사항이 서로 핑퐁 될 수 있기 때문에 필터 조건을 넣어야 한다.
  1. CodeMirror에서 사용하는 Pos라는 타입이 있는데 커서의 위치 같은 것을 나타낸다.
  1. CodeMirror에서 Text가 여러 줄일 때 배열로 와서 처리를 해야 한다.
  2. Yorkie Document를 업데이트

> phase 3

/main.js
doc.getRoot().content.text.onChanges((changes) => {
  console.log(changes);
  // (1)
  for (const change of changes) {
    // (2)
    if (change.type !== 'content' || change.actor === client.getID()) {
      continue;
    }
    // (3)
    const from = editor.posFromIndex(change.from);
    const to = editor.posFromIndex(change.to);
 
    // (4), (5)
    addChange(editor, from, to, change.content || '');
    // editor.replaceRange(change.content, from, to, 'yorkie')
  }
});

Yorkie to CodeMirror 핸들러 구현

  1. changes가 배열로 오기 때문에 반복문을 만든다.
  2. 자신의 편집을 반영할 필요는 없으니 본인을 필터하고 컨텐츠 변경이 아닌 경우 필터링
  3. Posindex로 바꿨는데 이번엔 indexPos로 바꿔야 한다.
  1. CodeMirror에서는 replaceRange라는 것으로 스크립트로 변경사항을 넣어줄 수 있다.
  1. CodeMirror의 replaceRange가 원격 편집을 고려하지 않은 경우,,,
/main.js
// https://github.com/codemirror/codemirror5/pull/5619
function addChange(editor, from, to, text) {
  let adjust = editor.listSelections().findIndex(({ anchor, head }) => {
    return CodeMirror.cmpPos(anchor, head) == 0 && CodeMirror.cmpPos(anchor, from) == 0;
  });
  editor.operation(() => {
    editor.replaceRange(text, from, to, 'yorkie');
    if (adjust > -1) {
      let range = editor.listSelections()[adjust];
      if (range && CodeMirror.cmpPos(range.head, CodeMirror.changeEnd({ from, to, text })) == 0) {
        let ranges = editor.listSelections().slice();
        ranges[adjust] = { anchor: from, head: from };
        editor.setSelections(ranges);
      }
    }
  });
}

> phase 4

  1. CodeMirror에서 제공하는 메소드를 호출해 초기값을 넣어준다.
/main.js
doc.getRoot().content.text.onChanges((changes) => {
  console.log(changes);
  // ...
  editor.setValue(doc.getRoot().content.toString());
});

04. 결론

동시편집기 결과물
동시편집기 결과물

04. 전체 코드

yorkie-editor-example/CodeMirror