-
Next.js와 Node.js 기반 실시간 대용량 편집기 Part1. 아키텍처 & 프로토콜Tip 2025. 4. 27. 22:39728x90반응형
대용량 문서와 이미지를 실시간으로 다수 사용자와 공유하기 위해서는 프론트엔드, 백엔드, 인프라 각 계층이 유기적으로 설계되어야 합니다.
프론트엔드는 Next.js를 기반으로 하여 SSR(서버사이드 렌더링)도 가능하지만, 실시간 편집기에서는 주로 클라이언트 측 SPA 동작과 WebSocket 통신을 활용하게 됩니다.
Next.js 앱은 사용자의 편집 UI를 제공하고 WebSocket 등의 실시간 채널을 통해 백엔드와 통신하며, 리액트 기반의 실시간 UI 업데이트를 처리합니다.
백엔드는 Node.js로 구현된 실시간 협업 서버로, TypeScript로 작성되며 실시간 동기화 로직(CRDT/OT 알고리즘 적용)과 스트림 데이터 처리를 담당합니다.
인프라는 AWS 클라우드 상에 Kubernetes(EKS 등)를 이용하여 배포되며, 컨테이너 기반으로 자동 확장과 로드 밸런싱, 모니터링 등을 구현합니다.
1. 시스템 아키텍처 개요
클라이언트에서 서버까지 데이터가 흐르는 과정을 나타낸 다이어그램 이러한 아키텍처에서 데이터 흐름은 다음과 같습니다.
사용자가 Next.js 편집기에서 문서를 편집하면 이벤트가 발생하고, 이 변화(텍스트 입력, 이미지 추가 등)를 백엔드 Node.js 서버에 실시간 전송합니다.
백엔드는 해당 이벤트를 받아 동기화 알고리즘으로 처리한 뒤, 같은 문서를 보고 있는 다른 사용자들에게 브로드캐스트 합니다.
프론트엔드는 수신한 변경 내용을 즉시 반영하여 사용자들이 동시에 문서 편집이 가능하게 합니다.
또한 10MB 이상의 대용량 이미지가 삽입되는 경우 등을 대비해 파일 저장소(예: AWS S3)에 이미지를 업로드하고 해당 링크나 파일 ID만 공유하는 방식을 함께 사용하여, 실시간 편집 데이터와 파일 전송을 분리하는 구조도 고려합니다.
예를 들어 이미지 업로드는 백엔드에서 S3 Pre-signed URL 등을 발급받아 직접 클라이언트가 S3에 업로드하게 하고, 업로드 완료 이벤트만 실시간 채널로 전파하여 다른 클라이언트들이 새 이미지의 URL을 로드하도록 하는 방법이 있습니다.
이렇게 하면 대용량 바이너리 데이터 전송으로 인한 실시간 채널 부하를 경감시킬 수 있습니다.
전체적으로 프론트엔드-백엔드 간에는 실시간 양방향 통신 채널(WebSocket 등)이 핵심이며, 백엔드-데이터베이스 간에는 필요에 따라 트랜잭션 처리나 오프라인 저장을 위한 API 통신이 추가됩니다.
또한 AWS 인프라에서는 로드 밸런서를 통해 다수 Node.js 인스턴스에 연결을 분산시키고, 쿠버네티스 오토스케일러로 사용량에 따라 서버 팟(pod) 수를 조절합니다.
Redis 같은 인메모리 메시지 브로커를 추가로 두어, 여러 개의 백엔드 인스턴스 간에 편집 내용을 동기화하거나, Socket.IO와 같은 라이브러리의 Redis 어댑터를 활용해 여러 서버에 분산된 소켓들을 묶어 처리하는 구조도 활용할 수 있습니다.
이를 통해 한 문서에 접속한 이용자들이 서로 다른 서버에 접속해 있더라도 동일한 편집 내용을 실시간으로 받도록 할 수 있습니다.
2. Node.js Streams 활용 및 대용량 데이터 처리
Node.js의 스트림(Streams)은 대용량 데이터를 효율적으로 처리하기 위한 핵심 도구입니다.
스트림을 사용하면 파일이든 네트워크 메시지든 데이터를 작은 청크(chunk) 단위로 분할하여 순차적으로 처리할 수 있습니다.
예를 들어 10MB짜리 이미지를 한 번에 메모리에 로드하여 보내는 대신, Node.js Readable Stream으로 파일을 읽어 일정 크기의 바이트 덩어리로 나눠서 전송할 수 있습니다.
이렇게 하면 한꺼번에 메모리를 많이 쓰지 않고도 데이터를 전달할 수 있어, 메모리 부족이나 GC 지연을 예방하고 지연 없이 실시간 전송이 가능합니다.
Node.js 스트림은 백프레셔(backpressure)를 자동으로 관리하여 수신 측이 느릴 경우 송신 속도를 조절해 주는 장점도 있습니다. WebSocket 같은 네트워크 소켓은 Node.js에서 Duplex Stream(읽기/쓰기 스트림)으로 볼 수 있는데, 이를 통해 파이프라인을 구성하면 예를 들어 fileReadStream.pipe(socketWritableStream) 형태로 코드 레벨에서 파일 스트림을 소켓으로 바로 연결할 수도 있습니다.
클라이언트가 업로드하는 대용량 파일도 스트림으로 처리합니다.
Next.js 클라이언트에서는 <input type="file"> 등을 통해 파일을 읽은 뒤 Blob의 slice나 스트림 API를 활용해 조금씩 백엔드로 전송하고, 백엔드는 그 데이터를 Writable Stream으로 받아서 바로 파일저장소에 기록하거나 다른 참가자들에게 중계합니다.
반대로 다른 사용자가 보낸 이미지를 수신할 때도, 백엔드는 파일을 읽어 여러 개의 프레임으로 나눠 차례로 WebSocket으로 보내거나, gRPC 스트림 응답을 통해 chunk를 순차 전달할 수 있습니다.
이런 방식으로 대용량 바이너리 데이터도 실시간 편집 흐름에 지장을 주지 않도록 분리하여 전송하며, 필요시 압축이나 이진 프로토콜을 사용해 전송 효율을 높입니다.
(예: 텍스트 데이터는 JSON 대신 바이너리 프로토콜 버퍼 사용, 이미지 등은 이미 압축되어 있으므로 그대로 전송).
Node.js Streams의 고수준 API(pipe, pipeline)를 잘 활용하면 파일 읽기->전송 또는 수신->파일 쓰기 과정을 깔끔하게 구현할 수 있고, 에러 처리나 완료 시점 관리도 용이합니다.
2.1 스트림 처리 예시
import { createReadStream } from 'fs'; import { pipeline } from 'stream/promises'; import { WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', (ws) => { ws.on('message', async (data: Buffer) => { await pipeline( Readable.from(data), transformStream, writableStream ); }); });
이러한 방식으로 메모리를 효율적으로 관리하며 데이터의 흐름을 제어할 수 있습니다.
3. 실시간 전송 프로토콜 선택: WebSocket vs gRPC vs WebRTC
실시간 양방향 통신을 구현할 프로토콜로는 WebSocket, gRPC, WebRTC 등 몇 가지 대안이 있습니다. 각각의 특성과 장단점을 비교해 보면 다음과 같습니다.
3.1 WebSocket
WebSocket은 웹에서 표준적인 양방향 통신 채널입니다.
HTTP 연결을 업그레이드하여 단일 TCP 소켓으로 클라이언트-서버 간에 메시지를 주고받을 수 있습니다.
풀 듀플렉스 통신으로 서버가 클라이언트 요청 없이도 데이터를 푸시할 수 있어 실시간 업데이트에 적합합니다.
또한 초기 Handshake 이후에는 추가 HTTP 헤더 오버헤드 없이 지속되므로 낮은 지연과 적은 대역폭으로 많은 메시지를 주고받을 수 있습니다.
실시간 채팅, 주식 시세, 협업 편집기 등에서 널리 사용되고 검증된 기술이며 브라우저 지원도 우수합니다.
단점으로는 메시지 형식이 응용 레벨에 위임되어 있어 (텍스트 또는 이진), 개발자가 직접 프로토콜 설계를 해야 하고, HTTP/1 기반이므로 HTTP/2/3의 멀티플렉싱 이점을 바로 활용하진 못합니다.
하지만 한 연결에서 여러 종류의 메시지를 주고받는 것은 메시지 타입 구분으로 해결 가능하며, 대부분의 시나리오에 충분한 성능을 발휘합니다.
Socket.IO와 같이 WebSocket을 추상화하면서 Fallback(Long Polling) 처리와 편의 기능 (방/네임스페이스)을 제공하는 라이브러리를 써서 생산성을 높일 수도 있습니다.
3.2 gPRC
gRPC는 HTTP/2 기반의 RPC 프레임워크로, 프로토콜 버퍼를 통해 타입이 지정된 데이터를 주고받고 스트리밍 RPC도 지원합니다.
서버-클라이언트 스트리밍 또는 양방향 스트리밍 RPC를 사용하면 WebSocket과 유사하게 지속 연결 상에서 연속적인 메시지 교환이 가능합니다.
TypeScript(Node) 백엔드와 프론트엔드(브라우저) 간 사용을 위해 gRPC-Web을 적용할 수 있는데, gRPC-Web은 실제로는 HTTP/1.1+XHR 또는 WebSocket을 통해 서버와 통신하도록 폴리필된 형태입니다.
장점은 명세가 엄격하고 API 명세로부터 클라이언트/서버 코드 생성이 가능해 타입 안정성이 높으며, 서버 측 스트리밍 지원으로 서버가 클라이언트에게 푸시도 할 수 있다는 점입니다.
또한 프로토콜 버퍼 바이너리 포맷을 사용하므로 데이터 효율성이 높습니다.
단점은 브라우저 환경에서 gRPC를 바로 사용할 수 없고 gRPC-Web 프록시를 세워야 하며, 완전한 양방향 스트리밍(bi-di streaming)은 제약이 있다는 것입니다 (gRPC-Web은 서버->클라이언트 스트리밍만 공식 지원).
또한 WebSocket에 비해 초기 설정이 복잡하고, 다중 사용자 브로드캐스트 같은 시나리오에서는 각 스트림 호출마다 개별적으로 데이터를 보내야 하므로 서버 구현이 복잡해질 수 있습니다.
따라서 실시간 협업 편집기의 브라우저 클라이언트 통신에는 gRPC보다는 WebSocket이 더 많이 채택되는 추세입니다.
다만, 내부 마이크로서비스 간 통신에서는 gRPC가 유용할 수 있는데, 예컨대 편집기 서버와 다른 백엔드 서비스(이미지 변환 서비스 등) 사이를 gRPC로 연결하면 효율적인 이원 통신이 가능하고, 서비스 경계를 명확히 할 수 있습니다.
3.3 WebRTC
WebRTC는 주로 브라우저 간 P2P 실시간 통신(영상/음성 스트림)을 위해 고안된 기술이지만, 데이터 채널(DataChannel)을 통해 임의의 바이너리 데이터를 P2P로 주고받는 것도 가능합니다.
WebRTC DataChannel은 내부적으로 UDP 기반의 SCTP (또는 최신 브라우저에서는 일부 QUIC) 프로토콜을 사용하여 낮은 지연과 네트워크 품질에 따른 전송 최적화를 제공합니다.
이점은 서버를 거치지 않고 클라이언트들 간 직접 통신하므로 대역폭을 절약하고 레이턴시를 최소화할 수 있다는 것입니다.
예를 들어 100명의 사용자가 있다면, 중앙 서버를 통하지 않고 각 브라우저들이 직접 메시지를 주고받게 하여 서버 부하 없이도 실시간 동기화가 가능합니다.
그러나 한계도 분명합니다.
P2P 연결을 위해서는 각 클라이언트 간에 ICE 시그널링 및 NAT 트래버설 단계를 거쳐야 하고, 100명 이상의 다자간 통신에서는 풀 메쉬 연결의 복잡도가 폭발적으로 증가합니다.
브라우저마다 수백 개의 peer connection을 유지하는 것은 비현실적이며 (Chrome의 peer connection 제한은 약 256개지만 대역폭이나 성능 문제가 큼), 패킷 손실 시 재전송 등 신뢰성 이슈를 애플리케이션 레벨에서 추가 처리해야 할 수도 있습니다.
따라서 WebRTC는 2~5명 정도의 소규모 그룹 협업이나, 또는 화상/음성 스트림 + 데이터 보조채널이 필요한 경우에 적합하고, 대규모 협업 편집 서비스의 기본 프로토콜로 쓰기에는 도전이 많습니다.
수백 명 수준의 동시 접속과 대용량 데이터 전송이 요구되는 경우에는 WebSocket 기반 중앙 허브 방식이 구현 난이도와 안정성 면에서 유리할 것입니다.
WebRTC는 향후 P2P 보조 전송 용도로 일부 기능을 추가적으로 활용할 수 있습니다 (예: 서버는 제어 메시지만 처리하고, 실제 대용량 파일은 P2P 공유) – 이에 대해서는 4편에서 더 다룹니다.
4. 프로토콜 선택 기준 정리
프로토콜 특징 (전송방식) 장점 단점 적합한 용도 WebSocket TCP (HTTP/1 업그레이드) 기반 양방향 통신 - Handshake 후 지속 연결로 추가 오버헤드 없음
- 서버 푸시로 실시간 업데이트 용이
- 브라우저 광범위 지원- 메시지 포맷 커스텀 필요 (텍스트/바이너리)
- 부하 분산 시 연결 분배 문제(Sticky 세션 필요)채팅, 실시간 편집, 알림 등 대부분의 웹 실시간 기능 gRPC (gRPC-Web) HTTP/2 (또는 WebSocket 프록시) 기반 RPC - 타입 안전(Protobuf) 및 자동 코드생성
- 스트리밍 RPC로 서버푸시 가능
- 다양한 언어 지원 (멀티플랫폼)- 브라우저에 직접 지원되지 않아 프록시 필요
- Full bi-di 스트리밍 제한
- 구현 복잡도 증가내부 서비스 간 통신, 모바일 앱
클라이언트 등WebRTC DataChannel UDP/QUIC 기반 P2P 데이터채널 - P2P 직접 통신으로 서버 부하 감소
- UDP 기반 낮은 지연, 손실 허용 시 빠른 전송
- 미디어+데이터 동시 활용 가능- 연결수 증가 시 비현실적 (N^2 연결)
- NAT 등 연결 설정 복잡
- 브라우저별 호환 이슈 및 디버깅 어려움소규모 그룹 P2P 협업,
파일 P2P전송 (보조 용도)결론적으로, Next.js 웹 편집기의 클라이언트-서버 통신에는 WebSocket (혹은 Socket.IO)가 가장 적합한 선택입니다.
WebSocket은 이미 많은 실시간 협업 에디터에서 검증된 방식으로, 낮은 지연과 양방향성이 뛰어나고 브라우저 표준 지원이 잘 됩니다.
또한 텍스트와 바이너리 데이터를 모두 전송할 수 있어 혼합 데이터(문서+이미지)에도 문제없습니다.
gRPC는 타입 안전성 등 매력적 요소가 있지만 웹 환경에서는 진입장벽이 있고, WebRTC는 P2P 환경 구축 부담이 커 대규모 서비스에는 부적합합니다.
다만 백엔드 마이크로서비스 간이나 모바일 네이티브 앱 클라이언트 등에서는 gRPC를 보조적으로 쓸 수 있고, WebRTC DataChannel은 4편에서 논의할 미래 지향적 기술로서 참고해 둡니다.
5. 대용량 혼합 데이터 실시간 전송 구조 설계
실시간 공유 편집기에서는 텍스트, 이미지, 파일 등 서로 크기와 성격이 다른 데이터 유형을 효율적으로 동시에 전송해야 합니다.
특히 이미지 같은 경우 수 MB ~ 수십 MB가량으로 커질 수 있으므로, 작은 텍스트 변경 사항과 동일 채널로 취급하면 텍스트 업데이트가 지연되거나 네트워크 혼잡이 발생할 수 있습니다.
5.1 이벤트 유형 분리
실시간 통신 프로토콜 상에서 메시지 타입을 구분하여, 텍스트 편집 이벤트와 파일 전송 이벤트를 다르게 처리합니다.
예를 들어 WebSocket 메시지에 "type": "text" 또는 "type": "file_chunk" 등의 필드를 두고, 텍스트는 즉시 브로드캐스트 하지만 파일 청크는 보다 큰 단위로 스트림 처리합니다.
이렇게 분리하면 대용량 파일이 전송 중이어도 새로운 텍스트 입력은 별도 메시지로 즉시 전파되어 편집 지연이 최소화됩니다.
5.2 Chunking (청크 분할)
대용량 이미지는 일정 크기의 바이트 덩어리(예: 64KB 등)로 나눠 순차적으로 전송합니다.
Node.js fs.createReadStream과 같은 API로 파일을 읽으면 기본 64KB 등의 highWaterMark 설정으로 스트림이 청크를 생성하며, 이 크기는 필요에 따라 조절 가능합니다.
클라이언트에서는 File 또는 Blob을 slice()하여 ArrayBuffer 청크를 만들고 보낼 수도 있습니다.
첫 청크에는 파일 식별자(임시 ID, 파일명 등)와 전체 크기 정보를 포함시키고, 수신 측에서는 청크들을 버퍼링 하여 완전한 파일을 재조립합니다.
모든 청크 수신이 완료되면 해당 파일 객체를 생성(예: Blob -> ObjectURL -> 이미지 표시)하거나 파일시스템에 저장합니다.
스트리밍 중에는 진행률을 표시하여 사용자에게 업로드/다운로드 상태를 보여줄 수 있습니다.
5.3 전송 경로 최적화
위에서 언급했듯이, 아주 큰 파일은 아예 직접 업로드 + 링크 공유 방식을 취하는 것이 효율적입니다.
즉, 편집기에서 사용자가 이미지를 올리면 클라이언트가 바로 S3 같은 저장소에 업로드하고, 실시간 편집 메시지에는 해당 파일의 URL 또는 키만 전송합니다.
다른 클라이언트는 이를 받아 필요할 때 해당 URL로 파일을 불러옵니다.
이 방식은 서버나 WebSocket에 과부하를 주지 않고 대용량 데이터는 CDN/스토리지 경로로 처리하게 해 줍니다.
단, 편집기의 실시간성 측면에서 다른 사용자가 바로 이미지 미리 보기를 볼 수 있어야 하므로, 업로드 완료 신호를 실시간으로 받고 나면 곧장 <img src="..."> 태그에 해당 URL을 넣어 렌더링 합니다.
Amazon S3의 경우 Pre-signed URL을 사용하면 일정 시간 유효한 다운로드 링크를 제공할 수 있고, 권한 제어도 가능하므로 보안도 유지할 수 있습니다.
5.4 메모리 관리 및 백프레셔
대용량 데이터 전송 시 송신 측 버퍼 메모리와 수신 측 처리 속도를 조절해야 합니다.
Node.js Streams를 사용하면 자동으로 조절되지만, WebSocket 라이브러리 (ws 등)을 직접 사용할 경우 socket.send()의 반환값이나 socket.bufferedAmount (브라우저 WebSocket API)를 모니터링하여 송신 버퍼가 과도하게 쌓이지 않도록 합니다.
예를 들어 서버에서 연속해서 청크를 보낼 때 socket.send()가 false를 리턴하면 드레인(drain) 이벤트를 기다렸다가 이어 보내는 식으로 백프레셔를 적용합니다.
이러한 흐름 제어(Flow Control)로 송신이 수신보다 앞서 가지 않도록 하면 메모리 폭주나 OOM을 방지할 수 있습니다.
5.5 데이터 일관성과 순서 보장
혼합 데이터의 경우 텍스트 편집과 이미지 전송 간 순서 동기화도 고려해야 합니다.
사용자가 문서의 특정 위치에 이미지를 삽입했다면, 해당 위치에 이미지가 들어간다는 텍스트(또는 구조) 변경과 실제 이미지 데이터가 밀접하게 연결됩니다.
프로토콜 상에서 텍스트/파일 메시지 종류를 구분하더라도, 수신자는 “여기에 이 이미지가 들어온다”는 정보를 알아야 합니다.
이를 위해 일반적으로 플레이스홀더 개념을 씁니다.
예를 들어 문서 텍스트에 특수한 토큰(![image:1234])을 넣고, 그 토큰에 매핑된 실제 이미지 데이터 ID=1234를 따로 전송합니다.
다른 클라이언트는 토큰을 받으면 우선 비어있는 이미지 프레임을 만들어두고, 해당 ID의 이미지 데이터 청크들이 도착하여 완성되면 이미지를 렌더링 합니다.
이러한 방법으로 텍스트-이미지 삽입 이벤트의 원자성을 어느 정도 유지할 수 있습니다.
이와 같이 아키텍처와 프로토콜 측면에서, Next.js + Node.js 기반의 실시간 편집 서비스는 WebSocket을 통해 텍스트/이미지 등 혼합 데이터의 스트리밍 전송을 가능하게 하고, Node.js Streams 활용과 데이터 분할/동기화 전략으로 대용량 데이터도 끊김 없이 처리하는 구조를 갖추게 됩니다.
2편에서는 실제 이러한 구조를 구현할 때의 상세 기법과 성능 튜닝에 대해 다룹니다.
반응형'Tip' 카테고리의 다른 글
Next.js와 Node.js 기반 실시간 대용량 편집기 Part2. 구현 및 성능 최적화 (0) 2025.05.03 스키마 유효성 검사 라이브러리 비교(feat. Zod vs Yup vs Joi) (1) 2025.01.05 C4 Model for Visualizing Frontend Architecture (Feat. FSD) (2) 2024.12.29 HTTP 클라이언트 라이브러리의 변화 Axios에서 Got, Ky로 (1) 2024.12.08 실무에서 유용한 MSW(Mock Service Worker) 활용 가이드 (2) 2024.11.30