회사에서 "CRDT라는 게 있으니 이걸 한 번 검토해서 폼 화면 실시간 공유에 써보자"라고 내려왔고,
그때부터 내가 뭐를 어떻게 파기 시작했는지 정리해 둔 기록.
나중에 비슷한 요구가 다시 나왔을 때
"그때 어떻게 붙였지?"를 빠르게 복기하려고 남겨두는 글이다.
1. 왜 이걸 하게 됐는지
출발점은 아주 단순했다.
- 상담/멘토링 화면에서 폼 입력 상태를 실시간으로 공유하고 싶다
- CRDT라는 개념이 있으니 이쪽을 한 번 조사해서 활용해 봐달라
채팅처럼 텍스트만 왔다 갔다 하는 수준이 아니라
- 텍스트 인풋 값
- 체크박스, 셀렉트 선택 상태
- 진행 단계
같은 것들을 두 화면에서 동시에 보고 싶다는 요구였다.
여기에 현실적인 제약이 몇 가지 붙었다.
- 네트워크 환경이 항상 좋지 않다
- 브라우저 탭을 여러 개 띄울 수 있다
- 재접속했을 때 상태를 자연스럽게 맞춰야 한다
그래서 자연스럽게 키워드가 이렇게 모였다.
- CRDT
- Yjs, Automerge
- WebSocket 서버
- 실험용 클라이언트(Next)
이 시점부터는 문서만 읽고 끝낼 수 없어서,
"일단 손으로 만들어 보면서 이해하자" 쪽으로 방향을 잡았다.
2. CRDT 방향 잡기 (Yjs vs Automerge)
"CRDT를 써보자"까지는 회사 쪽에서 방향이 내려온 상태였고,
어떤 라이브러리와 조합으로 갈지는 내가 정해야 했다.
CRDT 라이브러리 후보는 사실상 두 개로 좁혀졌다.
- Yjs
- Automerge
둘 다 "충돌 없는 병합"을 목표로 하지만, 성격은 꽤 다르다.
그때 기준은 이 정도로 잡았다.
- 실시간 폼/문서 편집에 얼마나 잘 맞는지
- 예제와 생태계, 참고 자료가 얼마나 많은지
- 커서/선택 상태 같은 것도 같이 공유하기 쉬운지
- 나중에 회사 인프라(WebSocket, 저장소 등)에 붙이기 편한지
결론은 Yjs 쪽으로 기울었다.
- 실시간 협업 쪽 예제가 많고
- Y.Text, Y.Map 같은 전용 타입들이 폼 상태 관리에 잘 맞고
- Awareness 기능으로 커서/선택 상태 공유까지 고려할 수 있었다.
또 Yjs는 네트워크 레이어에 꽤 독립적인 편이라
처음에는 로컬 WebSocket 서버로만 돌려 보다
나중에 회사 WebSocket 인프라에 그대로 옮겨 붙이는 그림을 그리기 좋았다.
3. WebSocket 서버 처음부터 띄워 보기 (로컬 싱글 버전)
CRDT 라이브러리를 정했다고 해서 끝은 아니었다.
결국 상태 변경 델타를 주고받을 통로가 필요하다.
- 변경 사항을 어디로 보낼지
- 각 클라이언트는 어디에서 상태를 받아올지
이걸 직접 손으로 만들어 봐야 해서, WebSocket 서버부터 한 번 띄워 보기로 했다.
여기서 첫 번째 장벽이 나왔다.
- 그 전까지 WebSocket 서버를 처음부터 만들어 본 적이 없었다.
그래서 가장 처음에 한 일은 최대한 단순하게 가는 거였다.
- 로컬에서 Node 기반 WebSocket 서버를 하나 띄우고
- Next.js 페이지 하나에서 그 서버에 붙어서 메시지를 주고받아 보기
이게 1번 목표였다.
3.1 로컬 테스트 구조
처음에 구성했던 구조는 대략 이런 느낌이었다.
- 서버
- Node로 만든 WebSocket 서버 한 개(싱글)
- 네임스페이스 같은 건 전혀 고려하지 않고, 접속한 클라이언트끼리 브로드캐스트
- 클라이언트
- Next.js로 테스트용 페이지 생성
- Yjs 문서를 만들고 WebSocket을 통해 델타를 주고받는지 확인
이 단계에서는 회사 인프라 같은 건 일부러 신경 안 썼다.
- 연결/재연결이 잘 되는지
- 브라우저 탭을 여러 개 열었을 때 상태가 제대로 맞춰지는지
- 새로고침했을 때 문서가 잘 복구되는지
이 정도만 확인하면서 "아, 이 조합이 이런 식으로 동작하는구나" 하는 감을 먼저 잡는 단계였다.
4. 중간에 했던 삽질 메모
크게 막힌 건 아니었는데, 중간중간 손이 많이 갔던 부분들을 기억나는 선에서 메모해 둔다.
- WebSocket 연결 생명주기 감 잡기
- 탭을 여러 개 열었다가 닫았다가 할 때
- 이벤트 등록과 해제가 꼬이지 않게 정리하는 데 조금 시간이 걸렸다.
- Yjs 문서와 소켓 연결을 어디에서 묶을지
- 페이지가 마운트될 때 만들지, 상위에서 공유할지
- "문서 수명"과 "소켓 연결 수명"을 헷갈리지 않게 분리해 두는 쪽으로 정리했다.
- 싱글 서버 기준 브로드캐스트 로직
- 처음에는 그냥 "연결된 애들 전체에 쏜다" 수준으로 짰다가
- 나중에 네임스페이스 구조로 옮길 때 다시 한 번 손을 보게 되었다.
전형적인 삽질이라 따로 문제가 될 건 아니었지만,
"처음 WebSocket 서버를 만지면 어디서 시간 쓰게 되는지"를 다시 떠올릴 때 참고가 될 것 같아서 남겨둔다.
5. 회사 WebSocket 인프라와의 차이 (네임스페이스 멀티 구조)
로컬에서 싱글 WebSocket 서버로 테스트할 때는 구조가 단순했다.
- 포트 하나 열고
- 연결 들어오면 다 같은 풀에서 관리하고
- 방 개념도 최소로만 둬도 동작에는 문제가 없었다.
하지만 실제 회사 환경으로 옮겨 가려면 이야기가 꽤 달라졌다.
- 이미 xxx-socket 같은 이름의 WebSocket 서버가 운영 중이었다.
- 네임스페이스 기준으로 소켓 연결을 나누고 있었고
- 여러 서비스가 같은 소켓 인프라를 공유하는 구조였다.
여기에 내가 만든 Yjs 기반 실시간 폼 공유 로직을 자연스럽게 올려야 했다.
그래서 전체 흐름은 이렇게 변했다.
- 로컬 실험
- Node 싱글 WebSocket 서버 한 개
- 네임스페이스, 멀티 테넌트 개념 없음
- 회사 환경
- xxx1/xxx-socket 같은 GitLab 레포가 이미 존재
- 네임스페이스별로 연결이 구분되는 구조
- 인증, 로그, 모니터링 포인트가 같이 붙어 있음
이 상황에서 해야 했던 일은
- 개인 레포에서 만들었던 Yjs + WebSocket 연동 코드를
- 회사 WebSocket 서버 레포 구조에 맞게 옮기고
- 네임스페이스 단위로 방을 나누면서도 Yjs 동기화가 깨지지 않게 만드는 것이었다.
6. GitLab 레포에 녹여 넣기까지
소켓 서버 담당 팀에서 사내 GitLab 레포 권한을 열어 준 뒤에는
대략 이런 순서로 작업했다.
- 로컬에서 돌리던 싱글 WebSocket 서버 코드를 정리했다.
- xxx-socket 레포 구조를 파악했다.
- 네임스페이스 사용 방식
- 인증/토큰 처리 방식
- 로깅, 모니터링 포인트
- Yjs 동기화 관련 코드를 최대한 모듈화했다.
- 회사 WebSocket 서버에 네임스페이스별 실시간 폼 방을 추가했다.
- 로컬/스테이징 환경에서 멀티 네임스페이스 동시 접속 테스트를 진행했다.
이 과정에서 가장 많이 고민했던 부분은
- 싱글 서버 기준으로 짜 놨던 브로드캐스트 로직을
- 네임스페이스 구조 안에서 어디까지 나눌지였다.
최종적으로는
- 네임스페이스 하나를 Yjs 문서 하나 또는 문서 그룹과 매핑하고
- 같은 네임스페이스 안에서만 델타를 브로드캐스트하는 방식으로 정리했다.
이렇게 해 두니 "어떤 폼이 어느 방에 매핑되어 있는지"를 머릿속에서 따라가기 쉬웠다.
7. 나중에 다시 볼 나를 위한 메모
이 글은 나중에 비슷한 요구가 나왔을 때
"뭐부터 다시 손대야 하지?"를 떠올리기 위한 메모이기도 하다.
- 폼 입력 값을 실시간으로 공유해야 한다면
단순 이벤트 전송보다 CRDT(Yjs) 구조를 먼저 떠올릴 것 - WebSocket 서버를 새로 만져야 한다면
로컬에서 가장 단순한 싱글 서버로 먼저 실험해 볼 것 - 회사 인프라에 붙일 때는
네임스페이스, 인증, 모니터링 같은 운영 관점을 먼저 파악할 것 - 개인 레포
"jbeat30/y-websocket-test"
"jbeat30/yjs-form-realtime"
이 두 레포는 앞으로도 "실시간 관련 개념을 다시 손으로 익혀 보고 싶을 때"
기본 실험장으로 계속 쓸 수 있을 것 같다.
8. 다음 글 예고
1편은 말 그대로
"왜 이걸 하게 됐고, 어떤 순서로 손을 대기 시작했는지"에 대한 이야기였다.
2편에서는
- 로컬에서 돌렸던 WebSocket 서버 구조를 조금 더 구체적으로 정리하고
- 회사에서 쓰는 "xxx-socket" 네임스페이스 구조 안에
- 내가 만든 Yjs 동기화 코드를 어떻게 녹여 넣었는지
같은 내용을 실무 기록 느낌으로 풀어볼 예정이다.
3편에서는
- Yjs Awareness로 커서, 선택 영역, 타이핑 상태를 어떻게 공유했는지
- 폼 구조를 문서 단위로 어디까지 쪼갤지 기준을 어떻게 잡았는지
- 실시간 공유 화면에서 "상대가 지금 어디를 보고 있는지"를 보여주는 UX를 어떻게 만들었는지 까지 정리해서,
CRDT 폼 실시간 공유 시리즈 1~3편을 한 세트로 마무리할 생각이다.
'FrontEnd > JavaScript' 카테고리의 다른 글
| JavaScript Function.call / apply / bind - this (0) | 2025.09.09 |
|---|---|
| [실무 기록] 폼 실시간 공유 3편 – Yjs Awareness로 상대 커서,선택 상태 보여주기 (0) | 2025.09.08 |
| [실무 기록] 폼 실시간 공유 2편 - WebSocket 서버 구조랑 네임스페이스 적응기 (0) | 2025.09.08 |
| [실무 기록] jQuery jsTree를 모듈로 감싸서 쓰면서 배운 것들 (0) | 2025.09.08 |
| JavaScript async/await와 Promise 체이닝, 어떻게 골라 쓸까 (0) | 2024.11.28 |