본문 바로가기
프로그래밍/PHP

Ajax와 PHP를 사용하여 채팅 애플리케이션 구현하기

by 백룡화검 2010. 12. 31.

http://www.ibm.com/developerworks/kr/library/x-ajaxxml8/?ca=dnn-krt-20080123

 

 

Jack D Herrington, Senior Software Engineer, Leverage Software Inc.

2008 년 1 월 22 일

Asynchronous JavaScript™ + XML (Ajax)과 PHP를 사용하여 채팅 시스템을 웹 애플리케이션에 구현해 봅시다. 여러분의 고객들은 특정 인스턴트 메시징 소프트웨어를 다운로드 하거나 설치하지 않고, 사이트의 콘텐트에 대해 서로 이야기 할 수 있습니다.

Web 2.0이라는 용어가 생겨나면서 개발자들은 커뮤니티에 대해 많은 이야기를 한다. 여러 가지 이야기들이 있지만, 어쨌든, 고객 또는 독자가 당면 주제나 여러분이 팔고 있는 제품에 관해 즉각적인 대화를 할 수 있도록 한다는 것은 매우 매력적인 일이 아닐 수 없다. 하지만, 여기에 어떻게 접근할 것인가? 제품 목록 페이지 같은 곳에 채팅 박스를 놓아서 고객들이 특별한 브라우저 또는 Adobe Flash Player를 설치할 필요가 없게끔 할 수 있을까? 가능하다! PHP, MySQL, dynamic HTML (DHTML), Ajax, Prototype.js 라이브러리 같은 무료의 툴들을 사용하면 가능하다.


고민하지 말고, 바로 구현에 들어가 보자.

로그인

채팅의 첫 번째 단계는 ID를 갖는 것이다. Listing 1에 보이는 것 같은 기본적인 로그인 페이지가 필요하다.


Listing 1. index.html

                
<html>
<head><title>Chat Login</title></head>
<body>
<form action="chat.php" method="post">
Username: <input type="text" name="username">
<input type="submit" value="Login">
</form>
</body>
</html>

그림 1은 이 페이지의 스크린샷 모습이다.


그림 1. 채팅용 로그인 창

주: 그저 누가 이야기를 하고 있는지를 구분하면 되기 때문에 이 예제에서는 이것만 필요하다. 여러분의 애플리케이션의 경우, 이미 로그인 페이지가 있을 것이므로, 기존 사용자 이름을 사용해도 된다.




위로


기본적인 채팅 시스템

채팅 시스템은 단순한 스트링 테이블로서, 각 스트링은 개인에게 할당된다. 가장 기본적인 버전의 스키마는 Listing 2와 같다.


Listing 2. chat.sql

                
DROP TABLE IF EXISTS messages;

CREATE TABLE messages (
message_id INTEGER NOT NULL AUTO_INCREMENT,
username VARCHAR(255) NOT NULL,
message TEXT,
PRIMARY KEY ( message_id )
);

이 스크립트에는 자동 증가하는 메시지 ID, 사용자 이름, 메시지를 포함하고 있다. 중요하다고 생각한다면, 각 메시지에 타임 스탬프를 추가하여 메시지가 보내진 시간을 추적할 수 있다.

다른 주제에 대해 다중 대화들을 관리하고 싶다면, 주제를 트래킹 하고, 메시지 테이블에 관련 topic_id를 포함하고 있는 또 다른 테이블이 있어야 한다. 예제를 단순화 하기 위해서, 가장 간단한 스키마만 사용했다.

데이터베이스를 설정하고 스키마를 로딩하기 위해, 다음 명령어를 사용했다:

% mysqladmin create chat
% mysql chat < chat.sql

MySQL 서버 설정과 보안 설정과 패스워드에 따라서, 명령어는 약간씩 다르다.

이 채팅의 기본적인 사용자 인터페이스(UI)는 Listing 3처럼 보인다.


Listing 3. chat.php

                
<?php
if ( array_key_exists( 'username', $_POST ) ) {
$_SESSION['user'] = $_POST['username'];
}
$user = $_SESSION['user'];
?>
<html>
<head><title><?php echo( $user ) ?> - Chatting</title>
<script src="prototype.js"></script>
</head>
<body>

<div id="chat" style="height:400px;overflow:auto;">
</div>

<script>
function addmessage()
{
new Ajax.Updater( 'chat', 'add.php',
{
method: 'post',
parameters: $('chatmessage').serialize(),
onSuccess: function() {
$('messagetext').value = '';
}
} );
}
</script>

<form id="chatmessage">
<textarea name="message" id="messagetext">
</textarea>
</form>

<button onclick="addmessage()">Add</button>

