[포트폴리안] 리액트와 socket.io를 이용한 채팅 기능 구현 과정 (진행중)

한지원·2022년 3월 28일
1

사용자 간 실시간 채팅 기능을 구현해서
프로젝트에 참여하고 싶은 사용자가 팀장에게 신청 및 질문을 할 수 있는 기능을 구현하고 있다.
socket 통신을 이용하여 클라이언트 단에서 채팅 기능을 어떻게 구현해 가는지 작성하고자 한다.

아직 기능 구현이 완료되지 않은 상태이며,
모바일, 백엔드 팀원과 api 명세 및 socket 이벤트를 만들어가는 과정 및 문제에 대해 고민하고 해결하는 과정에 대해 작성할 것이다.
따라서 기능 구현하는 방법을 얻고자 하는 분들께는 도움이 안될 포스팅이기도 하다.


1. socket 연결하기

백엔드 팀원과 소켓 서버를 열어 클라이언트와 연결이 되는지 테스트를 진행해보기로 했다.
socket.io-client 을 설치하고
import socketIoClient from 'socket.io-client로 모듈을 호출해
테스트 하고자 하는 컴포넌트에서 연결을 시도했다.
여러 예시를 참고했었는데 연결을 하는 방법이 하나로 정해져 있던것 같지가 않았다.

클라이언트 단에서 연결할 때

  1. const socket = socketIoClient.connect('서버 주소', ...) 이 방식과
  2. const socket = socketIoClient('서버 주소', ...) 이 방식중 한가지를 쓰는 것 같았고,

    (두 번째 인자인 ...은 옵션을 넣는 자리이다. connect 옵션에 관련된 socket.io공식문서 )
    우리 프로젝트에서는 폴링 방식 없이 웹소켓만 구현할 것이기 때문에 { transports: ["websocket"] } 이 옵션을 넣어주었다.


이곳의 설명을 보니
기본적으로는 socketIoClient('서버 주소') 를 사용하면 autoConnect가 적용된다. (autoConnect의 기본 값은 true이기 때문)

socketIoClient의 인자로 autoConnect: false옵션을 넣어준 후 수동으로 connection을 시도할 때
const socket = socketIoClient({autoConnect: false}) 해준 후 connect가 필요할 때
socket.connect('서버 주소')를 해주는 것이었다.

리액트 프로젝트에서는 유저가 웹에 접속 시 바로 connect를 해줄 것이기 때문에 autoConnect 옵션을 따로 설정해주지 않고
첫번째 방법을 선택했다.

서버 단에서 connect를 시도하는 이벤트를 받을 때

  1. server.on('connect', () => {...})
  2. server.on('connection', () => {...}) 중 어떤걸 선택해야할지 몰랐었다.

테스트를 해봤는데 connectconnection 모두 정상 작동을 했다.
socket.io의 DOCS를 살펴보니
connectconnection는 같은 기능을 한다.


2. 상황별 이벤트와 통신 방식 정의하기

REST API 명세서는 사실 백엔드 팀원이 담당한 일이고, 우리 백엔드 팀원은 REST API명세를 진짜 엄청나게 잘짠다.
하지만 채팅 방 생성부터 상대방에게 알림을 주고 채팅을 주고받기까지의 모든 로직을 구성함에 있어
어떤 상황에는 socket으로 이벤트를 주고받고, 어떤 상황에는 REST API로 HTTP통신을 해야하는지
팀원들이 함께 고민하고 결정해야할 것 같았다.

유저가 앱에 접속했을 때

접속과 동시에 socket에 connect를 하고
socket.emit('authorization', {유저 정보})를 통해 서버로 유저 정보를 보내 현재 접속중인 유저임을 알린다.

채팅방 생성시

유저 A가 유저 B에게 채팅을 걸 때 혹은 채팅방에 입장할 때
클라이언트는 서버로 A가 B와의 채팅방에 입장한다는 것을 HTTP통신으로 서버에게 알린다.
서버는 응답으로 roomId를 보내준다.

만약 이미 생성된 room에 들어가는거라면 해당 roomId를,
새로 생성하는거라면 룸 관련 정보를 DB에 생성 후 새로 생성한 roomId를 보내준다.

채팅방 입장시

HTTP통신으로 채팅 내역을 가져온다.
이후 socket.emit('room:enter', {룸 정보}) 를 통해 유저 A가 채팅방에 입장했음을 소켓 서버에 이벤트로 알린다.
만약 읽지 않은 메시지가 있다면 서버에서 해당 채팅방에서 읽지 않은 메시지를 관리하다가
해당 룸에 들어간 이벤트를 인지한 서버에서 룸의 new message에 대한 A의 읽기 여부를 update 시켜준다.
(new message list를 old message list로 바꿔주는 고런 작업)

채팅방 퇴장시 (채팅방 삭제 아님)

채팅창을 잠깐 닫을 때 socket.emit('room:leave', {룸 정보}) 이벤트를 소켓 서버로 보내
서버는 이 이후 유저에게 전송된 메시지를 읽지 않은 메시지로 관리한다.

메시지 보내기

보낸 메시지는 서버 통신과 별개로 말풍선 컴포넌트에 담아 대화 리스트에 업데이트 시킬 예정이다.
상대방에게 전송하기 위해서는
socket.emit('chat:send', {메시지 정보}) 이벤트를 서버로 보내고
서버는 해당 메시지를 DB에 저장 후 상대방에게 새로운 메시지가 왔음을 알린다.

