> OSSCA Yorkie(2) - 15분 만에 동시편집 에디터 구현하기
Crescent
00. 서론
- Yorkie를 사용해서 에디터를 만드는 영상이 있어서 이를 따라해보며 정리를 해보았다.
- 영상에 나오는 내용이 내가 지금 할 때랑 다른 환경 상태도 있어서 정리할 겸,,,
- 참고하기 좋은 문서: yorkie-js-sdk/examples/index.html
01. CodeMirror 에디터 만들기
<!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>- 디렉토리에
index.html만들기 - https://cdnjs.com/libraries/codemirror 들어가서
js,css코드 복사 <textarea/>을<body/>에 추가 →id="codemirror"로 만들어준다main.js만들기
console.log('hit');
const editor = CodeMirror.fromTextArea(document.getElementById('codemirror'), {
lineNumbers: true,
});- 이러고
index.html을 실행시키면
잘 나온다!
02. Yorkie Client, Document 생성
Yorkie JS SDK받고 서버 실행
git clone https://github.com/yorkie-team/yorkie-js-sdk.git- 클론!
docker-compose -f docker/docker-compose.yml up --build -dcompose를 이용해Docker를 띄움- Yorkie가
DB도 사용하고 있고,grpc-web을 써서Envoy같은 것들이 뜰 수 있다고 한다.
Yorkie JS SDK빌드 → 빌드 안해도 됩니다!
https://cdnjs.com/libraries/yorkie-js-sdk
- 스크립트? 데려오기만 하면 되는
<!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>cdnjs에서 데려와서 추가하기
- Yorkie Client 생성, 활성화 & Yorkie Document 생성 & Text 생성
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();- 영상에서는
yorkie.creativeClient()인데yorkie.Client()이어야 한다.constructor Client cannot be invoked without 'new'라고 해서 앞에new를 붙여야 한다.yorkie.createDocument→yorkie.Document, 앞에new를 붙여야 함
- document는 적당한 이름인
doc await을 썼기 때문에async await을 해줘야 함 → 함수를 만들었음- 이러면 Yorkie 쪽에 문서가 붙어서 클라이언트에 붙어서 만들어져야 한다
Text데이터 타입을 만들어 CodeMirror의 변경사항들이Yorkie에 저장될 수 있도록 한다.content라는 키: 없을 때만 만들어준다Text데이터 타입을content라는 키로 만들게 되었다root.createText('context')→root.content = new yorkie.Text()
03. CodeMirror와 Yorkie 서로 연동(바인드)

- CodeMirror와 Yorkie Document사이에서 바인드(bind) 되어 서로 주고 받아줘야 함
- 서로 주고 받으려면
Handler가 필요함Handler: 특정 유형의 데이터에 특화되어 있거나 특정 특수 작업에 초점을 맞춘 루틴/기능/태그
Document: 응용프로그램의 모델이 표현되는 CRDT 기반 데이터 유형. 클라이언트는 오프라인 상태에서도 편집할 수 있다.
CodeMirror to Yorkie / Yorkie to CodeMirror
- CodeMirror to Yorkie: CodeMirror의 변경사항이 Yorkie쪽으로 전달되게 하는 부분
- Yorkie to CodeMirror: Yorkie쪽 변경 사항을 CodeMirror로 반영되게 하는 부분
> phase 1
// doc.update((root) => {});
// (1) CodeMirror
editor.on('beforeChange', (cm, change) => {
console.log(change);
});
// (2) Yorkie
doc.getRoot().content.text.onChanges((changes) => {
console.log(changes);
});- CodeMirror 에디터 쪽에
Handler를 추가한다.
change를 받아 추가change를 console로 찍어본다
- Yorkie 쪽에서도
Handler를 추가한다
Text쪽에서도changes를 받아 처리Text가 Yorkie Document쪽에content라는 이름이 있음changes를console.log에 넣어 출력
Handler가 제대로 붙었는지 확인하려면index.html파일 열어서 편집을 했을 때 콘솔이 찍히는지 확인한다
- 잘 찍히면
Handler가 제대로 달린 것이다! CodeMirror쪽console.log가 나온 것으로 생각
> phase 2
// 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);
});
});- 변경사항들이 Yorkie 변경 사항과 CodeMirror 변경 사항이 서로 핑퐁 될 수 있기 때문에 필터 조건을 넣어야 한다.
change.origin === 'yorkie',change.origin === setValueyorkie나setValue라는 이름으로 변경이 일어날 때도 필터링
- CodeMirror에서 사용하는
Pos라는 타입이 있는데 커서의 위치 같은 것을 나타낸다.
- 이 것을 숫자 인덱스를 바꿔야 한다 →
indexFromPos를 사용해서 변경 - 수정을 할 때 영역을 잡아서 편집할 수 있기 때문에
range로from,to로 만들어 준다
- CodeMirror에서
Text가 여러 줄일 때 배열로 와서 처리를 해야 한다. - Yorkie Document를 업데이트
root.content.edit호출
> phase 3
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 핸들러 구현
changes가 배열로 오기 때문에 반복문을 만든다.- 자신의 편집을 반영할 필요는 없으니 본인을 필터하고 컨텐츠 변경이 아닌 경우 필터링
Pos를index로 바꿨는데 이번엔index를Pos로 바꿔야 한다.
editor.posFromIndex사용range이여서 영역을 잡아줘야 한다
- CodeMirror에서는
replaceRange라는 것으로 스크립트로 변경사항을 넣어줄 수 있다.
- Yorkie에서 변경한 것이기 때문에 origin을
yorkie로 한다.
- CodeMirror의
replaceRange가 원격 편집을 고려하지 않은 경우,,,
// 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);
}
}
});
}- 해당 이슈
- 괜찮은 방법이 없는 것 같다?
yorkie-js-sdk예제도 이 코드를 사용 중 editor.replaceRange(text, from, to, 'yorkie');←yorkie넣어야 한다.- 아무튼 이것을
function main()앞에 추가
- 괜찮은 방법이 없는 것 같다?
> phase 4
- 새로고침을하면 저장된 내용들이 반영이 안된다.
- CodeMirror에서 제공하는 메소드를 호출해 초기값을 넣어준다.
doc.getRoot().content.text.onChanges((changes) => {
console.log(changes);
// ...
editor.setValue(doc.getRoot().content.toString());
});TypeError: e.split is not a function에러 →toString()붙이기
04. 결론

- 얏호
Crescent