웹사이트 검색

Socket.IO, Angular 및 Node.js를 사용하여 실시간 앱을 만드는 방법


소개

WebSocket은 서버와 클라이언트 간의 전이중 통신을 허용하는 인터넷 프로토콜입니다. 이 프로토콜은 일반적인 HTTP 요청 및 응답 패러다임을 뛰어넘습니다. WebSockets를 사용하면 클라이언트가 요청을 시작하지 않고도 서버가 클라이언트에 데이터를 보낼 수 있으므로 일부 매우 흥미로운 애플리케이션이 가능합니다.

이 자습서에서는 실시간 문서 공동 작업 애플리케이션(Google Docs와 유사)을 빌드합니다. 이를 위해 Angular 7을 사용할 것입니다.

GitHub에서 이 예제 프로젝트의 전체 소스 코드를 찾을 수 있습니다.

전제 조건

이 자습서를 완료하려면 다음이 필요합니다.

  • Node.js를 로컬에 설치했습니다. Node.js를 설치하고 로컬 개발 환경을 만드는 방법에 따라 수행할 수 있습니다.
  • WebSocket을 지원하는 최신 웹 브라우저.

이 튜토리얼은 원래 Node.js v8.11.4, npm v6.4.1 및 Angular v7.0.4로 구성된 환경에서 작성되었습니다.

이 튜토리얼은 Node v14.6.0, npm v6.14.7, Angular v10.0.5 및 Socket.IO v2.3.0에서 검증되었습니다.

1단계 - 프로젝트 디렉토리 설정 및 소켓 서버 생성

먼저 터미널을 열고 서버와 클라이언트 코드를 모두 포함할 새 프로젝트 디렉토리를 만듭니다.

  1. mkdir socket-example

다음으로 프로젝트 디렉터리로 변경합니다.

  1. cd socket-example

그런 다음 서버 코드를 위한 새 디렉터리를 만듭니다.

  1. mkdir socket-server

그런 다음 서버 디렉토리로 변경하십시오.

  1. cd socket-server

그런 다음 새 npm 프로젝트를 초기화합니다.

  1. npm init -y

이제 패키지 종속성을 설치합니다.

  1. npm install express@4.17.1 socket.io@2.3.0 @types/socket.io@2.1.10 --save

이러한 패키지에는 Express, Socket.IO 및 @types/socket.io가 포함됩니다.

이제 프로젝트 설정을 완료했으므로 서버용 코드 작성으로 이동할 수 있습니다.

먼저 새 src 디렉토리를 만듭니다.

  1. mkdir src

이제 src 디렉토리에 app.js라는 새 파일을 만들고 원하는 텍스트 편집기를 사용하여 엽니다.

  1. nano src/app.js

Express 및 Socket.IO에 대한 requi 문으로 시작합니다.

const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);

보시다시피 우리는 Express를 사용하고 있으며 Socket.IO는 기본 WebSocket에 대한 추상화 계층을 제공합니다. WebSocket을 지원하지 않는 이전 브라우저에 대한 폴백 메커니즘 및 생성 기능과 같은 멋진 기능이 함께 제공됩니다. 잠시 후에 이것이 실제로 작동하는 것을 보게 될 것입니다.

실시간 문서 협업 애플리케이션의 목적을 위해 문서를 저장할 방법이 필요합니다. 프로덕션 설정에서는 데이터베이스를 사용하려고 하지만 이 자습서의 범위에서는 문서의 메모리 내 저장소를 사용합니다.

const documents = {};

이제 소켓 서버가 실제로 수행할 작업을 정의해 보겠습니다.

io.on("connection", socket => {
  // ...
});

이것을 분해합시다. .on(...)은 이벤트 리스너입니다. 첫 번째 매개변수는 이벤트의 이름이고 두 번째 매개변수는 일반적으로 이벤트 페이로드와 함께 이벤트가 실행될 때 실행되는 콜백입니다.

