코멧(Comet) #2 – Ajax 폴링(Ajax polling) 채팅방 예제로 배우기

코멧을 구현하는 첫 번째 방법은 Ajax 폴링(Ajax Polling)이라고 하는 기법입니다. 이 기법은 모든 코멧 방법론 중에서 가장 직관적이고 구현이 간단하여, 코멧 방법론에 입문할 때에 처음 만나게 되는 기법입니다. 기본적인 아이디어는 일정시간마다 Ajax 통신을 하여 서버의 상태를 가져오는 것이라고 요약할 수 있습니다.

이 포스팅에서는 자바스크립트와 더불어 PHP, MySQL로 구현한 간단한 채팅방 예제를 통해 Ajax 폴링을 구현하는 방법을 알아봅니다.

Ajax 폴링 채팅방 예제코드 (다운로드)

금번에 소개하는 예제는 Ajax 폴링 기법으로 최소한의 기능만을 갖춘 채팅방을 구현한 것입니다. zip 파일 1개로 압축되어 있으며, 아래와 같이 6개의 파일이 있습니다.

  • index.html : 채팅방 메인 페이지
  • proc.php : GET 파라메터로 주어진 날짜 이후의 채팅내용을 가져와 JSON 포맷으로 출력하는 페이지
  • write.php : 닉네임과 채팅내용을 받아 데이터베이스에 입력하는 페이지
  • chat.js : 자바스크립트 파일
  • chat.css : 스타일시트 파일
  • chat.sql : 데이터베이스 테이블을 생성하기 위한 sql문 (MySQL 기준)

본 예제를 여러분의 서버에 업로드하여 실행하실 때에는, 먼저 MySQL 상에서 chat.sql 파일 안의 sql문을 실행하여 DB 테이블을 생성하셔야 합니다. 또한 proc.php와 write.php 파일에는 DB 연결하는 코드가 있으므로, 이 부분을 여러분이 사용하는 MySQL의 계정과 비밀번호, 데이터베이스 이름으로 바꾸셔야 합니다.

Ajax Polling 채팅방 예제

제대로 업로드와 세팅을 마쳤다면, 이제 서로 다른 여러 개의 웹 브라우저로 index.php를 호출해보시면 위처럼 채팅방이 실행되는 모습을 볼 수 있습니다.

 

자바스크립트 예제코드 – chat.js

성공적으로 예제를 실행할 수 있었다면 이제 자바크스립트 코드를 살펴볼 차례입니다. 아래 예제코드에서는 좀더 분명한 설명을 위해, 여러분들이 다운로드 받은 소스코드 중 일부가 생략되어 있습니다.

var chatManager = new function(){
	var interval	= 500;
	var xmlHttp		= new XMLHttpRequest();
	var finalDate	= '';

	// Ajax Setting
	xmlHttp.onreadystatechange = function()
	{
		if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
		{
			// JSON 포맷으로 Parsing
			res = JSON.parse(xmlHttp.responseText);
			finalDate = res.date;
			
			// 채팅내용 보여주기
			chatManager.show(res.data);
		}
	}

	// 채팅내용 가져오기
	this.proc = function()
	{
		// Ajax 통신
		xmlHttp.open("GET", "proc.php?date=" + finalDate, true);
		xmlHttp.send();
	}

	// 채팅내용 보여주기
	this.show = function(data)
	{
		var o = document.getElementById('list');
		var dt, dd;

		// 채팅내용 추가
		for(var i=0; i<data.length; i++)
		{
			dt = document.createElement('dt');
			dt.appendChild(document.createTextNode(data[i].name));
			o.appendChild(dt);

			dd = document.createElement('dd');
			dd.appendChild(document.createTextNode(data[i].msg));
			o.appendChild(dd);
		}
	}

	// interval에서 지정한 시간마다 실행
	setInterval(this.proc, interval);
}

위 코드가 생성하고 있는 chatManager라는 객체는 먼저 XMLHttpRequest 객체를 하나 가지고 있습니다. 이 객체를 통해 Ajax 통신이 완료되었을 때 수행할 로직은 7번줄부터 정의되어 있는데, responseText를 JSON으로 파싱하여, 날짜는 변수에 저장하고, 채팅내용은 show() 함수에 인자로 넘겨주는 매우 간단한 형태입니다. 이렇게 호출된 show() 함수는 반복문으로 인자로 받은 채팅내용을 화면에 HTML 코드로 표시해줍니다.