메시지 받기

위에서 메시지 보내기 이벤트를 받은 서버는
메시지를 받아야 하는 상대방의 접속 여부에 따라 이벤트를 보내줄지 보내주지 않을지 결정한다.
접속해 있는 유저라면 서버는 즉시 chat:receive 이벤트를 발생시키고
클라이언트는 socket.on('chat:receive') 를 통해 이벤트를 받아 새로운 메시지가 왔음을 인지한다.
여기서 고민해야할 점이

메시지를 받는 유저가 접속해 있을 때

  1. 메시지를 받는 유저가 채팅방에 들어와 있다면
    socket.on('chat:receive') 이벤트 내에 실린 데이터를 파싱해서 채팅룸에 바로 띄워준다.

  2. (거의 대부분의 경우일 듯) 메시지를 받는 유저가 채팅방에 들어와 있지 않다면
    클라이언트 단에서 메시지가 왔을 때 실시간으로 알림을 주는 것이 아니라면
    채팅 리스트를 보는 아이콘에 새로운 채팅이 왔음을 알리는 표시 정도를 해주는게 전부일 것이다.
    이럴 땐 socket.on('chat:receive') 로 이벤트 감지만 한 뒤
    읽지 않은 메시지를 띄워주는 컴포넌트를 리렌더링 해주는 작업을 해야할 것 같다.

메시지를 받는 유저가 접속해 있지 않을 때
서버는 접속해 있지 않은 유저에게 전송된 메시지를 읽지 않았다는 처리 후 DB에 저장하고
해당 유저가 접속했을 때 클라이언트 단에서 채팅 관련 API 요청을 보냈을 때
읽지 않은 메시지에 대한 정보를 응답으로 보내줘야 할 것이다.
응답으로 받은 정보를 화면에 출력하는건 통신의 영역이 아닌 프론트엔드 구현의 영역인 것 같다.

채팅방 목록 가져오기

HTTP통신 으로 채팅방 리스트를 요청하면
서버에서 채팅방에 대한 정보(유저, 마지막 대화, 읽지 않은 메시지의 개수, 룸 아이디..)를 담은 응답을 내려주기로 했다.

채팅방 생성 및 삭제

채팅방에 단순히 입장하고 잠시 나가는 것과 달리 처음 입장해서 생성하는 것과 영영 나가버리는 것을 다르게 구현해야한다.
단순히 입장하고 채팅방을 잠깐 닫는 이벤트는 room:enter, room:leave
채팅방에 처음 입장하고 삭제하는 이벤트는 notice:enter, notice:leave 로 정의해서
notice:enter과 notice:leave 이벤트를 받은 클라이언트는
유저에게 'ㅇㅇㅇ님이 입장하셨습니다.'나 'ㅇㅇㅇ님이 퇴장하셨습니다.'와 같은 메시지를 띄워주기로 했다.

사실 이 부분은 아직 머릿속에 완전히 그려지지 않는다. 팀원들과 상의를 할 때마다 생각이 달라지고 정해져도 자꾸 햇갈린다.

채팅방 열고닫기, 채팅방 생성삭제 이 부분의 통신 대한 명확한 정의를 다시 해야겠다.


(2022.03.30)
채팅방 생성 및 삭제에 대해 백엔드 팀원과 이야기 한 내용
(A와 B가 채팅을 한다고 가정, A가 B에게 처음 채팅을 요청한 상황)
1. A가 채팅방을 생성할 시에 클라이언트는 서버에 HTTP통신으로 채팅방을 열었다는 것을 보내고 roomId를 응답으로 받음
(서버는 만약 A와 B의 채팅방이 원래 존재했다면 기존의 roomId를 응답하고)
2. 처음 생성하는 거라면 서버는 새로운 roomId를 생성하고 room에 대한 정보를 DB에 저장
3. socket 서버에서 A와 B의 클라이언트단으로 notice:enter 이벤트를 보냄 (?) <- 이 이벤트를 받아서 클라이언트 단에서 어떤 처리를 해줘야하는지는 고민해봐야겠음.. (새로운 채팅이 생성됐으니까 채팅 관련 컴포넌트를 업데이트 해줘야할까?)
4. A와 B가 HTTP통신으로 채팅방 리스트를 요청할 때 서버의 응답 데이터에 type:chat이나 type:notice와 같은 값을 넣어주면 클라이언트는 리스트에서 마지막 채팅 내역 혹은 방 생성 문구 중 어떤걸 띄워줄지 결정할 수 있음


위의 설명은 해결방법이 아니라 어제까지 팀원들과 나눈 내용을 정리한 것이다.
따라서 구현하면서 얼마든지 바뀔 수 있고 더 자세히 생각해야할 수도 있다.

백엔드 팀원에게 너무 일이 많이 쏠릴까봐 가능하면 프론트 단에서 해결할 수 있는 부분은 해결하고 싶은데
안드, ios, 웹 각각 한명씩 맡고있는 프론트엔드 팀원끼리도 의견을 맞춰야 하기 때문에 의사 결정이 쉽지가 않을 것 같기도 하다.

그래도 팀원들과 이야기를 하며 하나씩 정리해 가는 과정이 즐겁다.
틀릴 때가 더 많은 내 의견을 진지하게 듣고 함께 고민해주는 팀원들에게 감사한 마음도 든다.

0개의 댓글