![]()
Yorkie JavaScript SDK 란?
글을 작성하기에 앞서, Yorkie 라는 오픈소스 프로젝트에 대해 간략하게 소개하고자 한다. Yorkie 는 피그마, 구글 독스와 같이 여러 명의 클라이언트가 실시간으로 협업할 수 있도록 필요한 여러 기술들을 제공해주는 오픈소스이다. Yorkie JS SDK 는 Yorkie 에 존재하는 여러 프로젝트들 중 하나로, 개발자들이 JavaScript 환경에서 Yorkie 를 손쉽게 활용할 수 있도록 API, 개발자 도구, 문서 등 개발에 필요한 여러 요소들을 제공한다.
Broadcast 기능은 왜 도입하게 되었을까?
Yorkie의 핵심은 “여러” 사용자들이 “동시” 에 협업하는 데에 있다. (Google Docs에서 여러 명이 하나의 문서를 편집하는 장면을 떠올리면 쉽다.) 결국 Broadcast 도 이러한 본질에 더 충실하기 위해 도입된 기능이다. Broadcast 기능은 하나의 이벤트를 같은 문서를 편집 중인 모든 클라이언트들에게 “임의의” 이벤트를 전송할 수 있는 기능이다. 그렇다면 여기서 들 수 있는 한가지 의문점은 Broadcast 가 도입되기 이전에는 어떻게 협업을 했을까이다. 기존의 Yorkie에서는 문서와 관련된 이벤트 (즉, 문서를 생성/편집 하는 이벤트)만을 유저 간 전송할 수 있었다. 이걸 글로만 작성하면 잘 와닿지 않아서, 이해를 돕기위한 간단한 pseudo 코드를 통해 살펴보면 다음과 같다.
switch eventType {
// 문서가 생성되었을 때
case DOCUMENT_CREATED:
notify_other_users()
// 문서가 수정되었을 때
case DOCUMENT_CHANGED:
notify_other_users()
// 등등
}
client1.send(EventType.DOCUMENT_CREATED, document)즉, 미리 정의된 eventType 들이 있고 사용자들은 이러한 이벤트들 내에서만 협업을 할 수 있는 것이다. 그런데, 만약 댓글을 다는 기능을 구현해야 한다고 하자. 댓글은 문서의 일부가 아니기 때문에 기존의 이벤트만으로 커버할 수 없다. 따라서, 새롭게 추가될 댓글 이벤트를 위해 코드를 다음과 같이 수정해야할 것이다.
switch eventType {
// 생략
// 댓글을 달았을 때
case COMMENT_CHANGED:
notify_other_users()
}
client1.send(EventType.COMMENT_CHANGED, "안녕")그런데 앞으로 추가될 이벤트가 댓글 뿐이겠는가. 문서의 제목을 수정해야할 수도 있고, 채팅을 구현해야할 수도 있고… 이러한 방식은 확장성이 떨어질 수 밖에 없다. 이를 해결하기 위해 기존의 이벤트 타입에 BROADCAST 라는 새로운 이벤트 타입을 추가한다. 그리고 BROADCAST 이벤트를 전송할 때 payload 로 이벤트의 종류와 메시지를 담는다.
switch eventType {
// 생략
case BROADCAST:
notify_other_users()
}
client1.send(EventType.BROADCAST, {
topic: "COMMENT_CHANGED",
message: "안녕"
})비록 간단한 해결책이긴 하지만, 이 방식을 통해 기존에 다루지 못했던 다양한 이벤트들을 다룰 수 있게될 것이다. 참고로, 위 내용은 Broadcast 기능이 도입된 배경에 대해 이해를 돕기 위해 설명했지만 내가 구현한 부분은 아니다. (관련 PR) Yorkie 는 새로운 기능이 추가될 때 코어인 Yorkie 에 먼저 추가되고 이후에 JS SDK, iOS SDK, AOS SDK 프로젝트 등에 순차적으로 적용된다. 나는 그 중 JS SDK 에서 Yorkie 의 Broadcast 기능을 사용할 수 있도록 일종의 다리를 만든 셈이다.
기여 과정
Yorkie JS SDK 내 Broadcast 기능을 어떻게 구현했는지 기술적 디테일보다는 기여하기까지의 과정을 기록하는 것이 더 의미가 있다고 생각이 들었다. 따라서 Issue 를 할당받고 최종적으로 PR 이 머지되기까지의 과정을 적어보고자 한다.
1. TDD 도입하기
내가 맡은 스펙은 일반적인 프로젝트처럼 명확한 요구사항을 담은 기획안이 있지 않았다. 나의 미션은 단 하나. JS SDK 에서 Broadcast 가 되게끔 하면 되는 것이다. 다만, 아직 익숙하지 않은 기존의 코드들과 뭔가 추상적인 느껴지는 요구사항 속에서 도대체 어디서부터 시작하면 좋을지 감이 잘 잡히지 않았다. 일단 요구사항부터 먼저 명확히 정의하기로 했다. 이전에 NEXT STEP 에서 진행했던 TDD 과정에서 테스트 코드는 그 자체로 기능 명세서 역할을 한다고 했다. 그래서 일단 무작정 테스트 케이스부터 작성해보기로 했다.
일단 확실히 아는 정보는 Broadcast 를 하기 위해서는 payload 로 직렬화가 가능한(serializeable) 값을 전달해야 한다는 것, 그리고 만약 A 이벤트에 대한 Broadcast 가 발생했고, 클라이언트가 이미 A 이벤트를 구독(subscribe)하고 있다면, 구독할 때 등록해둔 이벤트 핸들러가 불려야한다.
it("Should successfully broadcast serializeable payload", () => {})
it("Should trigger the handler for a subscribed broadcast event", () => {})위의 정상적인 경우에 대한 테스트 케이스를 작성하면 자연스럽게 예외적인 상황에 대한 테스트 케이스들을 작성할 수 있다. 예를 들어, 직렬화가 불가능한(unserializeable) payload 를 전달하려고 할 경우에 에러가 발생해야할 것이고, A 이벤트에 대한 구독하지 않은 클라이언트에게는 A 이벤트가 발생해도 아무런 일이 발생하지 않을 것이다.
it("Should successfully broadcast serializeable payload", () => {})
it("Should throw error when broadcasting unserializeable payload", () => {})
it("Should trigger the handler for a subscribed broadcast event", () => {}) // 추가된 예외 케이스 1
it("Should not trigger the handler for an unsubscribed broadcast event", () => {}) // 추가된 예외 케이스 2그리고 나머지 테스트 케이스들은 필요할 때마다 하나하나씩 추가하였다. 추가한 이후에는 테스트 케이스들을 통과시키기 위해 노력하고 다시 테스트 케이스를 추가하고 구현하고의 사이클을 반복하였다.
2. 리뷰 받기
구현을 끝낸 뒤 메인테이너분들의 리뷰를 받는다. 새로운 기능인 만큼 꽤 많은 대화가 오갔는데, 주로 Broadcast의 인터페이스에 관한 내용이었다. 기존 사용자들이 혼란 없이 기능을 사용할 수 있도록, 인터페이스 설계와 호환성 유지에 관련하여 논의가 이루어졌다.
Broadcast함수는Client와Document중 누가 호출해야할까?
Yorkie 공식 문서에 따르면 Yorkie에는 아래와 같은 개념이 존재한다.
- Client는 서버와 통신하는 주체이다. 클라이언트는 문서에서 발생한 변경 사항을 서버와 동기화하여 실시간 협업을 가능하게 한다.
- Document는 Yorkie의 핵심 데이터 구조로, Conflict-free Replicated Data Types(CRDTs)을 기반으로 한다.
우선, Broadcast 는 서버를 거쳐야 하는 함수이므로, Client의 property 로 존재하는 것이 논리적으로 적합하다. 따라서 처음에는 client.broadcast() 형태로 호출하는 방식으로 구현을 하였다.
class Client {
private id: string;
public broadcast(docKey: DocumentKey, topic: string, payload: Json){
this.rpcClient.broadcast({
clientId: this.id,
documentId: attachment.docID
topic,
payload: new TextEncoder().encode(JSON.stringify(payload))
})
}
}
const client = new Client('123')
client.broadcast('DOCUMENT_ID', "COMMENT_CHANGED", "hello")하지만, Broadcast 이벤트를 구독하는 과정은 Document에서 발생한다. (subscribe 는 기존에 존재했던 함수를 재사용한 것이다.)
const document = new Document()
const unsubscribe = document.subscribe("broadcast", (event) => {
// 이벤트 핸들러
})이렇다 보니 broadcast를 구독하는 동작은 Document에서 이루어지지만, 실제 broadcast 호출은 Client에서 수행되기 때문에 두 부분이 서로 일관되지 않다는 문제가 생긴다. 이러한 부분을 해결하기 위해 결론적으로 broadcast 또한 Document에서 불리도록 수정하였다. (여기서 순환 참조등 다양한 문제들을 헤쳐나가야 했지만 생략하기로 한다…)
Subscribe인터페이스 통일하기
기존의 subscribe 함수는 다음과 같이 첫 번째 파라미터로 이벤트 타입을 받는다.
public subscribe(
type: 'connection',
next: DocEventCallbackMap<P>['connection'],
error?: ErrorFn,
complete?: CompleteFn
): Unsubscribe;broadcast의 경우 단일 이벤트 아래 다양한 topic이 올 수 있기 때문에, 기존의 subscribe 인터페이스와는 달리 첫 번째 파라미터로 이벤트 이름과 토픽을 함께 전달하도록 설계했다. 이렇게 하면 사용자가 특정 토픽에 대해 직접 비교 작업을 하지 않고도 원하는 이벤트를 간편하게 처리할 수 있다는 장점이 있었다.
그러나 broadcast 이벤트를 구독할 때만 다른 인터페이스를 적용하여 일관성을 해치는 점이 마음에 걸렸다. 마침 다른 이벤트에서도 사용자들이 직접 조건을 비교하여 이벤트를 처리하고 있는 선례를 발견하여, 기존의 subscribe 인터페이스를 유지하면서 사용자가 직접 topic 을 비교하도록 인터페이스를 수정하였다.
// 변경 전
document.subscribe({ type: "broadcast", topic: "TOPIC_NAME" }, (topic, payload) => {
// Handle the broadcast event for the specified topic `
})
// 변경 후
document.subscribe("BROADCAST", (event) => {
const { topic, payload } = event.value
if (topic === broadcastTopic1) {
// 사용자가 직접 topic 을 비교
}
})머지🎊
이러한 과정을 거쳐 최종적으로 컨펌을 받게되면, main 브랜치로 머지되게 된다.
이슈: https://github.com/yorkie-team/yorkie/issues/628
PR: https://github.com/yorkie-team/yorkie-js-sdk/pull/884
