-
Notifications
You must be signed in to change notification settings - Fork 6
socket.io
socket.io 도입 계기
저희 프로젝트 black은 실시간 플랫폼입니다.
그렇기에 클라이언트와 서버간 실시간으로 연동될 수 있는 기능이 필요했고 이를 위해 도입한 것이 socket.io였습니다.
socket.io를 활용하면서 많은 고민을 했던 부분은 크게 3가지로 흐름, 구조화, 권한 인증 및 연결 방식이었습니다.
connection event
black 웹사이트에 접속하면 해당 접속자는 자신이 속한 채팅방의 새로운 메시지를 실시간으로 받을 수 있는 방법이 필요했습니다.
이를 위해 최초로 socket이 connection 이벤트를 받으면 해당 socket을 해당 사용자가 참여하고 있는 모든 채팅방의 room에 join할 수 있도록 설정했습니다.
또한 특정 개인에게 메시지를 보내는 DM 또한 black에서는 분류상 채팅방이라 room에 join을 합니다. 이 때 다른 유저에 의해 DM이 만들어진 사용자는 해당 DM의 생성 여부를 파악해서 해당 room에 join할 수 있는 방법이 필요했습니다. 이를 위해 socket 연결시 사용자의 user id와 socket id를 DB에 저장하도록 하였으며 같은 사용자여도 다른 탭인 경우 socket id가 다르기 때문에 하나의 user는 여러 socket id를 가질 수 있도록 DB에 저장했습니다.
그리고 관리를 위해 해당 socket의 연결이 종료되면 DB에 저장된 userid와 socket id가 삭제되도록 설정했습니다.
message event
메시지를 새롭게 생성하는 기능을 위한 event입니다.
메시지 생성을 위한 event를 서버측에서 받으면 서버는 해당 메시지를 생성하고 새롭게 생성된 메시지를 다른 사용자들에게도 모두 보내주도록 설정했습니다.
이 때 서버측에서 emit을 할 때 해당 채팅방에만 emit할 수 있도록 설정했습니다.
나머지 event도 이와 비슷한 흐름으로 작성되어 있습니다. 이와 관련해 black의 socket.io에 관심이 있으시면 socket.io 명세서를 참조해주시면 됩니다.
전체적인 패턴
처음 socket.io를 사용하기로 결정했을 때 가장 고민했던 부분은 구조였습니다. socket.io와 관련한 코드를 하나의 파일에 전부 넣자니 가독성 면에서 좋지 않다고 판단을 했습니다. 또한 event와 관련해서 파일을 나눈다 하더라도 event 대기 코드 → 처리 코드 → 비즈니스 로직 코드가 모두 한 곳에 포함되는 것은 유지보수 측면에서도 좋지 않으리라 생각했습니다.
그렇기에 코드를 나눌 필요가 있다고 생각해서 많은 검색을 해보았으나 대체로 많은 코드들은 하나의 파일에 적는 형태를 가지고 있었습니다.
이와 관련해서 스스로 구조화를 해야겟다는 판단을 했고 평소 사용하는 mvc구조에서 힌트를 얻어서 활용했습니다.
event에서 해당 이벤트만을 on으로 등록하며 handler에서 이벤트에 필요한 서비스 호출 및 응답, 그리고 service에서 비즈니스 로직만을 처리할 수 있도록 구조화를 하였습니다.
권한 인증
socket.io를 사용하면서 권한을 인증해서 권한이 있는 사용자인지, 그리고 해당 권한을 확인해서 사용자의 정보를 알아야할 필요가 있었습니다.
black은 기존 express에서 JWT인증 방식을 이용하고 있습니다.
API를 통한 login을 하면서 사용자가 받은 JWT를 이용하여 socket.io를 통한 통신을 할 수 있도록 하는 것이 목표였습니다.
이를 이루기 socket.io에 middleware를 장착하고 해당 middleware에서 권한 인증 및 사용자 정보를 획득할 수 있도록 처리했습니다.
//client
const socket = io(socketURL, {
transportOptions: {
polling: {
extraHeaders: {
Authorization: 'token'
}
}
}
});
그리고 이 과정에서 한 번의 시행착오를 겪었습니다.
최초에는 사용자가 로그인을 통해 획득한 JWT를 socket.io의 extra header를 통해 보내는 것으로 구현했습니다. 실제로 잘 동작했고 문제가 없는 줄 알았습니다.
연결 방식
그러나 기술 세미나 때 사용자 데이터를 모으기 위해 black을 홍보하는 중 문제가 발생했습니다.
*polling과 websocket이 함께 사용되고 있었습니다.*
이 질문을 받는 순간 바로 느낀 것은 제대로 socket.io을 이해하면서 사용하고 있지 않는다였습니다. transport 방식을 생각하지 않고 default로 동작하게 했기 때문에 명확한 답변을 할 수 없는 상황이었습니다.
그러기 위해 연결 방식에 대한 공부를 다시 시작했습니다.
- polling: 저희가 사용하던 방식입니다. 클라이언트가 평범한 http request를 서버로 계속 날려서 이벤트 내용을 전달받는 방식입니다. 계속적으로 request를 날리기 때문에 서버에 부담이 큰 편입니다.
- long polling: 클라이언트에서 일단 서버로 http request를 보냅니다. 이상태로 계속 기다리다가 서버에서 해당 클라이언트로 전달할 이벤트가 있다면 그순간 response 메시지를 전달하면서 연결이 종료됩니다.
- streaming: 클라이언트와 서버 사이의 연결을 끊지 않고 필요한 메세지를 계속 전달합니다. 그렇기 때문에 커넥션을 맺는 과정에서 발생하는 부담이 줄어듭니다.
- websocket
웹 소켓은 클라이언트와 서버간의 전이중 통신을 지원하기 위한 통신 프로토콜입니다.
연결 방식은 아래와 같습니다.
1. 클라이언트와 서버간에 전이중 통신을 수행하려면 클라이언트가 서버로 HTTP UPGRADE 요청을 보내야 합니다. 이를 웹 소켓 프로토콜 핸드 쉐이크라고 합니다.
2. 서버가 커넥션을 UPGRADE 할 수 있는 경우, HTTP 101 응답을 클라이언트에게 보냅니다. 서버는 핸드 쉐이크가 성공적으로 수행되었다고 판단하고, 서버와 클라이언트 사이의 커넥션을 웹 소켓 프로토콜로 UPGRADE 합니다. 클라이언트와 서버 사이의 HTTP 101 응답이 전달되는 순간, 서버와 클라이언트 사이의 커넥션은 HTTP 프로토콜이라고 하지 않습니다. 그리오 이순간 양방향 통신이 가능해집니다.
3. 웹 소켓으로 연결된 모든 클라이언트는 다른 클라이언트에게 커넥션을 끊는 요청을 전송할 수 있습니다.
즉 위에 설명한 다른 단방향통신을 어떻게든 이용해서 실시간 통신을 구현한 방식에서 벗어나 연결을 유지하면서 양방향 통신을 지원할 수 있는 프로토콜입니다.
이러한 연결방식에서 websocket을 사용해야하는 것은 속도와 부하면에서 당연하게 보였고, 기존에 사용하던 polling 방식에서 websocket으로 연결을 할 수 있도록 전환을 시도했습니다.
// client
const socket = io(socketURL, {
transports:['websocket','polling']
transportOptions: {
polling: {
extraHeaders: {
Authorization: 'token'
}
},
websocket: {
extraHeaders: {
Authorization: 'token'
}
}
}
});
그러나 이 코드에서 socket.io 는 생각대로 동작되지 않았습니다.
기존의 extraheader로 Authorization을 붙여줬었는데 이는 매번 request가 보내지는 polling 방식에서만 지원되는 방식이었습니다.
그렇기에 인증 처리를 위한 로직을 변경할 필요가 있었습니다.
다시 권한 인증
websocket에서 권한 인증을 할 수 있도록 새롭게 도입한 방식은 query string이였습니다. query string으로 토큰을 전달받으면 middleware에서 websocket handshake때 받은 query를 이용해 유효 검사를 할 수 있도록 구현했습니다.
//client
const socket = io(socketURL, {
query: { token: window.localStorage.getItem('token') },
transports: ['websocket', 'polling']
});
//server socket middleware
const jwtMiddleware = (io) => {
const wrap = (middleware) => (socket, next) => {
const { request } = socket;
const { token } = socket.handshake.query;
request.headers.authorization = token;
middleware(request, {}, next);
};
io.use(wrap(passport.authenticate('jwt', { session: false })));
};
위와 같이 코드를 변경함으로써 성공적으로 websocket을 이용한 실시간 통신을 할 수 있도록 변경했습니다.
서버에 부하를 줄 수 있는 polling 방식이 network tab에서 사라지고 websocket만으로 통신할 수 있게 되었습니다.
느낀 점
해당 기업 프로젝트를 선택하게 된 이유 중 하나는 socket.io이었습니다.
(나머지 하나는 도커 쿠버네티스입니다.)
백엔드, 네트워크에 평소 관심이 많았으며 실시간으로 데이터를 주고받는다는 것이 활용해본 경험이 없지만 막연히 이거라면 분명 재밌겠다!라는 확신이 들어서 주저없이 이 프로젝트를 선택해서 1순위로 지망했던 기억이 있습니다.
그리고 백엔드 개발에서 socket.io를 전담하게 되면서 다행히도 기대와 같이 재밌는 작업이었습니다. 제가 작성한 코드로 인해 실시간으로 동작하는 것에서 재미를 느낄 수 있었습니다.
그러나 언제나 작업을 하면서 반성과 피드백을 하게 되는 과정이 있습니다.
처음 socket.io를 활용하면서 막연히 공식 문서를 보면서 따라하니깐 되네?라는 과정으로 시작을 했습니다. 실제로도 socket.io를 활용한 개발은 그리 어렵지는 않았습니다.
그러나 이후 진행을 하면서 동작은 하지만 부적절한 연결 방식에서 공부와 이해가 필요했고, handshake, 브라우저가 websocket이 지원되면 101 응답코드를 보내서 websocket으로 업그레이드를 하는 등에 대한 공부를 해야했고 이 과정을 거치면서 생각의 흐름을 가졌습니다.
잘 지원해주는 라이브러리라도 역시 알고 써야 하는구나 → 공식 문서를 정독할 필요가 있네 → 이 문서를 정독하면서 이해를 하려면 기본적인 cs지식이 바탕이 되어야겠구나
이러한 정말 개발자라면 당연한 이야기지만 받아들이고만 있던 흐름을 직접 깨달을 수 있었던 소중한 경험이었습니다.
기본적인 지식을 공부하고 이해할 수 있는 과정이 필요했음에도 그 과정은 재밌었습니다. 평소 클라우드에 흥미를 가져서 네트워크를 좋아해서인지 이해도 나름 잘 됐고 새롭게 알아가는 과정이 좋았고 원리와 동작을 이해하면서 발전할 수 있었던 과정 또한 너무 좋았습니다.