<script>
function getMessages()
{
new Ajax.Updater( 'chat', 'messages.php', {
onSuccess: function() { window.setTimeout( getMessages, 1000 ); }
} );
}
getMessages();
</script>

</body>
</html>

스크립트 위에, 로그인 페이지의 인자에서 사용자 이름을 받아서, 이것을 세션에 저장한다. 이 페이지는 계속해서 수 많은 Prototype.js JavaScript 라이브러리를 로딩하는데, 이것은 모든 Ajax 작동을 핸들한다.

이후에, 페이지는 채팅 메시지가 가는 한 지점을 갖는다. 이 영역은 파일 아래에 위치한 getMessages() JavaScript 함수로 채워진다.

메시지 영역 아래에 하나의 폼과 textarea가 있는데, 이곳에서 사용자는 메시지 텍스트를 입력한다. 또한 메시지를 채팅에 추가하는 Add라는 레이블이 달린 버튼이 생긴다.

이 페이지는 그림 2와 같은 모습이다.


그림 2. 간단한 채팅 창

getMessages() 함수를 자세히 보면, 이 페이지가 실제로 1,000 밀리초(1초)마다 서버를 폴링하여 새로운 메시지를 검사하고, 호출의 아웃풋을 페이지 위에 있는 메시지 영역에 둔다는 것을 알 수 있다. 이 글 후반에 폴링에 대해 이야기 하겠지만, 지금으로서는, 채팅의 기본적인 구현을 끝마쳐야 하므로 현재의 메시지 세트를 리턴하는 messages.php 페이지를 보여주겠다. 이 페이지는 Listing 4와 같다.


Listing 4. messages.php

                
<table>
<?php
require_once("DB.php");

$db =& DB::Connect( 'mysql://root@localhost/chat', array() );
if (PEAR::isError($db)) { die($db->getMessage()); }

$res = $db->query('SELECT * FROM messages' );
while( $res->fetchInto( $row ) )
{
?>
<tr><td><?php echo($row[1]) ?></td>
<td><?php echo($row[2]) ?></td></tr>
<?php
}
?>
</table>

스크립트 상단에, DB 라이브러리를 가진 데이터베이스로 연결하는데, 이것은 PEAR(참고자료)에서 사용할 수 있다. 아직 이 라이브러리를 설치하지 않았다면, 다음 명령어를 사용해서 설치하라:

% pear install DB

PEAR가 설치되면, 스크립트는 현재 메시지를 쿼리하고, 각 행을 보내고, 사용자 이름과 코멘트 텍스트를 출력한다.

마지막 스크립트는 add.php인데, 이것은 페이지 상의 addmessage() 함수에 있는 Prototype.js Ajax 코드에서 호출된 것이다. 이 스크립트는 세션에서 메시지 텍스트와 사용자 이름을 취하고, 새로운 행을 메시지 테이블에 삽입한다. 이 코드는 Listing 5와 같은 모습이다.


Listing 5. add.php

                
<?php
require_once("DB.php");

$db =& DB::Connect( 'mysql://root@localhost/chat', array() );
if (PEAR::isError($db)) { die($db->getMessage()); }

$sth = $db->prepare( 'INSERT INTO messages VALUES ( null, ?, ? )' );
$db->execute( $sth, array( $_SESSION['user'], $_POST['message'] ) );
?>
<table>
<?php
$res = $db->query('SELECT * FROM messages' );
while( $res->fetchInto( $row ) )
{
?>
<tr><td><?php echo($row[1]) ?></td>
<td><?php echo($row[2]) ?></td></tr>
<?php
}
?>
</table>

add.php 스크립트 역시 현재 메시지 리스트를 리턴한다. 원래 페이지의 Ajax 코드는 리턴된 HTML 코드에서 채팅 메시지를 업데이트 하기 때문이다. 이러한 작동은 사용자에게 코멘트를 대화에 추가했던 즉각적인 피드백을 제공한다.

이것이 채팅 시스템의 기본이다. 다음 섹션에서는 폴링(polling)을 더욱 효과적으로 하는 방법을 설명하겠다.




위로


더 나은 채팅

원래의 채팅 시스템에서는, 페이지가 매 초마다 대화를 위해 모든 채팅 메시지를 요청한다. 짧은 대화에는 그렇게 나쁜 것은 아니지만, 더 긴 대화일 경우, 실질적인 성능 문제가 될 수 있다. 다행히도, 매우 쉬운 솔루션이 있다. message_id는 각 메시지와 연결되며, 그 수는 점점 증가하고 있다. 따라서, 여러분이 특정 ID에 대한 메시지를 갖고 있다는 것을 알고 있다면, 그 ID 다음에 발생하는 메시지를 요청한다. 이것으로 메시지 트래픽을 줄일 수 있다. 대부분의 요청에서, 새로운 메시지를 거의 받지 않으며, 이것은 매우 작은 패킷이다.

