AJAX, WebSocket

JavaScript로 비동기 HTTP 요청을 보내는 AJAX에 대해서 공부합니다. 또한 웹 브라우저 상에서 게임, 채팅 등 양방향 통신에 사용되는 웹 소켓 기술과 Socket.io에 대해서 알아봅니다.
웹 소켓, Socket.io, AJAX, 비동기 HTTP 요청


AJAX

AJAXAJAX

동기, 비동기 Request의 비교

동기 Request비동기 Request (AJAX)
통신이 완료 될 때까지 웹 브라우저가 대기통신 중 웹 브라우저는 다른 작업을 수행
Reponse가 도착하면 페이지가 갱신Response가 도착하면 콜백을 실행
페이지가 갱신되기에 일반적으로 Response는 HTML 문서Response가 오직 데이터(JSON 등)라도 동적으로 DOM을 생성 할 수 있음

Ajax(Asynchronous JavaScript And XML)는 웹 브라우저 상에서 JavaScript로 비동기 HTTP Request를 보내는 기술을 의미합니다. 이름과는 관계 없이 비동기 HTTP 통신으로 XML 뿐만 아니라, HTML, JavaScript, 바이너리 등 어떠한 Content-Type의 데이터도 주고 받을 수 있습니다.

Ajax를 이용한 Flickr 갤러리

XMLHttpRequestfetch 함수 같은 브라우저의 내장 객체를 이용해도 좋지만, 좀 더 추상화된, 또 크로스 브라우징 문제를 해결해주는 AJAX 라이브러리의 Ajax 관련 API를 이용 할 수 있습니다.

XMLHttpRequest 객체
ES6의 fetch 함수
jQuery Ajax API
jQuery Ajax Shorthand API

jQuery Ajax Shorthand API

$.get("URL_TO_GET_REQUEST")
    .done(function() {
      // 성공적 응답
    })
    .fail(function() {
      // 오류 응답 또는 바디의 해석 오류
    })
    .always(function() {
      // 응답 후 항상
    });

$.post("URL_TO_POST_REQUEST")
    .done(function() {
      // 성공적 응답
    });

쉬운 예를 위해 jQuery를 예로 들었지만, 실제 제품에는 Axios 같은 풍부한 라이브러리를 사용하면 좋겠습니다.

jQuery Ajax API를 이용한 Flickr 갤러리

WebSocket

Google Analytics RealtimeGoogle Analytics Realtime

HTTP 프로토콜은 기본적으로 TCP 연결을 지속적으로 열어두지 않습니다. 태생적으로 문서 전달을 위한 프로토콜이기에 Request와 Response가 교환되면 TCP 연결을 종료합니다. 또한 HTTP 프로토콜은 단방향 통신이기에 클라이언트의 Request 없이는 서버에서 일방적으로 데이터를 전송(Server Push) 할 수 없습니다. 즉 HTTP 프로토콜로는 실시간 상호작용이 힘들고, 반복적인 통신으로 실시간을 흉내내더라도 계속해서 첨부되는 헤더 때문에 효율성이 떨어집니다.

웹 서비스가 발달하면서 웹 브라우저에서 실시간 연결에 대한 수요가 생겼습니다. 채팅 서비스, 모니터링 서비스 등은 정말 데이터를 실시간으로 반영 할까요? 실시간이라는 기준 자체가 명확하지 않지만, 웹 소켓의 등장 이전에는 Server Push를 구현하기 위해서 Comet으로 불리는 다양한 편법들을 쓰거나, 서버에 몇 초, 몇 십초에 한번씩 Ajax 등을 이용해 페이지 전환 없이 Request를 보냄으로써 실시간을 흉내 낼 수 있었습니다.

WebSocket 연결 과정WebSocket 연결 과정