실제로 Ajax 통신을 시작하는 함수는 21번줄부터 시작하는 proc()으로, 내용을 살펴보면 proc.php라는 파일에 변수에 저장되어 있던 날짜를 인자로 담아 HTTP 요청을 보내는 것을 볼 수 있습니다. 이 PHP 파일에는 데이터베이스에서 인자로 받은 날짜 이후에 입력된 채팅내용들과 HTTP 요청이 들어온 날짜를 JSON 포맷으로 출력해주는 코드가 담겨 있습니다. 이 proc() 함수는 48번줄의 setInterval()에 의해 500ms에 한 번씩 실행됩니다.

이상의 내용을 종합하면, 우리는 이 코드를 실행했을 때 어떻게 작동하는지를 알 수 있습니다. 위 코드는 500ms마다 Ajax 통신을 하여 데이터베이스로부터 JSON 포맷으로 된 채팅내용을 가져와 HTML 코드로 화면에 표시합니다. 이 과정에서 마지막으로 채팅내용을 가져온 날짜를 매번 갱신하기 때문에, 한 번 가져왔던 채팅내용은 다음 Ajax 요청에서는 가져오지 않게 됩니다. 일정시간마다 Ajax 통신으로 서버의 상태를 가져오는 Ajax 폴링 기법의 원리를 여기서 확인할 수 있습니다.

 

Ajax 폴링의 장단점

위 예제코드에서도 확인한 바와 같이, Ajax 폴링은 원리가 단순한만큼 간결한 소스코드 만으로도 손쉽게 구현할 수 있습니다. 생산성과 유지보수의 수월성 측면에서 Ajax 폴링은 확실한 이점을 가지고 있습니다. 그러나 단순한 구현방법에는 그만큼의 결점이 있는 법입니다.  Ajax 폴링의 한계는 2가지 정도로 정리할 수 있습니다.

첫 번째 한계는 HTTP 통신의 빈도가 비교적 높다는 것입니다. 이 때문에 실시간성이 중요한 대형 서비스에서 Ajax 폴링을 도입하기는 어렵습니다. 위 예제코드의 경우, HTTP 통신이 이루어지는 빈도는 1초에 2회입니다. 만약 위 예제코드로 서비스하는 채팅방에 100명이 접속한다면, 서버는 1초에 200회나 되는 접속량을 감당해야 할 것입니다.

두 번째 한계는 Ajax 통신에서 응답이 지연될 경우에 위험한 상황이 발생할 가능성이 있다는 것입니다. 이를테면 위 예제코드에서 서버나 데이터베이스에 부하가 높아져 proc.php 파일의 응답시간이 500ms를 넘으면 어떻게 될까요. 아직 이전의 Ajax 통신이 끝나지 않았음에도 다음 Ajax 통신이 중첩되어 실행될 우려가 있습니다. 이렇게 중첩되어 실행된 Ajax 통신은 또다시 지연을 가중시키고, 그렇게 느려진 와중에 또다시 Ajax 통신이 시작되고, 또다시 지연이 가중되는 악순환이 벌어집니다. 여러분께서 다운로드 받은 예제파일에는 이러한 문제를 막고자 Ajax 통신의 중복실행을 방지하는 플래그가 들어 있습니다.

 

Ajax 폴링을 도입하기에 적합한 상황

그렇다고 Ajax 폴링이 실무에서 전혀 쓸 수 없는 애물단지는 아닙니다. Ajax 폴링은 HTTP 통신간격을 충분히 두어도 되는 서비스에서는 유용하게 사용할 수 있습니다.

이를테면 스포츠 경기 문자중계의 경우, 10~20초에 한 번 정도만 업데이트 해주어도 비교적 만족스러운 서비스의 질을 유지할 수 있습니다. 이 정도의 시간간격이라면 서버가 감당해야 하는 접속량도 줄어들고, 일시적으로 응답이 지연되더라도 위험한 상황에 이를 가능성도 줄어들 것입니다.