설정을 보다 효율적인 디자인으로 바꾸려면 chat.php 페이지를 약간 수정해야 한다. (Listing 6)


Listing 6. chat.php (revised)

                
<?php
if ( array_key_exists( 'username', $_POST ) ) {
$_SESSION['user'] = $_POST['username'];
}
$user = $_SESSION['user'];
?>
<html>
<head><title><?php echo( $user ) ?> - Chatting</title>
<script src="prototype.js"></script>
</head>
<body>

<div style="height:400px;overflow:auto;">
<table id="chat">
</table>
</div>

<script>
function addmessage()
{
new Ajax.Request( 'add.php', {
method: 'post',
parameters: $('chatmessage').serialize(),
onSuccess: function( transport ) {
$('messagetext').value = '';
}
} );
}
</script>

<form id="chatmessage">
<textarea name="message" id="messagetext">
</textarea>
</form>

<button onclick="addmessage()">Add</button>

<script>
var lastid = 0;
function getMessages()
{
new Ajax.Request( 'messages.php?id='+lastid, {
onSuccess: function( transport ) {
var messages = transport.responseXML.getElementsByTagName( 'message' );
for( var i = 0; i < messages.length; i++ )
{
var message = messages[i].firstChild.nodeValue;
var user = messages[i].getAttribute('user');
var id = parseInt( messages[i].getAttribute('id') );

if ( id > lastid )
{
var elTR = $('chat').insertRow( -1 );
var elTD1 = elTR.insertCell( -1 );
elTD1.appendChild( document.createTextNode( user ) );
var elTD2 = elTR.insertCell( -1 );
elTD2.appendChild( document.createTextNode( message ) );

lastid = id;
}
}
window.setTimeout( getMessages, 1000 );
}
} );
}
getMessages();
</script>

</body>
</html>

모든 메시지들을 보유하고 있는 "chat" <div> 태그 대신에, 이제는 <table> 태그가 생겼다; 메시지가 들어올 때마다 각각의 새로운 메시지에 대해 동적으로 행을 태그에 추가한다. getMessages() 함수에 변경 사항이 생겼음을 볼 수 있는데, 이것은 첫 버전보다 약간 변했다.

새로운 버전의 getMessages()는 messages.php 페이지의 결과가 새로운 메시지를 가진 XML 블록이 될 것으로 기대한다. messages.php 페이지는 이제 id라고 하는 매개변수를 받는데, 이것은 페이지가 보았던 마지막 메시지의 message_id이다. 첫 번째 타임아웃 시, 이 ID는 0이기 때문에, messages.php 페이지는 갖고 있는 모든 것을 리턴한다. 이후에, 지금까지 보았던 마지막 메시지의 ID가 보내진다.

XML 응답은 onSuccess 핸들러에 의해 나뉘고, 각 엘리먼트는 insertRow(), insertCell(), appendChild() 같은 표준 DHTML 문서 객체 모델(DOM) 함수들을 사용하여 테이블에 추가된다.

HTML 대신 XML을 리턴하는 messages.php 파일의 업그레이드 버전은 Listing 7에 보이는 것과 같다.


Listing 7. messages.php

                
<?php
require_once("DB.php");

header( 'Content-type: text/xml' );

$id = 0;
if ( array_key_exists( 'id', $_GET ) ) { $id = $_GET['id']; }

