Node.js+Socket.IO 실시간 채팅방 [1] 클라이언트-서버 연결
중요한 개념인 네트워크 통신 쪽을 잘 몰라서 시작하게 됐다.
웹 개발 지망생은 아니지만 웹이 인터넷 통신에 있어 가장 기본이 되는 개념이라 생각해 웹을 사용하여 정리하고 있다.
순서대로, 비슷한 것끼리 묶어서 정리하면 좋겠지만 개인적으로 네트워크 공부는 머리에 그려지는 것 자체가 어렵기 때문에
그때그때 필요한 개념을 찾아서 적기로 했다.
예시를 통한 공부가 제일 효과적이라는 말이 네트워크에 특히 해당되는 말이라 생각한다.
생활 코딩, NAVER d2를 참고하여 이론을 정리하고,
Socket.IO 공식 홈페이지를 참고하여 실습을 진행할 예정이다.
1-1. Node.js란
1억 개의 웹 페이지가 있고 이들 간에 공통된 수정 사항이 있을 경우, 우리는 1억 개의 웹 페이지를 하나씩 들어가 오류를 고쳐야 한다.
이런 최악의 상황을 피하기 위해 등장한 것이 Node.js이다.
JavaScript가 사용자와 웹 브라우저 간 동적 상호작용을 위해 나왔다면,
Node.js는 이 JavaScript를 이용해 프로그램(함수)을 통해 웹 페이지를 효율적으로 관리하기 위해 나온 것이다.
경쟁 관계로는 PHP, JSP, Django 등이 있다.
특히 리얼타임 웹서비스(ex: 넷플릭스), API 사용이 많이 포함된(=커스터마이징이 필요한) 서비스를 만들고자 할 때 효율적이다.
1-2. Socket.IO란
Web Socket 기술을 쉽게 사용할 수 있게 하는 모듈이다.
1-3. Web Socket이란
웹 페이지의 한계에서 벗어나 실시간으로 상호작용하는 웹 서비스를 만드는 표준 기술이다.
기존 웹 브라우저 렌더링 방식은 HTTP Request에 대한 HTTP Response를 받아서 (무전기처럼 단방향 통신)
브라우저의 화면을 깨끗하게 지우고 받은 내용을 새로 표시하는 방식이다. 이때 브라우저의 깜빡임이 생기게 된다.
이러한 깜빡임 없이 원하는 부분만 다시 그리며, 실시간으로 사용자와 상호작용하는 방식이 나타난다. (양방향 통신)
2. Node.js + Socket.IO를 이용한 실습
검색하면 정말 많은 예시가 나와있지만 사실 시도는 여러 번 해봤었다.
하지만 나는 노베이스였던 탓에
npm으로 다 깔았는데 저 코드를 어떻게, 어디에 쓰지? js파일을 어떻게 실행하지? 이런 난관들에 금방 포기했었다.
하지만 맘을 편히 먹고 천천히 따라해보니 성공했다.
따라한 예제는 https://socket.io/get-started/chat/ Socket.IO 공식 홈페이지에 있는 챗 예제이다.
Node.js를 먼저 설치한 후, npm 명령어로 Socket.IO, Express(Node.js를 사용하기 위한 프레임워크)를 설치하면 개발 환경은 끝이다.
나는 Atom 에디터를 사용했다.
2-1. 전체 코드
< index.js >
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
|
var app = require('express')(); //express 모듈 사용
var http = require('http').createServer(app); //http라는 이름의 express 모듈 기반 http web server 객체 생성
var io = require('socket.io')(http); //http web server에 socket.io 모듈 사용 (웹 서버에 소켓이 부착되는 느낌으로)
http.listen(3000, () => { //3000번 포트에서 대기 중인 http 웹 서버 생성
console.log('listening on *:3000');
});
app.get('/', (req, res) => {
//객체 app(web server)가 request(get method)를 받았을 경우
//3000번 포트에 "누군가 들어온 경우 (=웹페이지에 누가 접속함)"
res.sendFile(__dirname + '/index.html'); //index.html을 response(웹 브라우저가 이를 받아서 화면에 렌더링)
});
io.on('connection', (socket) => { //소켓이 붙어있는 http web server에 connection 발생
console.log('a user connected');
socket.on('chat message', (msg) => {
//client가 'chat message'라는 이름의 이벤트를 보낸 경우(발생시킨 경우)
//msg(해당 이벤트의 결과물)라는 데이터를 받아온다
io.emit('chat message', msg); //client에게 'chat message'라는 이름의 이벤트를 보낸다
});
socket.on('disconnect', () => {
console.log('user disconnected');
});
});
|
cs |
< index.html >
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
35
36
37
38
39
40
|
<!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: 0.5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script>
$(function () {
var socket = io();
$('form').submit(function(e) { //<form>의 <input>(채팅방의 send 버튼)이 클릭될 경우
e.preventDefault(); // prevents page reloading
socket.emit('chat message', $('#m').val());
//web server에게 'chat message'라는 이름의 이벤트를 보냄(발생 시킴)
//아이디가 m인 태그가 가진 값(사용자가 채팅 입력창에 입력한 내용)을 함께 전송
$('#m').val(''); //아이디가 m인 태그 값(사용자의 채팅 입력창) 초기화
return false;
});
socket.on('chat message', function(msg) { //web server로부터 'chat message'라는 이름의 이벤트가 온 경우
$('#messages').append($('<li>').text(msg)); //아이디가 messages인 태그(채팅 출력창)에 <li>msg를 할당
});
});
</script>
</body>
</html>
|
cs |
<package.json>
1
2
3
4
5
6
7
8
|
{
"name": "socket-chat-example",
"version": "0.0.1",
"description": "my first socket.io app",
"dependencies": {
"socket.io": "latest"
}
}
|
cs |
2-2. Web Server 준비 과정
통신을 위해서는 web client와 web server가 필요하고,
index.js는 이 중 http web server를 연결하고 request에 따른 처리를 담당한다.
2-2-1. Client와 Server, http
2개의 컴퓨터(A, B)
A는 웹브라우저(ex: 크롬, 파이어폭스, 인터넷 익스플로어)를 사용 중인 컴퓨터,
B는 naver-blog.html이라는 html파일을 하드 디스크에 갖고 있는 컴퓨터.
* 웹 브라우저는 html 파일을 렌더링하여 사용자(A)의 컴퓨터 화면에 뿌려주는 역할을 한다.
A가 웹 브라우저에서 네이버 서비스를 이용 중이고, (naver.html이라는 이름의 파일을 보고 있다고 가정)
B는 naver.html, naver-blog.html을 인터넷에 올려둔(공유해둔) 상태이다.
* 인터넷은 여러 사용자가 서로 다른 웹을 공유할 수 있도록 판을 깔아주는 개념이라 생각하면 편하다.
이때 A가 네이버 블로그를 보기 위해 블로그 아이콘을 클릭하는 이벤트가 발생했다면 그림과 같은 일들이 일어난다.
이때 A를 Client, B를 Server(=web server) 라 부른다.
현재 실습에서는 컴퓨터 2대가 아닌 컴퓨터 1대에서 web client와 web server 역할을 동시에 수행하고 있는 상황이다.
index.js 파일은 이 중 web server와 관련된 파일이다.
http는 웹 브라우저와 웹 서버 간의 통신 규약으로, 나는 단순히 모든 web server 객체는 http 기반으로 만들어지는 것으로 이해했다.
위의 코드에서는 http라는 이름의 객체가 사람일 때 express 객체를 발판으로, io 객체를 옆에 끼고 있는 자식으로 생각했다.
코드는 순서가 상관 없지만 이해를 위해 순서대로 나열했다.
2-2-2. http Request의 종류
GET, POST, HEAD, PUT 등 종류는 많지만
우선 개념만 이해하기 위해서 가장 대표적인 GET, POST에 대해서만 찾아봤다.
둘의 차이점을 통해 개념을 이해하는 게 훨씬 편했다.
Client가 Server로 Request 할 때, Request는 Client→Server 방향으로 HTTP 패킷을 보내는 것과 같다.
* 패킷은 인터넷에서 데이터를 전송할 때 쓰이는 단위로 이해하면 좋다.
HTTP 패킷은 여러 부분으로 구성돼있는데, 여기서는 헤더와 바디만 고려한다.
HTTP 패킷을 게시글이라 생각하면 헤더는 게시글의 제목 부분, 바디는 게시글의 본문 부분이라 보면 된다.
참고로 위 코드는 웹 페이지 접속 request 이므로 별다른 데이터가 필요하지 않다. 그래서 두 method의 차이가 크게 없다.
GET method
GET은 패킷의 헤더 부분에 보내고자 하는 내용을 모두 포함시켜 request 신호를 보낸다.
(예: /test/demo_form.php?name1=value1&name2=value2)
보통 게시글을 등록하면 게시글 리스트에 제목 별로 나열된다. 즉, 클릭하지 않아도 게시글 제목들은 모두 알 수 있다.
따라서 GET은 보안 상 취약하다.
하지만 URL에 보내는 내용이 표기되므로 URL을 보관하는 캐시 메모리에 캐싱이 가능하다.
캐싱이 된다는 것은, 1번 이상 보내진 Request는 캐시 메모리에서 빠르게 찾을 수 있어 검색 속도 면에서 효율적이다.
또한 게시글 제목 칸은 본문 칸에 비해 크기가 매우 작다.
GET 또한 보낼 수 있는 데이터의 종류와 크기에 제한이 있다.
POST method
GET과 달리 패킷의 바디 부분에 보내고자 하는 내용을 포함시켜 request 신호를 보낸다.
따라서 GET에 비해 URL에 보내는 내용이 표기되지 않고, 보내고자 하는 데이터에 대한 제한이 없다.
단, 헤더에 크게 표기되는 내용이 없으므로 GET과 달리 캐싱이 불가능하다.
Socket.IO 공식 홈페이지에 이런 세부 기능들을 구현해보라고 나름 과제를 줬다. 시간이 될 때마다 하나씩 추가할 예정이다.
- Broadcast a message to connected users when someone connects or disconnects. (들어온 사람, 나가는 사람 알림)
- Add support for nicknames. (닉네임 주기)
- Don’t send the same message to the user that sent it himself. Instead, append the message directly as soon as he presses enter. (사용자는 본인이 직전에 보냈던 메시지를 중복해서 보낼 수 없음)
- Add “{user} is typing” functionality. (입력 중인 사람 알리기)
- Show who’s online. (현재 접속 중인 사람 목록 보여주기)
- Add private messaging. (비밀 메시지 기능)
잘못된 개념이나 추가하고 싶은 부분에 대한 댓글들 모두 환영합니다!!