Socket.IO로 AngularJS 앱 작성

소개

AngularJS는 사용하기 쉽고 빠른 양방향 데이터 결합, 재사용 가능한 맞춤 구성요소를 만들 수 있는 강력한 디렉티브 시스템 등을 제공하는 멋진 JavaScript 프레임워크입니다. Socket.IO는 웹소켓을 위한 크로스브라우저 래퍼 및 폴리필로, 이를 사용하면 실시간 애플리케이션을 간편하게 개발할 수 있습니다. 두 가지 방법은 서로 잘 호환됩니다.

이전에 Express로 AngularJS 앱 작성에 관해 작성한 적이 있지만 이번에는 Socket.IO를 통합하여 AngularJS 애플리케이션에 실시간 기능을 추가하는 방법을 설명하겠습니다. 이 튜토리얼에서는 인스턴트 메시지 앱을 작성하는 방법을 설명합니다. 이 튜토리얼은 서버에서 유사한 node.js 스택을 사용하는 이전 튜토리얼을 기반으로 하므로 Node.js 또는 Express에 익숙하지 않은 경우 먼저 해당 튜토리얼을 확인하는 것이 좋습니다.

데모 열기

언제나처럼 GitHub에서 완성된 제품을 가져올 수 있습니다.

기본 요건

Socket.IO를 설정하고 Express와 통합하는 데 약간의 상용구가 필요하므로 Angular Socket.IO Seed를 만들었습니다.

시작하려면 GitHub에서 angular-node-seed 저장소를 클론합니다.

git clone git://github.com/btford/angular-socket-io-seed my-project

또는 ZIP 파일로 다운로드합니다.

시드가 있으면 npm으로 몇 가지 종속 항목을 가져와야 합니다. 시드가 있는 디렉터리의 터미널을 열고 다음을 실행합니다.

npm install

이러한 종속 항목을 설치하면 스켈레톤 앱을 실행할 수 있습니다.

node app.js

브라우저의 http://localhost:3000에서 확인하여 시드가 예상대로 작동하는지 확인합니다.

앱 기능 결정

채팅 애플리케이션을 작성하는 방법에는 여러 가지가 있으므로 이 애플리케이션에 포함될 최소한의 기능을 설명해 보겠습니다. 모든 사용자가 속한 채팅방은 하나만 있습니다. 사용자는 이름을 선택하고 변경할 수 있지만 이름은 고유해야 합니다. 서버는 이 고유성을 적용하고 사용자가 이름을 변경하면 이를 공지합니다. 클라이언트는 메시지 목록과 현재 채팅방에 있는 사용자 목록을 노출해야 합니다.

간단한 프런트엔드

이 사양을 사용하면 필요한 UI 요소를 제공하는 Jade로 간단한 프런트엔드를 만들 수 있습니다. views/index.jade를 열고 block body 내에 다음을 추가합니다.

div(ng-controller='AppCtrl')
.col
  h3 Messages
  .overflowable
    p(ng-repeat='message in messages') : 

.col
  h3 Users
  .overflowable
    p(ng-repeat='user in users') 

.clr
  form(ng-submit='sendMessage()')
    | Message: 
    input(size='60', ng-model='message')
    input(type='submit', value='Send')

.clr
  h3 Change your name
  p Your current user name is 
  form(ng-submit='changeName()')
    input(ng-model='newName')
    input(type='submit', value='Change Name')

public/css/app.css를 열고 열과 오버플로를 제공하는 CSS를 추가합니다.

/* app css stylesheet */

.overflowable {
  height: 240px;
  overflow-y: auto;
  border: 1px solid #000;
}

.overflowable p {
  margin: 0;
}

/* poor man's grid system */
.col {
  float: left;
  width: 350px;
}

.clr {
  clear: both;
}

Socket.IO와 상호작용

Socket.IO는 windowio 변수를 노출하지만 AngularJS의 의존성 주입 시스템에 캡슐화하는 것이 좋습니다. 먼저 Socket.IO에서 반환된 socket 객체를 래핑하는 서비스를 작성합니다. 나중에 컨트롤러를 훨씬 더 쉽게 테스트할 수 있으므로 좋습니다. public/js/services.js을 열고 콘텐츠를 다음으로 바꿉니다.

app.factory('socket', function ($rootScope) {
  var socket = io.connect();
  return {
    on: function (eventName, callback) {
      socket.on(eventName, function () {  
        var args = arguments;
        $rootScope.$apply(function () {
          callback.apply(socket, args);
        });
      });
    },
    emit: function (eventName, data, callback) {
      socket.emit(eventName, data, function () {
        var args = arguments;
        $rootScope.$apply(function () {
          if (callback) {
            callback.apply(socket, args);
          }
        });
      })
    }
  };
});

각 소켓 콜백을 $scope.$apply로 래핑합니다. 이렇게 하면 AngularJS에 전달된 콜백을 실행한 후 변경사항이 있는 경우 애플리케이션 상태를 확인하고 템플릿을 업데이트해야 한다고 알립니다. 내부적으로 $http도 동일한 방식으로 작동합니다. 일부 XHR이 반환된 후 $scope.$apply를 호출하여 AngularJS가 뷰를 적절하게 업데이트할 수 있습니다.

이 서비스는 전체 Socket.IO API를 래핑하지는 않습니다 (이는 독자에게 연습문제로 남겨 둡니다 ;P ). 하지만 이 가이드에서 사용된 메서드를 다루며, 이를 확장하려는 경우 올바른 방향을 제시해 줍니다. 전체 래퍼 작성을 다시 살펴볼 수도 있지만 이 튜토리얼에서는 다루지 않습니다.