그러던 2011년 웹 브라우저에서 실시간, 양방향 통신을 지원하기 위한 WebSocket 프로토콜(ws://)이 표준화되었으며, 대부분의 최신 웹 브라우저가 이를 지원하고 있습니다. (IE 10버전 이하는 지원하지 않음)

웹 소켓은 최초 연결시 HTTP 프로토콜을 이용합니다. 클라이언트가 Upgrade: WebSocket 등의 헤더를 웹 서버에 보내고, 웹 서버가 웹 소켓을 지원하는 경우 TCP 연결을 유지하면서 WebSocket 프로토콜로 전환합니다. 즉 웹 서버측에서 WebSocket 프로토콜을 지원 할 준비가 되어있어야 합니다. 이후 WebSocket 프로토콜을 통해 TCP 연결처럼 양방향으로 바이너리, 또는 UTF-8로 인코딩된 텍스트를 전송 할 수 있습니다.

추가로 HTTPS 프로토콜에 대응되는 암호화된 Secure WebSocket 프로토콜(wss://)도 존재합니다.

웹 브라우저의 native WebSocket 객체

Socket.io

Socket.ioSocket.io

WebSocket은 TCP for Web의 표준으로 채택되었지만 크로스 브라우징 문제에서 자유롭지는 않습니다. Socket.io 라이브러리는 TCP for Web 서버 작성을 위한 추상화된 Node.js 모듈과, 웹 브라우저 환경에 맞게 적절한 Comet 방식 또는 WebSocket을 이용해 연결을 맺어주는 JavaScript 모듈을 제공하고 있습니다. 즉 디바이스나 웹 브라우저에 관계 없이 WebSocket 연결이나 실시간의 흉내를 위한 서버, 클라이언트 양측을 위한 모듈입니다.

Realtime Graph with WebSocketRealtime Graph with WebSocket

실시간 통신을 이용하면 웹에서 게임, 채팅, 스트리밍 등 다양한 형태의 서비스를 제공 할 수 있습니다. 예시로 Socket.io를 이용해서 간단한 실시간 그래프를 구현해보도록 하겠습니다.

Socket.io 홈페이지 및 API 문서

ws/index.js (Server)

const http = require('http');
const fs = require('fs');
const path = require('path');
const io = require('socket.io');

// 클라이언트에게 줄 index.html, socket.io.js
let clientHTML = fs.readFileSync(path.join(__dirname, 'index.html'));
let clientJS = fs.readFileSync(path.join(__dirname, 'node_modules/socket.io-client/socket.io.js'));

// 일반적인 웹 서버 (HTTP)
let webServer = http.createServer((req,res) => {
    if (req.url == '/') {
        res.statusCode = 200;
        res.write(clientHTML);
        res.end();

    } else if (req.url == '/socket.io.js') {
        res.statusCode = 200;
        res.write(clientJS);
        res.end();  
    
    } else {
        res.statusCode = 404;
        res.end();
    }

}).listen(7000);

// 웹소켓, 혹은 Comet 방식을 지원하는 확장된 webServer (WS / HTTP)
let wsServer = io(webServer);
wsServer.on('connect', socket => { // 새 소켓이 연결 될 때
    
    // hello_my_name_is 타입의 메세지를 받았을 때
    socket.on('hello_my_name_is', data => {
        console.log(data, 'is connected');

        // 0.05초에 한번씩 graph_data 타입의 메세지를 보내줌
        setInterval(() => {
                        // 데이터는 랜덤으로 생성    
            let data = socket.oldData || {x:0, y: Math.floor(Math.random()*300)};
            data.x += 1;
            data.y += Math.floor(Math.random()*30 - 15) // -15~15
            data.y = data.y < 0 ? 0 : (data.y > 300 ? 300 : data.y);

            socket.emit('graph_data', data);
            socket.oldData = data;
        }, 50);
    });
});

ws/index.html (Client)

<html>
<body>
<style>
#map {
    border: 1px solid #eee;
    box-shadow: 5px 0 5px 0 rgba(0,0,0,0.1);
    width: 100%;
    height: 300px;
    position:relative;
    overflow-x: scroll;
}
#map div {
    position:absolute;
    display:inline-block;
    width: 4px;
    height: 4px;
    background-color: red;
    border-radius: 50%;
}
</style>

<h1>Realtime Graph <small>(<span id="x">0</span>)</small></h1>
<div id="map">
</div>

<script src="/socket.io.js"></script>
<script>
var name = "Client-"+Math.floor(Math.random()*1000);
var socket = io('http://localhost:7000');

socket.on('connect', function(){ // 서버와 연결 시
  socket.emit('hello_my_name_is', name); // hello_my_name_is 타입의 메세지 보냄
});

socket.on('ok_nice_to_meet_you', function(){ // ok_nice_to_meet_you 타입의 메세지 받음
    console.log('server said nice to meet you');
});

socket.on('graph_data', function(data){ // graph_data 타입의 메세지 받음
    var mapDiv = document.getElementById('map');
    var pointDiv = document.createElement('div');
    var xSpan = document.getElementById('x');
    xSpan.textContent = data.x;

    pointDiv.style.left = data.x + 'px';
    pointDiv.style.top = data.y + 'px';

    mapDiv.appendChild(pointDiv);
    mapDiv.scrollLeft = mapDiv.scrollWidth;
});
</script>
</body>
</html>
목차
4. 웹 백엔드
5. 데이터베이스
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.