21 thoughts on “코멧(Comet) #2 – Ajax 폴링(Ajax polling) 채팅방 예제로 배우기

    • 방문과 덧글 감사합니다. 롱 폴링(Long Polling)에 관한 3번째 포스팅도 최대한 빠르게 준비하겠습니다.

  1. 소중한 지식을 공유해 주셔서 감사합니다.
    혹시~ 작성되는 글이 지금은 위로 쌓이지만 아래쪽으로 내려가면서 정렬되게 하려면 어떻게 해야되는지 정중히 여쭈어 봅니다.

    • 해외에 있었던터라 답이 많이 늦어 죄송합니다. 예제파일을 실행해보시면, 노스텔지어님께서 원하시는대로 위에서 아래쪽으로 시간순 정렬이 되면서 채팅내용이 표시됨을 확인할 수 있습니다. 시간역순 정렬을 원하신다면 appendChild() 대신 insertBefore()를 활용하면 되겠습니다.

    • 저야말로 방문해주시고 흔적 남겨주셔서 감사합니다. 많은 도움이 되셨기를 바라겠습니다.

  2. socket.io 를 이용한 채팅 관련 포스팅도 해주실수있을까 싶네요. 이번에도 좋은 글 잘 읽었습니다^^

    • 심재문님 반갑습니다. IE 버전을 알지 못하고 콘솔창을 보지 않은 상태에서 정확히 진단하기는 어렵지만, 가장 쉽게 의심해볼 수 있는 부분은 12번줄의 JSON 객체입니다. JSON 객체는 IE7 이하에서 지원하지 않는 것으로 알려져 있고, IE8에서도 작동하지 않았다고 주장하는 사례도 있습니다. JSON 객체를 대체하는 방법에 대해서는 json.org 사이트에서도 다루고 있습니다. 다음 링크를 참고해보시면 좋겠습니다: http://www.json.org/js.html

  3. 군인이라서 예제를 그대로 사용못하고

    예제를 다운받아서 제 테스트용 PC에 손수 타이핑하면서 전부 적었는데

    예제안에 chat.js 에서 xmlHttpWrite.send(param.join(‘&’)); 부분에 send가 잘 안되는거 같습니다.

    DB에 안들어갑니다. 그리고 저 지점에서 디버깅을 해보니 readytState = 1, status = 0 이뜹니다.

    저 지점에서 readytState = 1, status = 0 이 나오는게 맞는건가요?

    아님 다른부분이 틀려서 안되는 걸까요?

    • 군복무로 노고가 많으십니다. 소스코드를 볼 수가 없어 정확한 진단은 어렵습니다만, 일반적으로 readyState 프로퍼티가 항상 1로 나오는 경우에는 디버깅 위치가 잘못된 경우가 많습니다. 제 예측이 맞다면, 아래와 같이 send() 메소드를 호출한 직후에 디버깅을 하셨을 가능성이 높습니다.

      xmlHttpWrite.send(param.join(‘&’));
      alert(xmlHttpWrite.readyState + ” ” + xmlHttpWrite.status);

      이것은 잘못된 것입니다. readyState 프로퍼티의 값은 최초에 0에서 시작하여, Ajax 통신이 진행되면서 1씩 올라가 최종적으로 4까지 변하게 됩니다. 하지만 위처럼 디버깅을 하면 send() 메소드를 호출한 직후의 readyState 값 밖에는 디버깅을 할 수 없습니다. 정확한 디버깅 방법은 아래와 같이 onreadystatechange() 메소드 안에서 디버깅을 하는 것입니다.

      xmlHttp.onreadystatechange = function()
      {
        alert(xmlHttpWrite.readyState + ” ” + xmlHttpWrite.status);
        // 그 외의 코드는 이 아래에 놓습니다.
      }

      onreadystatechange()는 readyState 값이 변할 때마다 호출되는 메소드입니다. 따라서 위와 같이 디버깅을 했을 때 Ajax 통신이 정확하게 이루어졌다면, 경고창이 4번 표시가 되며, 표시될 때마다 readyState이 1에서 4로 변화하는 모습을 볼 수 있습니다. 아울러 status 프로퍼티는 readyState가 4가 된 순간에, 200(정상) 혹은 404(페이지 없음) 중 하나로 변하는 것을 확인하게 될 것입니다.

      위 방법을 이용해서 다시 한 번 디버깅을 진행해보시고, 제가 잘못 예측했거나 후속질문이 있으시면 덧글 남겨주세요.

  4. 굉장히 수고가 많으십니다. 공유 해 주신것 또한 매우 감사하게 생각합니다.

    보고 많은 공부가 되었습니다. 정말 감사합니다.

    • 방문해주시고 댓글까지 남겨주셔서 감사합니다. Hidden iframe이나 롱폴링에 대해서도 준비를 해야하는데, 매번 작성을 못하고 있어서 많은 분들께 송구한 마음을 가지고 있습니다. 앞으로도 좋은 글로 찾아 뵙겠습니다.

      • 교육내용은 잘 읽어보았습니다. 감사합니다

        혹시 리눅스에서 코멧을이용하여 클라이언트들에게 일방적으로

        파일을 푸쉬가 가능하게끔 하려면 어떤 문서를 참조하면 될까요?

        (업데이트서버를 구현해보고자 합니다 , client들의 업데이트여부를 DB로 파악하여
        업데이트를 하지않은 pc들에게 일방적으로 파일 전송이 가능하게끔…)

        • 질문 감사드립니다. 먼저 말씀하신 내용은 서버푸시에 관한 것인데, 일반적으로는 HTTP 통신으로 구현할 수 없습니다. 이전 글(http://dev.epiloum.net/790)에도 다루었지만, 웹 환경의 기반이 되는 HTTP 통신은 클라이언트의 요청에 대해서 서버가 응답하는 형태로 작동하기 때문입니다. 클라이언트의 요청이 없이 서버가 단독으로 푸시를 할 수는 없습니다.

          가장 이상적인 방법은 서버 프로그램을 직접 작성하는 것입니다. 최근에는 Node.js와 같이 서버 프로그램을 쉽게 작성할 수 있는 도구가 많이 있습니다. 구현시 프로토콜은 자체적으로 정할 수도 있고, FTP와 같이 기존에 있는 프로토콜을 이용할 수도 있겠지요.

          끝내 아파치 서버와 HTTP 통신을 포기할 수 없다면, 생각을 조금 바꾸셔야 할 것 같습니다. 바로 클라이언트들이 충분히 짧은 시간간격으로 업데이트 서버에 HTTP 통신을 하는 것입니다. 업데이트 서버는 HTTP 통신을 받으면, 업데이트 여부를 DB에서 확인한 후에 필요한 파일을 전송하면 되고, 업데이트할 파일이 없다면 미리 정해진 행동을 취하게 하면 될 것입니다.

  5. 좋은 글 잘봤습니다^^ 제가 꼭 필요한 내용이었고 많은 도움이 되었습니다.
    질문이 있어서 댓글을 달게되었는데요
    db값을 ajax로 일정한 주기로 값을 가져오면서 row data bound를 할때 그 값을 체크해서
    1인경우 이미지를 바꾸고 알림을 주고 싶습니다.
    그런데 이미지는 바뀌는데 알림은 처음 로드시에 값이 1인경우에만 팝업이 뜨더라구요. 그뒤로 바뀌는 값들에 대해서는 알림팝업이 뜨지않습니다.
    팝업은 자바스크립트로 주었구요.. 좋은 방법이 없을까요ㅠㅠ

    • 안녕하세요, 글에 관심을 가져주시고 질문을 남겨주셔서 감사합니다. 소스코드를 직접 볼 수 없어 진단하기가 어렵습니다만, 이미지를 변경하는 코드와 Alert를 띄우는 코드가 같은 함수 안에 있다만 사실상 위와 같이 작동할 가능성은 대단히 낮아 보입니다.

      만약 두 코드가 같은 함수 안에 있음에도 위와 같이 작동한다면, 브라우저가 Alert를 막았을 가능성이 있습니다. (예를 들어 크롬의 경우, 자바스크립트로 표시한 Alert창에 “이 페이지가 추가적인 대화를 생성하지 않도록 차단합니다”라는 체크박스가 있어 이를 체크하면 더이상 해당 페이지에서 Alert가 표시되지 않습니다.) 이 경우가 의심되는 경우에는 브라우저를 변경하여 테스트해보시면 좋을 것 같습니다.

      만약 두 코드가 서로 다른 함수 안에 있을 때에는 경우의 수가 많습니다. 그러나 예상할 수 있는 가장 대표적인 실수는 Closure 문제, 즉 이벤트 발생의 시차로 인해 변수가 다른 값으로 덮어쓰여지는 문제를 예상할 수 있습니다. row data bound 할 때의 값을 전역변수에 넣어 참조하게 구현하셨거나 함수의 인자로 row data bound 할 때의 값을 넘기고 있다면, 실제로 각 코드가 실행될 때 변수에 무엇이 들어있는지 확인해보시는 것이 좋을 것 같습니다.

  6. 감사합니다 덕분에 Comet에 관하여 많은 지식을 배워 가는것 같습니다.
    다른 포스팅도 잘읽어보고 새로 올라올 글들도 기대하겠습니다!

    • cokey님, 안녕하세요. 좋은 포스팅으로 계속 찾아 뵈어야 하는데, 업무시간 외에 시간을 할애하기가 참 어렵습니다. 관심 가져주시는 따뜻한 덧글에 좀더 힘을 내야겠다는 다짐 해봅니다. 감사합니다.

  7. 포스팅 잘 읽었습니다~
    궁금한게 있어서 질문 하나 드립니다.. 현재 redis에 있는 데이터를 실시간으로 보여주는 페이지를 간단하게 만들고 있는데 폴링 방식이 맞는 건지 .. 궁금하네요 .. 1초에 한 번씩 redis에 데이터를 가져올 생각인데 데이터양이 그렇게 많지 않고 웹페이지 사용자도 3명정도입니다. 웹 소켓도 찾아 봤는데 그것보단 폴링 방식이 맞지 않을까 싶은데 .. 조언 부탁드리겠습니다.

    • mercy님, 반갑습니다. 추측하기로는 아마도 Redis에 저장된 값을 모니터링하는 도구를 작성하려고 하시는 것 같습니다.

      말씀하신 내용을 폴링으로 구현할 때 가장 중요하게 생각해보아야 할 부분은, 통신이 1초에 한 번 일어난다는 것입니다. 이것은 곧 어떠한 이유로 통신이 1초 이상 지연되는 상황이 발생하고 이것이 지속되면, connection이 쌓일 염려가 있음을 의미합니다. 만약 구현환경을 검토해보셨을 때, 이런 1초 이상의 지연이 자주 발생한다면 폴링은 적합한 구현방식이 아닙니다. 그러나 통상적인 상황에서는 통신이 1초 미만에 종료된다고 확신할 수 있고, 1초를 넘기는 상황은 (네트워크 부하 등의) 이상상황이며 빈번한 것이 아니라고 한다면, 폴링은 손쉽게 구현할 수 있고 유지보수가 쉬운 좋은 선택일 것입니다. 중복통신을 방지하기 위한 약간의 예외처리는 필요하겠습니다만.

      다만 그럼에도 불구하고 여전히 폴링이 리소스 소모가 많은 방식임은 분명합니다. 만약에 Redis에 저장된 데이터의 변경이 빈번하지 않다면, 폴링의 통신간격을 좀더 늘리시거나 롱폴링의 도입을 검토해보시는 것을 권해드립니다.

      한편, Web Socket을 이용한 구현은 제가 Redis를 현업에서 사용해보지 못해서 정확한 답을 드리기가 어렵습니다. 일단 command를 소켓 통신으로 Redis에 직접 전달할 수 있다면 검토해볼 수 있는 구현방식이기는 합니다. 다만 대부분의 DB들과 같이 소켓 통신으로 바이너리 데이터를 보내야 하는 경우에는, 아마 Web Socket으로는 제약되는 부분이 많을 것입니다. Webdis를 비롯해 HTTP 통신으로 Redis의 command를 전달할 수 있는 API도 있는 모양입니다만, 이런 API를 쓰려고 마음먹었다면 이미 폴링이나 롱폴링으로 손쉬운 구현이 가능하기 때문에 Web Socket을 고수할 필요가 사라질 것입니다.

홍길동 에 응답 남기기 응답 취소

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.

*

다음의 HTML 태그와 속성을 사용할 수 있습니다: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>