이제 컨트롤러 내에서 $http와 마찬가지로 socket 객체를 요청할 수 있습니다.

function AppCtrl($scope, socket) {
  /* Controller logic */
}

컨트롤러 내에서 메시지 전송 및 수신 로직을 추가해 보겠습니다. js/public/controllers.js을 열고 콘텐츠를 다음으로 바꿉니다.

function AppCtrl($scope, socket) {

  // Socket listeners
  // ================

  socket.on('init', function (data) {
    $scope.name = data.name;
    $scope.users = data.users;
  });

  socket.on('send:message', function (message) {
    $scope.messages.push(message);
  });

  socket.on('change:name', function (data) {
    changeName(data.oldName, data.newName);
  });

  socket.on('user:join', function (data) {
    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + data.name + ' has joined.'
    });
    $scope.users.push(data.name);
  });

  // add a message to the conversation when a user disconnects or leaves the room
  socket.on('user:left', function (data) {
    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + data.name + ' has left.'
    });
    var i, user;
    for (i = 0; i < $scope.users.length; i++) {
      user = $scope.users[i];
      if (user === data.name) {
        $scope.users.splice(i, 1);
        break;
      }
    }
  });

  // Private helpers
  // ===============

  var changeName = function (oldName, newName) {
    // rename user in list of users
    var i;
    for (i = 0; i < $scope.users.length; i++) {
      if ($scope.users[i] === oldName) {
        $scope.users[i] = newName;
      }
    }

    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + oldName + ' is now known as ' + newName + '.'
    });
  }

  // Methods published to the scope
  // ==============================

  $scope.changeName = function () {
    socket.emit('change:name', {
      name: $scope.newName
    }, function (result) {
      if (!result) {
        alert('There was an error changing your name');
      } else {

        changeName($scope.name, $scope.newName);

        $scope.name = $scope.newName;
        $scope.newName = '';
      }
    });
  };

  $scope.sendMessage = function () {
    socket.emit('send:message', {
      message: $scope.message
    });

    // add the message to our model locally
    $scope.messages.push({
      user: $scope.name,
      text: $scope.message
    });

    // clear message box
    $scope.message = '';
  };
}

이 애플리케이션에는 뷰가 하나만 표시되므로 public/js/app.js에서 라우팅을 삭제하고 다음과 같이 간소화할 수 있습니다.

// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']);

서버 작성

routes/socket.js를 엽니다. 사용자 이름이 고유하도록 서버 상태를 유지하는 객체를 정의해야 합니다.

// Keep track of which names are used so that there are no duplicates
var userNames = (function () {
  var names = {};

  var claim = function (name) {
    if (!name || userNames[name]) {
      return false;
    } else {
      userNames[name] = true;
      return true;
    }
  };

  // find the lowest unused "guest" name and claim it
  var getGuestName = function () {
    var name,
      nextUserId = 1;

    do {
      name = 'Guest ' + nextUserId;
      nextUserId += 1;
    } while (!claim(name));

    return name;
  };

  // serialize claimed names as an array
  var get = function () {
    var res = [];
    for (user in userNames) {
      res.push(user);
    }

    return res;
  };

  var free = function (name) {
    if (userNames[name]) {
      delete userNames[name];
    }
  };

  return {
    claim: claim,
    free: free,
    get: get,
    getGuestName: getGuestName
  };
}());

기본적으로 이름 집합을 정의하지만 채팅 서버의 도메인에 더 적합한 API를 사용합니다. 클라이언트가 실행하는 호출에 응답하도록 서버의 소켓에 연결해 보겠습니다.

// export function for listening to the socket
module.exports = function (socket) {
  var name = userNames.getGuestName();

  // send the new user their name and a list of users
  socket.emit('init', {
    name: name,
    users: userNames.get()
  });

  // notify other clients that a new user has joined
  socket.broadcast.emit('user:join', {
    name: name
  });

  // broadcast a user's message to other users
  socket.on('send:message', function (data) {
    socket.broadcast.emit('send:message', {
      user: name,
      text: data.message
    });
  });

  // validate a user's name change, and broadcast it on success
  socket.on('change:name', function (data, fn) {
    if (userNames.claim(data.name)) {
      var oldName = name;
      userNames.free(oldName);

      name = data.name;

      socket.broadcast.emit('change:name', {
        oldName: oldName,
        newName: name
      });

      fn(true);
    } else {
      fn(false);
    }
  });

  // clean up when a user leaves, and broadcast it to other users
  socket.on('disconnect', function () {
    socket.broadcast.emit('user:left', {
      name: name
    });
    userNames.free(name);
  });
};

그러면 신청이 완료됩니다. node app.js를 실행하여 사용해 보세요. Socket.IO 덕분에 애플리케이션이 실시간으로 업데이트됩니다.

결론

이 채팅 앱에는 더 많은 기능을 추가할 수 있습니다. 예를 들어 빈 메시지를 제출할 수 있습니다. ng-valid를 사용하여 클라이언트 측에서 이를 방지하고 서버에서 확인할 수 있습니다. 서버는 앱에 가입하는 신규 사용자를 위해 최근 메시지 기록을 보관할 수 있습니다.

다른 라이브러리를 사용하는 AngularJS 앱을 작성하는 것은 라이브러리를 서비스로 래핑하고 Angular에 모델이 변경되었음을 알리는 방법을 이해하면 간단합니다. 다음으로 인기 있는 시각화 라이브러리인 D3.js와 함께 AngularJS를 사용하는 방법을 설명할 예정입니다.

참조

Angular Socket.IO 시드 완성된 인스턴트 메시지 앱 AngularJS Express Socket.IO`