첫 번째 예는 클라이언트가 소켓 서버에 연결할 때입니다(연결은 Socket.IO에서 예약된 이벤트 유형임).

하나의 소켓 또는 여러 소켓(즉, 브로드캐스팅)에 대한 통신을 시작하기 위해 콜백에 전달할 소켓 변수를 얻습니다.

safeJoin

회의실 참여 및 퇴장을 처리하는 로컬 함수(safeJoin)를 설정할 것입니다.

io.on("connection", socket => {
  let previousId;

  const safeJoin = currentId => {
    socket.leave(previousId);
    socket.join(currentId, () => console.log(`Socket ${socket.id} joined room ${currentId}`));
    previousId = currentId;
  };

  // ...
});

이 경우 클라이언트가 방에 참가하면 특정 문서를 편집하고 있는 것입니다. 따라서 여러 고객이 같은 방에 있으면 모두 같은 문서를 편집하고 있는 것입니다.

기술적으로 소켓은 여러 방에 있을 수 있지만 한 클라이언트가 동시에 여러 문서를 편집하는 것을 원하지 않으므로 문서를 전환하는 경우 이전 방을 떠나 새 방에 참여해야 합니다. 이 작은 기능이 이를 처리합니다.

소켓이 클라이언트에서 수신하는 세 가지 이벤트 유형이 있습니다.

  • getDoc
  • addDoc
  • editDoc

소켓에서 클라이언트로 보내는 두 가지 이벤트 유형은 다음과 같습니다.

  • 문서
  • 문서

문서 가져오기

첫 번째 이벤트 유형인 getDoc에 대해 작업해 보겠습니다.

io.on("connection", socket => {
  // ...

  socket.on("getDoc", docId => {
    safeJoin(docId);
    socket.emit("document", documents[docId]);
  });

  // ...
});

클라이언트가 getDoc 이벤트를 내보낼 때 소켓은 페이로드(우리의 경우 ID일 뿐임)를 가져오고 해당 docId로 방에 참가한 다음 내보낼 것입니다. 저장된 문서는 시작 클라이언트에만 반환됩니다. 여기서 socket.emit(document, ...)가 작동합니다.

addDoc

두 번째 이벤트 유형인 addDoc에 대해 작업해 보겠습니다.

io.on("connection", socket => {
  // ...

  socket.on("addDoc", doc => {
    documents[doc.id] = doc;
    safeJoin(doc.id);
    io.emit("documents", Object.keys(documents));
    socket.emit("document", doc);
  });

  // ...
});

addDoc 이벤트에서 페이로드는 document 객체이며, 현재 클라이언트에서 생성한 ID로만 구성됩니다. 우리는 소켓에 해당 ID의 방에 참여하도록 지시하여 향후 편집 내용을 같은 방에 있는 모든 사람에게 방송할 수 있도록 합니다.

다음으로 서버에 연결된 모든 사람이 작업할 새 문서가 있음을 알기를 원하므로 io.emit(documents, ...) 기능을 사용하여 모든 클라이언트에 브로드캐스트합니다.

socket.emit()io.emit()의 차이점에 유의하세요. io 버전은 우리 서버에 연결된 모든 사람에게 방출하기 위한 것입니다.

editDoc

세 번째 이벤트 유형인 editDoc에 대해 작업해 보겠습니다.

io.on("connection", socket => {
  // ...

  socket.on("editDoc", doc => {
    documents[doc.id] = doc;
    socket.to(doc.id).emit("document", doc);
  });

  // ...
});

editDoc 이벤트를 사용하면 페이로드는 키 입력 후 해당 상태의 전체 문서가 됩니다. 데이터베이스의 기존 문서를 교체한 다음 현재 해당 문서를 보고 있는 클라이언트에게만 새 문서를 브로드캐스트합니다. 특정 방에 있는 모든 소켓으로 방출하는 socket.to(doc.id).emit(document, doc)를 호출하여 이를 수행합니다.

