Node/Express

Express에 소캣 연결하기 & 이벤트 분리

biglol 2022. 11. 27. 20:02

Express에 소캣 연결 및 이벤트 분리


 

http는 비연결성이라는 특징을 갖고 있습니다.

비연결성이란 client와 server가 한 번 연결을 맺고 요청-응답에 대한 통신을 마치면 연결 자체를 끊어버리는 것을 뜻합니다.

따라서 클라이언트에서 보내는 모든 요청에 대해 새로운 connection을 만들어야 합니다.

 

다만 채팅이나 알림같은 실시간 통신의 경우 client와 server간의 연결 자체가 계속 살아있어야 합니다.

이를 구현하기 위한 방법 중 하나는 Socket을 이용하는 것이며 이를 express에서 어떻게 구현하고 어떻게 이벤트를 분리시킬 수 있는지 살펴보겠습니다.

 

패키지: socket-io

 

우선 app.js부터 작업해야 합니다

  • app.js 파일 소스
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';

const app = express();
const server = http.createServer(app);

const io = (server: http.Server) => {
	return new Server(server, {
		cors: {
			origin: '*',
		},
	});
};

io.on('connection', (socket) => {
	console.log(`a new user connected with socketId of ${socket.id}`);
    
	socket.on('connectUser', async ({ userId }) => {
		await addUser(userId, socket.id);
	});

	socket.on('disconnect', async (obj) => {
		await removeUser(socket.id);
	});
});

CORS는 *(전체)로 설정한 이유는 어차피 express cors에서 허용 도메인들 설정해주면 특정 도메인들만 통신이 가능하기에 소켓에도 굳이 할 필요는 없어서 그렇습니다.

 

socket.on('이벤트명')은 클라이언트에서 특정 이벤트를 날렸을 때 받는 역할이며 socket.emit('이벤트명')은 특정 이벤트를 날리는 역할입니다. 즉, 위 소스에선 서버는 'connectUser'랑 'disconnect' 소캣이벤트를 받을 준비가 되어있다는 겁니다.

 

위 소스에선 클라이언트가 로그인하고 로그인했다는 connectUser라는 이벤트를 userId와 함께 날리면 서버에서 그걸 받아 유저아이디와 소캣아이디를 매칭시킵니다. 위 예제에선 단순 배열에 유저아이디와 소캣id를 매칭하는 json 객체를 넣었지만 보통은 인메모리 db인 Redis를 이용해 이런 값들을 저장합니다.

 

  • Client 코드 예제
import io from 'socket.io-client';

const socket = useRef();

useEffect(() => {
    if (!socket.current) {
      socket.current = io('백엔드 url'); // 서버에 연결
    }
    if(socket.current){
    	socket.current.connect(); // 소캣 연결
	socket.current.emit('connectUser', { userId }); // connectUser 이벤트 전송
    }
},[])

클라이언트에선 페이지 진입 시 소캣이 있는지 확인하고 없으면 io('백엔드 url')로 연결, 소캣이 있으면 connectUser이벤트를 날리면서 userId라는 값을 보냅니다 ( {userId: userId} )


* 만약에 서버에서 핸들링하는 이벤트가 수십개가 될 경우 어떻게 해야할까요?

app.js에서 socket.on, socket.emit 소스가 엄청 많아지면서 각 이벤트를 어떻게 핸들링할지 알려주는 로직도 들어가면 app.js는 몇 백줄 몇 천줄 될 수 있습니다. 이러면 가독성도 안 좋아져 이벤트를 찾기 힘들 것입니다.

 

이를 해결하기 위해 이벤트를 다른 파일로 분리할 필요가 있습니다.

먼저 소캣 이벤트를 처리할 파일을 하나 만듭니다.

 

  • Socket 이벤트를 따로 처리할 파일 소스 (util폴더에 socketDefinition.js생성, 파일명은 자유롭게 해도 됩니다)
import { Socket } from 'socket.io';

module.exports = function (socket: Socket) {
	socket.on('textChangeNotification', ({ sendingUser }) => {
		socket.broadcast.emit('textChangeNotification', sendingUser);
	});
};

예시를 위해 작성한 것이며 textChangeNotification이벤트를 받으면 클라이언트에 textChangeNotification이벤트를 날리는 예제입니다. Slack처럼 누군가 채팅내용을 작성하고 있을 시 다른 사용자들에게 '누구누구님이 채팅을 입력중입니다...'를 표시하기 위한 예제입니다.

이후 app.js에서 이걸 불러오면 됩니다.

 

  • 변경된 app.js
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';

const app = express();
const server = http.createServer(app);

const io = (server: http.Server) => {
	return new Server(server, {
		cors: {
			origin: '*',
		},
	});
};

io.on('connection', (socket) => {
	console.log(`a new user connected with socketId of ${socket.id}`);
    
	socket.on('connectUser', async ({ userId }) => {
		await addUser(userId, socket.id);
	});

	socket.on('disconnect', async (obj) => {
		await removeUser(socket.id);
	});
    
    	require('./util/socketDefinition')(socket);
});

여기에선 단 하나의 파일만 만들고 해당 파일에 단 하나의 이벤트만 선언했지만 목적에 따라 파일을 여러개 만들고 여러 이벤트를 선언하면 됩니다.