오늘 Node.js 공부하면서 드디어 웹 소켓과 관련된 내용이 나왔다. 웹 소켓으로 구현된 채팅 코드를 본 적이 있는데 코드는 상당히 짧았으나 자바스크립트 코드가 굉장히 읽기 어려웠고 내용도 모르니 어렵기만 했었다. 공부할때도 좀 긴가민가 했는데 어느순간 득음하였다.

웹에서는 소켓을 이용한 통신이 없었다. 최근에서야 실시간 통신을 흉내내기 위한 ws와 wss가 추가되었다. wss는 https와 마찬가지로 SSL을 적용한 프로토콜이다.

이전에 PHP에서 AJAX를 공부했을때, “와 이거 이용하면 채팅 만들 수 있겠는데?”라는 생각을 했었다. 웹 소켓이 등장하기 전에는 실제로 AJAX를 활용해서 소켓통신을 흉내냈다고 한다. 하지만 이는 서버에 심각한 부하를 일으킬 수 있어 권장되는 방법은 아니다. 이제는 웹 소켓이라는 것을 통해 실시간으로 통신하는 서버를 구현할 수 있게 되었다.

필자가 이해하기로 Node.js의 웹 소켓은 on과 emit의 통신이라고 이해했다. on은 수신자, emit은 송신자라는 개념으로 파악했다. 누군가 송신을해야 수신이 이루어지고 결과를 도출 할 수 있다. 서버에서 emit을 했으면 클라이언트에서 on, 클라이언트가 emit을 했으면 서버가 on. 이 과정의 반복이라고 이해했다.


서버측

1
2
3
4
socket.on('send message', (name, text) => {
	...
    io.emit('receive message', massage);
});

클라이언트측

1
2
3
4
5
6
7
socket.on('receive message', function(msg) {
    ...
});
function chat_submit() {
	socket.emit('send message', $('#name').val(), $('#message').val());
	...
}

위 코드를 살펴보자. 먼저 서버에서는 send message라는 신호를 기다리고 있다. 그럼 send message는 어디서 발생하는지 살펴보자. 클라이언트가 채팅을 입력하고 버튼을 누르면 발생하는 함수로 구현되어 있다. 클라이언트가 emit을 하면 기다리던 서버에서는 신호를 받고 receive message라는 신호를 보내준다. 그럼 receive message 신호를 on하던 클라이언트의 메소드가 실행된다. 이 과정의 반복이다.




사용할 NPM, 서버 프로그래밍

1
2
3
4
var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);

먼저 socket.io와 express가 필요하다. express를 사용하지 않고도 구현할 수 있지만 좀 더 간결하게 html을 전송할 수 있으므로 사용할 것이다.

1
2
3
4
5
6
7
app.get('/',(req, res) => {
  res.sendFile(__dirname + '/index.html');
});

app.get('/main.css', (req, res) => {
  res.sendFile(__dirname + '/main.css');
});

이제 사용자에게 위와같이 루트 디렉터리로 접근했을때 index.html 파일을 제공할 것이다. 또한 main.css라는 스타일시트를 제공할 것이다. 다수의 스타일 시트나 스크립트 파일을 전달할 예정이라면 다음과 같이 작성할 수 있다.

1
app.use(express.static(__dirname + '/public'));

이후 public 이라는 디렉터리에 스타일시트와 스크립트 파일을 넣어주면 큰 문제없이 static 파일들을 호출 할 수 있다.

1
var tempNick = ["감자탕","맛탕","새우탕","그라탕","갈비탕","백설탕"];