$db =& DB::Connect( 'mysql://root@localhost/chat', array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
?>
<messages>
<?php
$res = $db->query( 'SELECT * FROM messages WHERE message_id > ?', $id );
while( $res->fetchInto( $row ) )
{
?>
<message id="<?php echo($row[0]) ?>" user="<?php echo($row[1]) ?>">
<?php echo($row[2]) ?>
</message>
<?php
}
?>
</messages>

그림 3은 새롭게 향상된 버전을 보여주고 있다.


그림 3. 최적화 된 채팅 창

룩앤필의 관점에서 볼 때 바뀐 것이 전혀 없다. 하지만, 원본 보다 훨씬 더 효율적이다.




위로


"실시간(real time)"이라는 신화

Ajax가 처음이거나, 오랫동안 이 분야에서 작업해온 능력 있는 프로그래머이든, "폴링(polling)" 이라는 개념은 늘 어렵다. 안타깝게도, 폴링이라는 문제는 여러분 모두 갖고 있는 문제이다. 라인 끝에 설치된 특별한 소프트웨어 없이는 클라이언트와 서버 간 크로스 플랫폼, 크로스 브라우저 방식으로 지속적인 파이프를 생성할 수 있는 방법이 없다. 심지어, 이 모든 것이 발생하게 하려면 특별한 방화벽 설정이 필요하다. 따라서, 여러분이 모든 사람들이 사용할 수 있는 쉬운 솔루션을 원한다면, Ajax와 폴링이 답이 된다.

하지만, 마케팅과 "실시간(real time)"에 대한 고집은 어떻게 생각하는가? 폴링은 실시간이 될 수 없다. 가능하기는 할까? 필자가 생각하기에 실시간이란 용어에 대한 정의는 여러분에게 달려있다. 필자가 전기 생리학 데이터를 얻기 위해 코드를 작성했을 때, 실시간은 마이크로초(microseconds)를 의미했다. 지질학자는 수분, 수일, 심지어는 수년의 개념으로 실시간을 생각할 수 있다.

Wikipedia를 찾아보면, 인간의 평균 반응 시간은 200에서 270 마이크로초라고 한다. 이것은 공을 치는 것 같은 행동을 하는 시간에 해당한다. 메시지를 읽고 응답을 작성하기 시작하는데 걸리는 시간은, 여러분이 아무리 대화에 몰두하더라도 더 길기 마련이다. 따라서, 200-밀리초 범위(약간 더 긴 범위)의 시간이면 이러한 채팅 메시지들을 기다리는데 알맞다. 1초 정도면 필자에게도 그리 느리게 느껴지지 않는다.

developerWorks의 Ajax 포럼 운영자로서(참고자료), 폴링실시간의 이슈는 적어도 한 달에 한번씩은 반드시 등장한다. 필자는 Ajax에 관한 폴링 문제와 실시간 문제를 다뤘다. 점점 복잡해지는 실시간 솔루션을 생성하기 전에 폴링을 시도할 것을 권하고 싶다. 커스텀 솔루션을 시도하기 전에 상용 툴들을 사용해 보는 것도 좋다.




위로


구현 가이드

이 섹션에서는 여러분의 애플리케이션에 채팅 시스템을 구현하는데 도움이 되는 가이드를 제공하겠다. 다음은 구체적인 구현 가이드이다:

  • 사용자 트래킹: 대화에 적극적으로 참여하고 있는 사람들의 리스트를 채팅 창에 둔다. 이렇게 하면 사람들은 누가 참여하고 있고, 들어왔다 나가는지를 알 수 있다.
  • 다중 대화 허용하기: 다른 주제에 대해 여러 대화들이 동시에 진행되도록 한다.
  • 이모티콘 사용하기: :-) 같은 문자 조합을 스마일리 페이스의 알맞은 이미지로 변환한다.
  • URL 파싱 사용하기: 클라이언트 측 JavaScript 코드에서 정규식을 사용하여 URL을 찾고, 이것을 하이퍼링크로 변환한다.
  • Enter 키 핸들하기: Add 버튼 대신에, textareaonkeydown 이벤트로 연결하여, Enter 또는 Return 키를 누르는 사용자들을 감시한다.
  • 사용자가 입력하고 있는 것을 보여주기: 사용자가 타이핑을 시작하면 서버에 경고하여, 다른 참여자들이 응답이 진행 중이라는 것을 볼 수 있도록 한다. 이는 상대방의 타이핑 속도가 느릴 경우, 대화가 잠깐 끊어졌다는 인식을 줄여준다.
  • 게시된 메시지의 크기 제한하기: 대화를 지속시키는 또 다른 방법은 메시지를 작게 유지하는 것이다. textarea —의 최대 문자 수를 제한하여(onkeydown 트래핑) 대화의 속도를 높인다.

이 코드를 향상시킬 수 있는 몇 가지 아이디어를 제시해 보았다. 여러분도 향상점을 발견한다면, 커뮤니티에서 여러분의 의견을 나눠주기 바란다. 소스 코드(

다운로드)에 적용하겠다.




위로


결론

필자는 채팅을 많이 하는 편은 아니다. 채팅 클라이언트를 가져본 적도 없다. 긴 텍스트 메시지를 한 번씩 사용하곤 한다. 필자의 채팅 핸들은 idratheryouemail.이 다. 심각한 수준이다. 하지만, 이 글에서 설명한 것처럼 콘텍스트 채팅은 매우 매력적이다. 이것인 사이트가 다루는 주제에 초점을 맞추기 때문에 최신 "TomKat(톰 크루즈와 케이티 홈즈 소식 등의 연예계 소식)" 뉴스에 정신을 팔 일이 없다.

여러분의 웹 애플리케이션에 이 예제 코드를 시도해 보기 바란다. 여러분의 독자와 고객이 실시간 대화에 참여하는지를 보고, developerWorks Ajax 포럼 사이트에서 일이 어떻게 진행되는지를 알려주기 바란다. 여러분에게 유익한 시간이 되었기 바란다.