CrescentCrescent
PostsTagsAbout

> OSSCA Yorkie TIL(5) - 20분 만에 동시편집 Quill 에디터 구현하기

Crescent

0. 서론

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

Quill에는 글자 스타일, 사진 넣기와 같은 기능이 있지만, 영상 속 CodeMirror에디터 처럼 텍스트 입력 기능만 진행하고자 합니다.


01. Quill 에디터 만들기

Quill

/index.html
<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="UTF-8" />
	<title></title>
	<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
	<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet" />
</head>
<body>
	<div id="editor" style="height: 400px"></div>
	<script src="./main.js"></script>
</body>
</html>
  1. 디렉토리에 index.html 만들기
  2. https://quilljs.com/docs/download/ 들어가서 quill.js, quill.snow.css 코드 복사
    • quill.snow.css: 테마
  3. <div><body>에 추가
    • id="editor"
  4. main.js 만들어 Quill 에디터와 연결
/main.js
console.log('hit');
const quill = new Quill('#editor', {
	modules: {
		toolbar: [],
	},
	placeholder: 'Compose an epic...',
	theme: 'snow',
});
기본적인 Quill 에디터의 모습
기본적인 Quill 에디터의 모습

02. Yorkie Client, Document 생성

  1. Yorkie JS SDK받고 서버 실행
terminal
git clone https://github.com/yorkie-team/yorkie-js-sdk.git
terminal
docker-compose -f docker/docker-compose.yml up --build -d
  1. Yorkie CDN 추가
/index.html
<!DOCTYPE html>
<html lang="ko">
<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>
	<div id="editor" style="height: 400px"></div>
	<script src="./main.js"></script>
</body>
</html>
  1. Yorkie Client 생성, 활성화 & Yorkie Document 생성 & Text 생성
/main.js
async function main() {
	console.log('hit');
	// 01. create an instance of Quill
	const quill = new Quill('#editor', {
		modules: {
			toolbar: [],
		},
		placeholder: 'Compose an epic...',
		theme: 'snow',
	});
 
	// 02. create client with RPCAddr(envoy) then activate it.
	const client = new yorkie.Client('http://localhost:8080');
	await client.activate();
	
	// 03. create a document then attach it into the client.
	const doc = new yorkie.Document('quill');
	await client.attach(doc);
	  
	doc.update((root) => {
		if (!root.content) {
			root.content = new yorkie.RichText();
		}
	});
}
main();

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

전체적인 Yorkie의 모습
전체적인 Yorkie의 모습

Quill to Yorkie / Yorkie to Quill

> PHASE 1

/main.js
// doc.update((root) => {});
 
// (1) Quill
quill.on('text-change', (delta, oldDelta, source) => {
	console.log('Quill:', JSON.stringify(delta.ops));
});
  
// (2) Yorkie
doc.getRoot().content.text.onChanges((changes) => {
	console.log('Yorkie: ', JSON.stringify(changes));
});
  1. Quill 쪽에서 Handler를 추가한다.
    • CodeMirrorchange라면 Quilldelta.ops
    • delta.opsconsole.log로 찍어본다. JSON.stringfy()는 보기 쉬우라고 한 것
  2. Yorkie쪽에서도 Handler를 추가한다
    • doc.getRoot().content에는 text라는 항목이 존재
    • Text쪽에서도 changes를 받아 처리
    • Text가 Yorkie Document쪽에 content라는 이름이 있다.
    • changesconsole.log에 넣어 출력한다.
  3. Handler가 제대로 붙었는지 확인하려면 index.html파일 열어서 편집을 했을 때 콘솔이 찍히는지 확인한다
    • 콘솔에 잘 찍히면 Handler가 제대로 달린 것이다!
Quill쪽에 console.log가 나오는 모습
Quill쪽에 console.log가 나오는 모습

> phase 2: Quill쪽부터 작업

/main.js
// doc.update((root) => {})
 
