시작해볼까!!!
우리 글래스모 팀은 1주차에 기획을 정하면서, 이 기획을 구현하기 위한 기술스택을 조사하였습니다.
그중에 BE쪽 라이브러리로 Nest.JS를 사용하기로 했습니다.
그 이유는
추가 이점:
이러한 특징들이 Nocta의 핵심 기능인 실시간 동시편집과 문서 관리 시스템 구축에 최적화되어 있다고 판단하여 NestJS를 선택하게 되었습니다.
NestJS 와 연동하기 전에, 먼저 NestJS가 뭔지 그리고 websocket과는 어떻게 연동되는지 알아야 했습니다.
만들어둔 링크드리스트 CRDT는 제쳐두고 NestJS, Websocket, MongoDB를 들여다 보았습니다.
💡Node.js 런타임 위에서 동작하는 TypeScript용 오픈 소스 백엔드 웹 프레임워크
NestJS는 Node.js 기반의 서버 사이드 프레임워크로, Angular의 아키텍처에서 영감을 받아 설계되었습니다. 기업용 애플리케이션 개발에 특화되어 있으며, TypeScript를 기본 언어로 사용합니다.
NestJS는 세 가지 핵심 구성 요소로 이루어져 있습니다:
애플리케이션의 각 기능을 독립적인 단위로 분리합니다. 예를 들어, 사용자 관리, 게시물 관리 등 각각의 기능이 하나의 모듈이 될 수 있습니다. 이러한 모듈화는 코드의 재사용성을 높이고 유지보수를 쉽게 만듭니다.
클라이언트의 요청을 받아 처리하는 역할을 합니다. HTTP 메소드(GET, POST 등)에 따라 적절한 엔드포인트를 제공하며, 요청을 서비스 계층으로 전달합니다.
실제 비즈니스 로직이 구현되는 곳입니다. 데이터베이스 조작, 데이터 가공 등 핵심적인 작업을 처리합니다.
@Injectable(), @Controller() 등의 데코레이터를 사용하여 각 컴포넌트의 역할을 명확하게 정의합니다. 이는 코드의 가독성을 높이고 의도를 명확하게 전달합니다.
@Module({
imports: [],
controllers: [AppController, UserController],
providers: [AppService, EventsGateway],
})
각 컴포넌트 간의 관계를 자동으로 관리해줍니다. 이를 통해 테스트가 용이하고, 코드의 재사용성이 향상됩니다.
요청 처리 과정에서 추가적인 로직을 쉽게 삽입할 수 있습니다. 인증, 로깅, 에러 처리 등을 효율적으로 구현할 수 있습니다.
WebSocket과 Socket.io를 기본적으로 지원하여 실시간 양방향 통신을 구현할 수 있습니다. 채팅, 실시간 알림등의 기능을 쉽게 개발할 수 있습니다.
그리고 NestJS의 경우 어느정도 공식적인 가이드라인하고 폴더구조가 제공돼있었습니다.
src
├── main.ts
├── app.module.ts
├── app.controller.ts
├── app.service.ts
├── modules
│ ├── module1
│ │ ├── module1.module.ts
│ │ ├── module1.controller.ts
│ │ ├── module1.service.ts
│ │ └── ...
│ ├── module2
│ │ ├── module2.module.ts
│ │ ├── module2.controller.ts
│ │ ├── module2.service.ts
│ │ └── ...
│ └── ...
└── ...
src
: 프로젝트의 소스 코드가 포함된 디렉토리입니다.main.ts
: 애플리케이션의 진입점으로, NestJS 애플리케이션의 인스턴스를 생성하고 실행합니다.app.module.ts
: 애플리케이션의 주 모듈입니다. 다른 모듈들을 임포트하고 컨트롤러와 서비스를 설정합니다.app.controller.ts
: 애플리케이션의 주 컨트롤러입니다. HTTP 요청을 처리하는 엔드포인트를 정의합니다.app.service.ts
: 애플리케이션의 주 서비스입니다. 비즈니스 로직을 구현합니다.modules
: 애플리케이션을 모듈 단위로 구성하는 디렉토리입니다. 모듈은 관련된 컨트롤러, 서비스, 프로바이더 등을 묶어서 구성합니다.굳이 위 구조를 안써도 되지만 NestJs의 구조가 저런 구조를 위해 만들어진 것을 확인했습니다.
기본적으로 Nest의 구조는 크게 두가지로 나뉩니다.
src
├── res # 혹은 domain
│ ├── user # 리소스 명
│ │ ├── dtos # DTO 폴더
│ │ ├── user.module.ts
│ │ ├── user.controller.ts
│ │ ├── user.service.ts
│ │ └── user.repository.ts
│ │
│ ├── post
│ │ ├── dtos # DTO 폴더
│ │ ├── post.module.ts
│ │ ├── post.controller.ts
│ │ ├── post.service.ts
│ │ └── post.repository.ts
│ │
│ └── comment
│ ├── dtos # DTO 폴더
│ ├── comment.module.ts
│ ├── comment.controller.ts
│ ├── comment.service.ts
│ └── comment.repository.ts
│
├── config # 설정관련 폴더
│ └── database # 데이터베이스 관련 설정 폴더
│
├── auth # 인증 관련 로직
├── middlewares # 미들웨어
│ ├── interceptors
│ ├── filters # exceptionfilters
│ └── decorators
│
└── utils # 유틸 폴더
├── constants # 유틸 변수
├── functions # 유틸 함수
└── types # 유틸 타입
src
├── res # 혹은 layer 라고 하셔도 무방합니다.
│ ├── modules
│ │ ├── user.module.ts
│ │ ├── post.module.ts
│ │ └── comment.module.ts
│ │
│ ├── controllers
│ │ ├── user.controller.ts
│ │ ├── post.controller.ts
│ │ └── comment.controller.ts
│ │
│ ├── services
│ │ ├── user.service.ts
│ │ ├── post.service.ts
│ │ └── comment.service.ts
│ │
│ ├── repositories
│ │ ├── user.repository.ts
│ │ ├── post.repository.ts
│ │ └── comment.repository.ts
│ │
│ └── dtos
│ ├── user
│ ├── post
│ └── comment
│
├── config # 설정관련 폴더
│ └── database # 데이터베이스 관련 설정 폴더
│
├── auth # 인증 관련 로직
├── middlewares # 미들웨어
│ ├── interceptors
│ ├── filters # exceptionfilters
│ └── decorators
│
└── utils # 유틸 폴더
├── constants # 유틸 변수
├── functions # 유틸 함수
└── types # 유틸 타입
이와 같은 폴더구조 세팅을 쉽게 할 수 있도록 nest 도구는 도와주는데요.
nest의 cli
를 이용하면 쉽게 탬플릿화해서 파일과 폴더를 만들어 줍니다.
nest g <alias> <resource이름>
사용예시)
nest g co user
를 입력하면 controller역할을 하는 user폴더를 만들어줍니다.
테스트 코드와 실제 코드 스크립트 가이드가 생성된 사진
이제 nestJS 의 구조를 파악 했으니 기본적으로 FE와 통신하는 환경을 만들어 보았습니다.
실시간 양방향 통신이 가능하도록 도와주는 라이브러리 2개를 찾을 수 있었습니다.
위 라이브러리 중
의 이유로 WebSocket
을 사용하기로 결정했습니다.
그리고 node.js로 동작하는 socket.io를 설치하였습니다.
→ 이를 nestJS에서 잘 동작하도록 하는 라이브러리를 설치해야합니다.
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
@nestjs/platform-socket.io : Socket.IO를 NestJS와 통합하기 위한 패키지
pnpm add socket.io-client
클라이언트와 서버에 설치해주었습니다.
socket.io-client
를 사용하여 서버에 연결합니다.@nestjs/websockets
: 웹소켓 기능을 위한 NestJS의 기본 패키지입니다.@nestjs/platform-socket.io
: NestJS와 socket.io
를 연결해주는 어댑터입니다.socket.io
: 실제 실시간 통신을 담당하는 라이브러리입니다.라이브러리 설치가 끝났으면, 아래와 같이 세팅해줍니다.
import React, { useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
const App = () => {
let socket: Socket;
useEffect(() => {
socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('서버에 연결되었습니다.');
});
socket.on('message', (message) => {
console.log(`서버로부터 메시지 수신: ${message}`);
});
return () => {
socket.disconnect();
};
}, []);
const sendMessage = () => {
socket.emit('message', '안녕하세요, 서버!');
};
return (
<div>
<h1>Socket.IO와 통신하기</h1>
<button
onClick={() => {
sendMessage();
}}
>
메시지 보내기
</button>
</div>
);
};
export default App;
서버의 경우 처음부터 완벽하게 세팅을 하기보다, 간단한 동작 환경 세팅을 위해 events
라는 이름으로 게이트 웨이를 만들어 동작을 확인했습니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
const bootstrap = async () => {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: 'http://localhost:5173',
credentials: true,
});
await app.listen(3000);
};
bootstrap();
enableCors 설정을 안해주면, server는 :3000이고, client는 :5173에서 HTTP 통신이 들어와서 통신할 수 없다는 200 에러가 계속 발생한다.
ERR_FAILED 200 (OK)
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserController } from './user/user.controller';
import { EventsGateway } from './events/events.gateway';
@Module({
imports: [],
controllers: [AppController, UserController],
providers: [AppService, EventsGateway],
})
export class AppModule {}
app.module은 모든 진입점을 담당하는 역할이라고 보면 됩니다.
nestJS가 loading되면서 app.module을 거쳐 시작합니다.
import { WebSocketGateway, WebSocketServer, SubscribeMessage } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: 'http://localhost:5173', // 클라이언트 주소로 변경
credentials: true,
},
})
export class EventsGateway {
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
console.log(`클라이언트 연결: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`클라이언트 연결 해제: ${client.id}`);
}
@SubscribeMessage('message')
handleMessage(client: Socket, payload: any): void {
console.log(`수신한 메시지: ${payload}`);
this.server.emit('message', payload);
}
}
events 는
nest g gateway events
cors
처리가 필요합니다.클라이언트 연결 성공!!
클라이언트가 연결되지만 입력사항을 주고받는 걸 확인할 수는 없습니다. 그래서 입력이 보이는 textarea를 공유하도록 바꿔보겠습니다.
import React, { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
let socket: Socket;
const App = () => {
const [content, setContent] = useState('');
useEffect(() => {
socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('서버에 연결되었습니다.');
});
socket.on('document', (data) => {
// 서버로부터 초기 문서 상태 수신
setContent(data);
});
socket.on('update', (data) => {
// 다른 클라이언트로부터 변경 사항 수신
setContent(data.content);
});
return () => {
socket.disconnect();
};
}, []);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newContent = e.target.value;
setContent(newContent);
// 서버로 변경 사항 전송
socket.emit('update', { content: newContent });
};
return (
<div>
<h1>실시간 편집기</h1>
<textarea
value={content}
onChange={handleChange}
rows={10}
cols={50}
placeholder="여기에 입력하세요..."
/>
</div>
);
};
export default App;
import { WebSocketGateway, WebSocketServer, SubscribeMessage } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
interface DocumentUpdate {
content: string;
// 필요한 경우 커서 위치, 사용자 정보 등 추가
}
@WebSocketGateway({
cors: {
origin: 'http://localhost:5173',
credentials: true,
},
})
export class EventsGateway {
@WebSocketServer()
server: Server;
private documentContent = ''; // 문서의 현재 상태를 저장
handleConnection(client: Socket) {
console.log(`클라이언트 연결: ${client.id}`);
// 새로운 클라이언트에게 현재 문서 상태 전송
client.emit('document', this.documentContent);
}
@SubscribeMessage('update')
handleUpdate(client: Socket, payload: DocumentUpdate): void {
console.log(`수신한 업데이트: ${payload.content}`);
this.documentContent = payload.content; // 서버의 문서 상태 업데이트
// 다른 클라이언트에게 변경 사항 브로드캐스트
client.broadcast.emit('update', payload);
}
}
위처럼 설정한다면 아래 처럼 보이게 됩니다.
이렇게 간단하게 socket.io와 nestJS를 통해 실시간 통신이 가능하도록 프로토타입을 제작해보았습니다.
등이 남아있지만, 차근차근 순차적으로 구현해 나아갈 생각입니다.
간단한 연동을 확인했으니, 이제 nestJS와 mongoDB간의 연동을 진행해보겠습니다.
실시간 동시편집 에디터인 Nocta는 일반적인 게시판이나 블로그와는 실시간 데이터 처리 요구사항을 가지고 있습니다. 특히 실시간으로 변화하는 문서 데이터와 복잡한 문서 구조를 효율적으로 저장하고 관리해야 하는 특성이 있습니다.
MongoDB는 NoSQL 데이터베이스로서, 실시간으로 빈번하게 변경되는 데이터를 처리하는데 탁월한 성능을 보여줍니다.
MySQL과 같은 관계형 데이터베이스는 스키마 변경이나 트랜잭션 처리에 overhead가 발생할 수 있지만, MongoDB는 유연한 스키마를 통해 이러한 문제를 해결합니다.
MongoDB의 문서 지향적 특성은 Nocta의 문서 구조와 자연스럽게 매칭됩니다.
{
documentId: "doc123",
title: "회의록",
blocks: [
{
id: "block1",
content: "첫 번째 섹션",
style: { type: "heading", level: 1 }
},
{
id: "block2",
content: "상세 내용",
style: { type: "paragraph" }
}
],
collaborators: [...]
}
이러한 구조는 문서의 계층적 구조를 자연스럽게 표현할 수 있으며, 특히:
에 유리합니다.
MongoDB는 수평적 확장이 용이하며, 데이터 구조의 변경이 자유롭습니다. 이는 추후 리치텍스트와 같은 특수한 효과들을 추가할 수 있는 서비스에 매우 중요한 특성입니다.
이러한 특성들을 종합적으로 고려했을 때, MongoDB는 Nocta의 실시간 동시편집 기능을 구현하는데 최적의 선택이라고 판단하였습니다.
이제 각 Clinet, Server에 CRDT를 적용해보겠습니다.
MongoDB 설치법 및 트러블 슈팅 :
https://abrupt-feta-9a9.notion.site/CRDT-2-NestJS-MongoDB-d49eb9f56f1044e6aca39f74fb9d338f?pvs=4