위는 사용자가 접근했을때 사용자에게 제공해 줄 임시 닉네임이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 사용자가 연결되면
io.on('connection', socket => {
	// 사용자의 소켓 아이디 출력
	console.log('User Connected: ', socket.id);

	// 사용될 닉네임을 결정
	var name = tempNick[Math.floor(Math.random()*6)];

	// 사용자에게 변경된 닉네임을 보내줌
	io.to(socket.id).emit('change name', name);

	// 사용자의 연결이 끊어지면
	socket.on('disconnect', () => {
		console.log('User Disconnected: ', socket.id);
	});

	// 사용자가 이름 변경 신호를 보내면
	socket.on('rename', (name,text) => {
		console.log(socket.id + '(' + name + ') => ' + text);
		// 사용자에게 변경된 닉네임을 보내줌
		io.to(socket.id).emit('change name', text);
		// (전체)사용자에게 메세지 전달
		io.emit('receive message', name +'님이 '+text+'(으)로 닉네임을 변경했습니다.');
	});
	
	// 사용자가 메세지 보낸다는 신호를 보내면
	socket.on('send message', (name, text) => {
		massage = name + ' : ' + text;
		console.log(socket.id + '(' + name + ') : ' + text);
		// (전체)사용자에게 메세지 전달
		io.emit('receive message', massage);
	});
});

이제 사용자가 접근하면 사용자에게 socket이 할당된다. 콘솔에서는 접근한 socket의 고유 아이디를 출력하고 사용자에게 임시 닉네임을 랜덤값을 돌려 할당한다. 그리고 사용자에게 change name 이라는 신호를 보내준다. 랜덤으로 돌려 할당된 이름을 적용시키기 위함이다.

그리고 아래에 나열된 각각의 코드는 사용자로부터 신호를 기다린다. 연결이 끊어진 것을 기다리는 것, 이름이 변경되는 것을 기다리는 것, 메세지를 받는 것을 기다린다. 안에 실행되는 코드들도 위에서 설명한 내용과 유사하므로 넘어간다.


클라이언트 프로그래밍

1
2
3
4
<input id="name" class="name" type="text" readonly>
<button onclick="rename_submit();">닉네임 변경</button>
<input id="message" onkeydown="JavaScript:Enter_Check();" class="message" type="text" autocomplete="off">
<button onclick="chat_submit();">전송</button>

클라이언트에게는 이름을 보여주는 인풋박스와 닉네임을 변경하는 버튼과 메세지를 입력할 인풋박스 메세지를 전송한 버튼이 제공된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script src="/socket.io/socket.io.js"></script>
<script src="//code.jquery.com/jquery-1.11.1.js"></script>
<script>
var socket = io();
socket.on('receive message', function(msg) {
	var P_Tag = document.createElement('p');
	var Text = document.createTextNode(msg);
	P_Tag.appendChild(Text);
	document.getElementById('chatLog').appendChild(P_Tag);
	var objDiv = document.getElementById("chatLog");
	objDiv.scrollTop = objDiv.scrollHeight;
});
socket.on('change name', function(name) {
	$('#name').val(name);
});
function chat_submit() {
	socket.emit('send message', $('#name').val(), $('#message').val());
	$('#message').val("");
	$("#message").focus();
	e.preventDefault();
}
function rename_submit() {
	socket.emit('rename', $('#name').val(), $('#message').val());
	$('#message').val("");
	$("#message").focus();
	e.preventDefault();
}
function Enter_Check(){
	if(event.keyCode == 13){
		chat_submit();
		document.getElementById('message').value = "";
	}
}
</script>

1행에 스크립트 파일을 불러오고 있는데 별다른 설정을 하지 않고 위와같이 스크립트 파일을 호출하면 정상적으로 동작이 가능했다. 또한 제이쿼리를 사용하였다. 클라이언트의 코드를 살펴보면 서버에서 emit으로 구현된 부분은 클라이언트에서 on으로 받아주고 서버에서 on으로 구현된 부분은 클라이언트에서 emit으로 되어있다. 위와같이 서로 상호작용 한다는 개념으로 보면 이해가 쉽지 않을까 생각한다. 만일 위 코드에서 이해가 안가는 부분이 많은 것 같다면 제이쿼리 학습을 선행하는 것을 추천한다.

지금의 결과물은 위와같지만 위 화면은 누가봐도 카카오톡의 느낌은 들지 않는다 다음글에서 데이터베이스 연동과 디자인 개선이 이루어진다.

WRITTEN BY

배진오

소비적인 일보단 생산적인 일을 추구하며, 좋아하는 일을 잘하고 싶어합니다 :D
im@baejino.com