마지막으로 새 연결이 만들어질 때마다 새 연결이 연결할 때 최신 문서 변경 사항을 수신하도록 모든 클라이언트에 브로드캐스트합니다.

io.on("connection", socket => {
  // ...

  io.emit("documents", Object.keys(documents));

  console.log(`Socket ${socket.id} has connected`);
});

소켓 기능이 모두 설정되면 포트를 선택하고 수신합니다.

http.listen(4444, () => {
  console.log('Listening on port 4444');
});

터미널에서 다음 명령을 실행하여 서버를 시작합니다.

  1. node src/app.js

이제 문서 협업을 위한 완전한 기능을 갖춘 소켓 서버가 생겼습니다!

2단계 — @angular/cli 설치 및 클라이언트 앱 만들기

새 터미널 창을 열고 프로젝트 디렉토리로 이동합니다.

다음 명령을 실행하여 Angular CLI를 devDependency로 설치합니다.

  1. npm install @angular/cli@10.0.4 --save-dev

이제 @angular/cli 명령을 사용하여 Angular Routing이 없고 스타일링을 위한 SCSS가 있는 새 Angular 프로젝트를 만듭니다.

  1. ng new socket-app --routing=false --style=scss

그런 다음 서버 디렉토리로 변경하십시오.

  1. cd socket-app

이제 패키지 종속성을 설치합니다.

  1. npm install ngx-socket-io@3.2.0 --save

ngx-socket-io는 Socket.IO 클라이언트 라이브러리에 대한 Angular 래퍼입니다.

그런 다음 @angular/cli 명령을 사용하여 문서 모델, 문서 목록 구성 요소, 문서를 생성합니다. > 구성 요소 및 문서 서비스:

  1. ng generate class models/document --type=model
  2. ng generate component components/document-list
  3. ng generate component components/document
  4. ng generate service services/document

이제 프로젝트 설정을 완료했으므로 클라이언트용 코드 작성으로 이동할 수 있습니다.

앱 모듈

app.modules.ts를 엽니다.

  1. nano src/app/app.module.ts

그리고 FormsModule, SocketioModuleSocketioConfig를 가져옵니다.

// ... other imports
import { FormsModule } from '@angular/forms';
import { SocketIoModule, SocketIoConfig } from 'ngx-socket-io';

그리고 @NgModule 선언 전에 config를 정의합니다.

const config: SocketIoConfig = { url: 'http://localhost:4444', options: {} };

이것이 서버의 app.js에서 이전에 선언한 포트 번호임을 알 수 있습니다.

이제 imports 배열에 추가하여 다음과 같이 표시합니다.

@NgModule({
  // ...
  imports: [
    // ...
    FormsModule,
    SocketIoModule.forRoot(config)
  ],
  // ...
})

이렇게 하면 AppModule이 로드되는 즉시 소켓 서버에 대한 연결이 시작됩니다.

문서 모델 및 문서 서비스

document.model.ts를 엽니다.

  1. nano src/app/models/document.model.ts

그리고 iddoc를 정의합니다.

export class Document {
  id: string;
  doc: string;
}

document.service.ts를 엽니다.

  1. nano src/app/services/document.service.ts

그리고 클래스 정의에 다음을 추가합니다.

import { Injectable } from '@angular/core';
import { Socket } from 'ngx-socket-io';
import { Document } from 'src/app/models/document.model';

@Injectable({
  providedIn: 'root'
})
export class DocumentService {
  currentDocument = this.socket.fromEvent<Document>('document');
  documents = this.socket.fromEvent<string[]>('documents');

  constructor(private socket: Socket) { }

  getDocument(id: string) {
    this.socket.emit('getDoc', id);
  }

  newDocument() {
    this.socket.emit('addDoc', { id: this.docId(), doc: '' });
  }