// 04. bind the document with the Quill.
// 04-1. Quill to Document.
quill.on('text-change', (delta, oldDelta, source) => {
	if (source === 'yorkie' || !delta.ops) {
		return;
	}
	// (2)
	let from = 0;
	let to = 0;
	for (const op of delta.ops) {
		if (op.insert !== undefined) {
			// (2)
			if (op.retain !== undefined) {
				to = from + op.retain;
			}
			if (to < from) {
				to = from;
			}
			console.log(`%c insert: ${from}-${to}: ${op.insert}`, 'color: green');
			// (4)
			doc.update((root) => {
				root.content.edit(from, to, op.insert, op.attributes);
			});
			from = to + op.insert.length;
		} else if (op.delete !== undefined) {
			//  (2)
			to = from + op.delete;
			console.log(`%c delete: ${from}-${to}: ''`, 'color: red');
			// (4)
			doc.update((root) => {
				root.content.edit(from, to, '');
			});
		} else if (op.retain !== undefined) {
			// (2)
			from = to + op.retain;
			to = from;
		}
	}
});
  1. 변경 사항이 없거나 Yorkie 변경 사항과 Quill 변경 사항이 서로 핑퐁 될 수 있기 때문에 조건을 넣어야 한다.
    • source === 'yorkie' || !delta.ops
  2. Quill에서는 tofrom를 구해주는 것이 없기 때문에 직접 분기를 나누면서 진행해야 한다.
    • insert / delete / retain 콘솔로 찍혀나오는 op들의 모습
  3. Quill에서는 들어오는 텍스드(op.insert)가 string으로 들어오기 때문에 그냥 집어넣으면 된다. 콘솔로 찍혀나오는 insert의 모습
  4. Yorkie Document를 업데이트
    • root.content.edit 호출

> phase 3: Yorkie to Quill 핸들러 구현

/main.js
function toDelta(change) {
	const { embed, ...attributes } = change.attributes ?? {};
	  
	const delta = embed
		? { insert: JSON.parse(embed), attributes }
		: {
		insert: change.content || '',
		attributes: change.attributes,
	};
	return delta;
}
// async function main() {
/main.js
// 04-2. document to Quill(remote).
doc.getRoot().content.text.onChanges((changes) => {
	console.log('Yorkie: ', JSON.stringify(changes));
	// (1)
	const delta = [];
	let prevTo = 0;
	for (const change of changes) {
		// (2)
		if (change.type !== 'content' || change.actor === client.getID()) {
			continue;
		}
		// (3)
		const from = change.from;
		const to = change.to;
		const retainFrom = from - prevTo;
		const retainTo = to - from;
		// (4)
		const { insert, attributes } = toDelta(change);
		console.log(`%c remote: ${from}-${to}: ${insert}`, 'color:green');
		if (retainFrom) {
			delta.push({ retain: retainFrom });
		}
		if (retainTo) {
			delta.push({ delete: retainTo });
		}
		if (insert) {
			const op = { insert };
			if (attributes) {
				op.attributes = attributes;
			}
			delta.push(op);
		}
		prevTo = to;
	}
	// (4)
	if (delta.length) {
		console.log(`%c to quill: ${JSON.stringify(delta)}`, 'color: green');
		quill.updateContents(delta, 'yorkie');
	}
});
  1. changes가 배열로 오기 때문에 반복문을 만든다.
    • 반복문을 돌면서 to를 체크해야 하고, 내용과 변화를 설명하는 데이터인 delta를 배열로 생성.
    • 사실 텍스트 입력만 다루는 상황에서는 attributesundefined이긴 하다.
delta
{ insert: 'Gandalf', attributes: { bold: true } },
{ insert: ' the ' },
{ insert: 'Grey', attributes: { color: '#cccccc' } }
  1. 자신의 편집을 반영할 필요는 없으니 본인을 필터하고 컨텐츠 변경이 아닌 경우 필터링한다
    • Quill에서는 content.type가 다양하게 있으나, content.type === 'content'일 때(텍스트 편집)만 다룬다.
  2. 변경사항을 나타내는 delta가 비어있지 않은 경우 그 deltaquill에 집어넣는다.
    • quill.updatecontents() 사용

> phase 4: 새로고침을 했는데 반영이 안됩니다.

/main.js
function toDeltaList(doc) {
	const obj = doc.getRoot();
	const deltas = [];
	for (const val of obj.content.values()) {
		deltas.push(toDelta(val));
	}
	return deltas;
}
// async function main() {
/main.js
// doc.getRoot().content.text.onChanges((changes) => {});
 
// 05. synchronize text of document and Quill.
quill.setContents(
	{
		ops: toDeltaList(doc),
	},
	'yorkie',
);
동시 편집이 되는 quill 에디터의 모습
동시 편집이 되는 quill 에디터의 모습

04. 전체 코드

yorkie-editor-example/Quill


05. 참고 문서