-
Notifications
You must be signed in to change notification settings - Fork 4
실시간 편집 구현 과정
Yjs는 자동으로 동기화되는 협업 애플리케이션을 구축하기 위한 고성능 CRDT 라이브러리이다.
내부 CRDT 모델을 동시에 조작할 수 있는 공유 **데이터 타입을 제공한다.
공유 타입(Shared Types)은 Array
, Map
또는 Set
과 같은 일반 데이터 유형과 유사하다.
자동으로 상태를 동기화하고, 변경이 발생하면 이벤트를 발생시키고, 병합 충돌 없이 자동으로 병합할 수 있다.
![](https://private-user-images.githubusercontent.com/102349522/391862866-9ff8b9a1-8c48-41dc-9689-43601e0d7c1f.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg4NjA2MjQsIm5iZiI6MTczODg2MDMyNCwicGF0aCI6Ii8xMDIzNDk1MjIvMzkxODYyODY2LTlmZjhiOWExLThjNDgtNDFkYy05Njg5LTQzNjAxZTBkN2MxZi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwMjA2JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDIwNlQxNjQ1MjRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04ZTU2YTg5Njk3ZGQ5NGUxYWRiN2VhNjFhZDkxZWVmMmYyOWM1NzhlNzM2M2NmNmJmYmFjNmMyNzU3M2EzYTk4JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.30DrjBBnNgnjCC7pv87FEAUWb8pJihX6l2teBcIkySU)
네트워크 공급자(e.g. WebSocket, WebRTC)에 YDoc을 전달하여 문서 업데이트를 다른 피어에 동기화하면 된다.
네트워크 공급자는 문서 업데이트를 다른 피어에 동기화한다.
// 클라이언트
import { SocketIOProvider } from "y-socket.io";
import * as Y from "yjs";
export const createSocketIOProvider = (roomname: string, ydoc: Y.Doc) => {
return new SocketIOProvider(
import.meta.env.VITE_WS_URL,
roomname,
ydoc,
{
autoConnect: true,
disableBc: false,
},
{
reconnectionDelayMax: 10000,
timeout: 5000,
transports: ["websocket", "polling"],
},
);
};
// 서버
this.ysocketio = new YSocketIO(this.server, {
gcEnabled: true,
});
this.ysocketio.initialize();
this.ysocketio.on('document-loaded', async (doc: Y.Doc) => {
// 피어 간에 동기화되는 ydoc을 서버에서도 관리할 수 있다.
...
}
우리 프로젝트 목표는 라이브러리를 적극 사용해서 빠르게 배포하고, 유저 QA로 서비스를 발전시켜 완성도 높은 서비스를 만드는 것이었다.
이를 위해, 노드와 엣지 캔버스를 위해 React Flow와 노션과 비슷한 강력한 기능을 제공하는 에디터를 위해 Novel을 이용했는데, 이 두 라이브러리와 잘 맞고 …
Provider에 YDoc을 설정하고, 웹소켓 서버에 연결한다. → 피어 간 동기화!!
const { ydoc } = useYDocStore(); // 전역으로 관리되는 YDoc
const wsProvider = createSocketIOProvider(`flow-room-${workspace}`, ydoc); // Provider
Y.Text
: 에디터의 제목, 이모지
Y.Map
: 노드와 엣지
Y.XmlFragment
: 에디터 컨텐츠
피어 간 공유되는 Y.Map
에 새로운 노드를 추가하고, 이 Y.Map
을 React Flow가 제공하는 useNodesState
훅으로 캔버스에 렌더링한다.
createMutation
.mutateAsync(...)
.then((res) => {
const nodesMap = ydoc.getMap("nodes");
nodesMap.set(res.pageId.toString(), {
id: res.pageId.toString(),
type: "note",
data: {
title: "제목 없음",
id: res.pageId,
emoji: "",
},
position: { x: 0, y: 0 },
selected: false,
isHolding: false,
});
})
- 서버에
POST
요청하고 생성된 노드id를 response로 받는다 -
nodesMap
에 노드에 대한 정보를 추가한다.
Y.Map
의 observe
메서드로 피어 간 공유되는 Y.Map
의 변경을 감지할 수 있다.
https://docs.yjs.dev/api/shared-types/y.map#observing-changes-y.mapevent
https://docs.yjs.dev/api/y.event
react flow가 제공하는 useNodesState
훅으로 캔버스에 노드들을 렌더링을 할 수 있다.
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
nodesMap.observe((event) => {
...
event.changes.keys.forEach((change, key) => {
const nodeId = key;
if (change.action === "add") {
const updatedYNode = nodesMap.get(nodeId) as YNode;
const updatedNode = Object.fromEntries(updatedNodeEntries) as Node;
queryClient.invalidateQueries({ queryKey: ["pages"] }); // ??
setNodes((nds) => {
const index = nds.findIndex((n) => n.id === nodeId);
if (index === -1) {
return [...nds, updatedNode];
}
const newNodes = [...nds];
newNodes[index] = {
...updatedNode,
selected: newNodes[index].selected,
};
return newNodes;
});
}
});
});
react flow의 캔버스가 제공하는 onNodesChange
이벤트 핸들러로 노드 상태 변경을 감지하고, 이를 Y.Map에 반영한다.
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
if (!ydoc) return;
const nodesMap = ydoc.getMap("nodes");
const edgesMap = ydoc.getMap("edges");
changes.forEach((change) => {
if (change.type === "position" && change.position) {
const node = nodes.find((n) => n.id === change.id);
if (node) {
const updatedYNode: YNode = {
...node,
position: change.position,
selected: false,
isHolding: holdingNodeRef.current === change.id,
};
nodesMap.set(change.id, updatedYNode);
}
}
});
onNodesChange(changes);
},
[nodes, edges, onNodesChange],
);
⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
✏️ 에디터
Novel이란?
Novel 스타일링 문제
에디터 저장 및 고려 사항들
📠 실시간 협업, 통신
Yorkie와 Novel editor 연동
YJS, Websocket, React-Flow
YJS, Socket.io
WebSocket과 Socket.io에 대해 간단히 알아보기
YJS 가이드 근데 이제 Socket.io를 곁들인
🏗️ 인프라와 CI/CD
NCloud CI CD 구축
BE 개발 스택과 기술적 고민
private key로 원격 서버 접근
nCloud 서버, VPC 만들고 설정
monorepo로 변경
⌛ 캐시, 최적화
rabbit mq 사용법
🔑 인증, 인가, 보안
passport로 oAuth 로그인 회원가입 구현
FE 로그인 기능 구현
JWT로 인증 인가 구현
JWT 쿠키로 사용하기
refresh token 보완하기
🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략
🌤️ 데일리 스크럼
📑 회의록
1️⃣ 1주차
킥오프(10/25)
2일차(10/29)
3일차(10/30)
4일차(10/31)
2️⃣ 2주차
8일차(11/04)
9일차(11/05)
11일차(11/07)
13일차(11/09)
3️⃣ 3주차
3주차 주간계획(11/11)
16일차(11/12)
18일차(11/14)
4️⃣ 4주차
4주차 주간계획(11/18)
23일차(11/19)
24일차(11/20)
25일차(11/21)
5️⃣ 5주차
5주차 주간계획(11/25)
29일차(11/25)
32일차(11/28)
34일차(11/30)
6️⃣ 6주차
6주차 주간계획(12/2)
37일차(12/3)