  editDocument(document: Document) {
    this.socket.emit('editDoc', document);
  }

  private docId() {
    let text = '';
    const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

    for (let i = 0; i < 5; i++) {
      text += possible.charAt(Math.floor(Math.random() * possible.length));
    }

    return text;
  }
}

여기서 메서드는 각각 소켓 서버가 수신하는 세 가지 이벤트 유형을 내보낸다는 것을 나타냅니다. 속성 currentDocumentdocuments는 클라이언트에서 Observable로 사용되는 소켓 서버에서 방출된 이벤트를 나타냅니다.

this.docId()에 대한 호출을 확인할 수 있습니다. 이것은 문서 ID로 할당할 임의의 문자열을 생성하는 작은 개인 메서드입니다.

문서 목록 구성 요소

문서 목록을 sidenav에 넣겠습니다. 지금은 임의의 문자열인 docId만 표시됩니다.

document-list.component.html을 엽니다.

  1. nano src/app/components/document-list/document-list.component.html

그리고 내용을 다음으로 바꿉니다.

<div class='sidenav'>
    <span
      (click)='newDoc()'
    >
      New Document
    </span>
    <span
      [class.selected]='docId === currentDoc'
      (click)='loadDoc(docId)'
      *ngFor='let docId of documents | async'
    >
      {{ docId }}
    </span>
</div>

document-list.component.scss 열기:

  1. nano src/app/components/document-list/document-list.component.scss

그리고 몇 가지 스타일을 추가합니다.

.sidenav {
  background-color: #111111;
  height: 100%;
  left: 0;
  overflow-x: hidden;
  padding-top: 20px;
  position: fixed;
  top: 0;
  width: 220px;

  span {
    color: #818181;
    display: block;
    font-family: 'Roboto', Tahoma, Geneva, Verdana, sans-serif;
    font-size: 25px;
    padding: 6px  8px  6px  16px;
    text-decoration: none;

    &.selected {
      color: #e1e1e1;
    }

    &:hover {
      color: #f1f1f1;
      cursor: pointer;
    }
  }
}

document-list.component.ts를 엽니다.

  1. nano src/app/components/document-list/document-list.component.ts

그리고 클래스 정의에 다음을 추가합니다.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';

import { DocumentService } from 'src/app/services/document.service';

@Component({
  selector: 'app-document-list',
  templateUrl: './document-list.component.html',
  styleUrls: ['./document-list.component.scss']
})
export class DocumentListComponent implements OnInit, OnDestroy {
  documents: Observable<string[]>;
  currentDoc: string;
  private _docSub: Subscription;

  constructor(private documentService: DocumentService) { }

  ngOnInit() {
    this.documents = this.documentService.documents;
    this._docSub = this.documentService.currentDocument.subscribe(doc => this.currentDoc = doc.id);
  }

  ngOnDestroy() {
    this._docSub.unsubscribe();
  }

  loadDoc(id: string) {
    this.documentService.getDocument(id);
  }

  newDoc() {
    this.documentService.newDocument();
  }
}

속성부터 시작하겠습니다. documents는 사용 가능한 모든 문서의 스트림입니다. currentDocId는 현재 선택된 문서의 ID입니다. 문서 목록은 우리가 어떤 문서에 있는지 알아야 하므로 sidenav에서 해당 문서 ID를 강조 표시할 수 있습니다. _docSub는 현재 또는 선택된 문서를 제공하는 Subscription에 대한 참조입니다. ngOnDestroy 수명 주기 메서드에서 구독을 취소하려면 이것이 필요합니다.

loadDoc()newDoc() 메서드는 아무 것도 반환하거나 할당하지 않습니다. 소켓 서버로 이벤트를 발생시키고 Observable로 다시 이벤트를 발생시킨다는 점을 기억하세요. 기존 문서를 가져오거나 새 문서를 추가하기 위해 반환되는 값은 위의 Observable 패턴에서 실현됩니다.

