-
Notifications
You must be signed in to change notification settings - Fork 3
데이터 관찰, 쿼리 발생 최적화 과정
현재 워크스페이스의 실시간 Node데이터를 보관하는 Y.Doc은
(1) Y.Doc의 데이터에 변경 사항이 하나라도 생길 경우
(2) 서버의 nodesMap.obeserve()
에 감지가 되어,
(3) 서버가 nodesMap에 존재하는 노드를 모두 순회하며 변경사항이 있는 노드를 찾은 뒤,
(4) 해당 변경사항을 Redis에 업데이트 해주며
사용자(들)의 문서 변경 사항을 DB로 업데이트 하고 있다.
하지만 해당 로직을 상용화해본 결과, 사용자가 캔버스에서 노드를 드래그하며 위치를 옮길때마다 서버가 해당 노드의 실시간 위치를 모두 감지, 너무 많은 데이터베이스 쿼리가 발생하여 서버가 다운되는 일이 생겼다.
어떻게 이 문제를 해결할까?
사용자가 노드를 드래그앤드롭으로 옮길 때 사용자의 행동을 관찰해보자. 사용자는 노드를 마우스 클릭으로 hold한뒤, 해당 노드가 옮겨질 위치를 선택한 후, 최종 위치에 마우스 클릭의 hold를 놓아 위치를 변경한다.
즉, 우리는 사용자가 노드를 hold하고 있지 않을 때의 위치 변경사항만 데이터베이스에 반영하면 되는 것이다!
const holdingNodeRef = useRef<string | null>(null);
useEffect(() => {
...
const yNodes = Array.from(nodesMap.values()) as YNode[];
const initialNodes = yNodes.map((yNode) => {
const nodeEntries = Object.entries(yNode).filter(
([key]) => key !== "isHolding",
);
return Object.fromEntries(nodeEntries) as Node;
});
...
const onNodeDragStart = useCallback(
(_event: React.MouseEvent, node: Node) => {
holdingNodeRef.current = node.id;
},
[],
);
const onNodeDragStop = useCallback(
(_event: React.MouseEvent, node: Node) => {
if (ydoc) {
const nodesMap = ydoc.getMap("nodes");
const yNode = nodesMap.get(node.id) as YNode | undefined;
if (yNode) {
nodesMap.set(node.id, { ...yNode, isHolding: false });
}
}
},
[ydoc],
);
- 이를 위해 클라이언트 쪽, 프런트엔드에서는 Y.Map에 넣을 노드 데이터에 isHolding 프로퍼티를 추가하여주었다.
- 사용자가 Drag를 시작할 때 False로 초기화되었던 노드의 isHolding 프로퍼티는 True가 되며, Drag를 멈췄을 때 해당 노드의 isHolding은 다시 False가 된다.
async isHoldingStatusChanged(
nodeId: number,
isHolding: boolean,
): Promise<boolean> {
const savedCacheValue = await this.get(nodeId);
return !!savedCacheValue && savedCacheValue.isHolding !== isHolding;
}
for await (const node of nodes) {
const { title, id } = node.data; // TODO: 이모지 추가
const { x, y } = node.position;
// 만약 캐쉬에 노드가 존재하지 않다면 갱신 후 캐쉬에 노드를 넣는다.
if (!this.nodeCacheService.has(id)) {
this.nodeService.updateNode(id, { title, x, y });
this.nodeCacheService.set(id, title);
return;
const isHolding = node.isHolding;
const updateCondition =
!(await this.nodeCacheService.has(id)) ||
!(await this.nodeCacheService.hasSameTitle(id, title)) ||
!(await this.nodeCacheService.isHoldingStatusChanged(
id,
isHolding,
));
if (updateCondition) {
await this.nodeService.updateNode(id, { title, x, y });
await this.nodeCacheService.set(id, { title, isHolding });
}
...
- 이제 서버에서는 위치 변경 데이터를 변경할 조건에
노드의 isHolding이 false일 때도 추가
하여 사용자가 위치 변경을 종료하였을 때만 해당 데이터를 DB에 변경한다. - 이제 사용자가 노드를 조금만 드래그 하더라도 수십개의 쿼리가 발생하는 상황을 관찰하지 않아도 된다!
하지만 여전히 우리는 node가 하나만 변경되더라도, 모든 노드의 정보를
// node의 변경 사항을 감지한다.
nodesMap.observe(async () => {
const nodes = Object.values(doc.getMap('nodes').toJSON());
// 모든 노드에 대해 검사한다.
for await (const node of nodes) {
const { title, id } = node.data; // TODO: 이모지 추가
const { x, y } = node.position;
const isHolding = node.isHolding;
const updateCondition =
!(await this.nodeCacheService.has(id)) ||
!(await this.nodeCacheService.hasSameTitle(id, title)) ||
!(await this.nodeCacheService.isHoldingStatusChanged(
id,
isHolding,
));
if (updateCondition) {
await this.nodeService.updateNode(id, { title, x, y });
await this.nodeCacheService.set(id, { title, isHolding });
}
}
});
이런 식으로 순회하며 조회해, 무엇이 변경되었는지를 찾아야한다. 즉, 데이터가 변경되지 않은 노드들까지 검사해야하는 것이다.
하지만 YMap이 제공해주는 observe 메소드에는 내부에서 변경된 key 값만 감지한 후 해당 value만 가져오는 기능
이 있었다!
// node의 변경 사항을 감지한다.
nodesMap.observe(async (event) => {
for (const [key, change] of event.changes.keys) {
if (change.action === 'update') {
const node: any = nodesMap.get(key);
const { title, id } = node.data; // TODO: 이모지 추가
const { x, y } = node.position;
const isHolding = node.isHolding;
if (!isHolding) {
await this.nodeService.updateNode(id, { title, x, y });
}
}
}
});
이런 식으로 변경 이벤트를 감지하면 해당 변경 이벤트가 발생한 노드까지 특정할 수 있어
바로 해당 노드의 정보를 변경할 수 있다.
이렇게 쿼리를 최적화하였고, 변경된 데이터를 관찰하는 과정을 단순화시켰다!
⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
✏️ 에디터
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)