문서 구성 요소

이것은 문서 편집 표면이 될 것입니다.

document.component.html을 엽니다.

  1. nano src/app/components/document/document.component.html

그리고 내용을 다음으로 바꿉니다.

<textarea
  [(ngModel)]='document.doc'
  (keyup)='editDoc()'
  placeholder='Start typing...'
></textarea>

document.component.scss를 엽니다.

  1. nano src/app/components/document/document.component.scss

그리고 기본 HTML textarea에서 일부 스타일을 변경합니다.

textarea {
  border: none;
  font-size: 18pt;
  height: 100%;
  padding: 20px  0  20px  15px;
  position: fixed;
  resize: none;
  right: 0;
  top: 0;
  width: calc(100% - 235px);
}

document.component.ts를 엽니다.

  1. src/app/components/document/document.component.ts

그리고 클래스 정의에 다음을 추가합니다.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { startWith } from 'rxjs/operators';

import { Document } from 'src/app/models/document.model';
import { DocumentService } from 'src/app/services/document.service';

@Component({
  selector: 'app-document',
  templateUrl: './document.component.html',
  styleUrls: ['./document.component.scss']
})
export class DocumentComponent implements OnInit, OnDestroy {
  document: Document;
  private _docSub: Subscription;

  constructor(private documentService: DocumentService) { }

  ngOnInit() {
    this._docSub = this.documentService.currentDocument.pipe(
      startWith({ id: '', doc: 'Select an existing document or create a new one to get started' })
    ).subscribe(document => this.document = document);
  }

  ngOnDestroy() {
    this._docSub.unsubscribe();
  }

  editDoc() {
    this.documentService.editDocument(this.document);
  }
}

위의 DocumentListComponent에서 사용한 패턴과 유사하게 현재 문서의 변경 사항을 구독하고 현재 문서를 변경할 때마다 소켓 서버에 이벤트를 발생시킵니다. 즉, 다른 클라이언트가 우리와 동일한 문서를 편집하는 경우 모든 변경 사항을 볼 수 있으며 그 반대의 경우도 마찬가지입니다. 우리는 RxJS startWith 연산자를 사용하여 사용자가 앱을 처음 열 때 작은 메시지를 보냅니다.

AppComponent

app.component.html을 엽니다.

  1. nano src/app.component.html

그리고 내용을 다음으로 대체하여 두 개의 사용자 지정 구성 요소를 구성합니다.

<app-document-list></app-document-list>
<app-document></app-document>

3단계 - 실행 중인 앱 보기

소켓 서버가 여전히 터미널 창에서 실행 중인 상태에서 새 터미널 창을 열고 Angular 앱을 시작하겠습니다.

  1. ng serve

별도의 브라우저 탭에서 http://localhost:4200 인스턴스를 두 개 이상 열고 작동 상태를 확인합니다.

이제 새 문서를 만들고 두 브라우저 창에서 모두 업데이트되는 것을 볼 수 있습니다. 한 브라우저 창에서 변경하고 다른 브라우저 창에 반영된 변경 사항을 볼 수 있습니다.

결론

이 자습서에서는 WebSocket 사용에 대한 초기 탐색을 완료했습니다. 실시간 문서 협업 애플리케이션을 구축하는 데 사용했습니다. 서버에 연결하고 여러 문서를 업데이트 및 수정하기 위해 여러 브라우저 세션을 지원합니다.

Angular에 대해 자세히 알아보려면 연습 및 프로그래밍 프로젝트에 대한 Angular 주제 페이지를 확인하십시오.

Vue.js와 Socket.IO 통합에 대해 더 알고 싶다면.

추가 WebSocket 프로젝트에는 실시간 채팅 애플리케이션이 포함됩니다. React 및 GraphQL을 사용하여 실시간 채팅 앱을 구축하는 방법을 참조하십시오.