이번 채팅기능을 구현할 때 가장 중요한 키포인트들이 있었다.
사용자가 특정 채팅방에 들어갈 때, 이전에 채팅했던 기록을 불러와 시간순으로 나타내야 했다. 이를 구현하기 위해 채팅방에서 글 작성 이벤트가 발생할 때마다 대화 내용을 DB에 저장하였다.
그리고 다시 해당 채팅방에 들어갈 때 페이지네이션을 통해 20개의 글로 나눠서 클라이언트로 리턴하였다.
socket.on(EVENT.ROOM, async ({ receiveUserId, sendUserId, chatText, sample }) => {
try {
// --- 앞부분 코드생략
//입력된 채팅기록 db에 저장
await chatService.createChat({
roomNum,
...getChat,
});
// ---뒷부분 코드 생략
io.to(roomNum).emit(EVENT.CHAT, getChat);
io.to(receiveUserId).emit(EVENT.LIST, getChat);
} catch (error) {
console.log(error);
}
});
사용자가 채팅방에 없더라도 신규 채팅이 온다면 사용자가 인지할 수 있게 알림기능을 구현해야 했다.
알림 기능을 구현하기위해 위에 나와있는 채팅 소켓 이벤트 코드에서 io.to(receiveUserId).emit()을 통해 채팅 상대방에게도 따로 채팅이 리턴되게 구현하였다.
이를 위해 사용자가 로그인을 하게되면 무조건 해당 사용자의 userId에 해당하는 room으로 join하게끔 코드를 구현하였다.
또한 채팅방에 입장하게됐을때 사용자의 userId에 해당하는 room에서 leave하게끔 구현해 채팅 기록이 중복으로 올라가는 것을 차단하였다.
//로그인 시 사용자의 userId에 해당하는 room으로 join시키는 코드
socket.on(EVENT.LOGIN, ({ userId }) => {
socket.join(userId);
});
//채팅방에 입장하게되면 로그인 시 join했던 userId room에서 leave시키는 코드 구현
socket.on(EVENT.JOIN_ROOM, async ({ userId, qUserId }) => {
try {
const roomNum = await roomNumMaker(userId, qUserId);
socket.join(roomNum);
socket.leave(userId);
} catch (error) {
console.log(error);
}
});
OAO 서비스의 기능 중 녹음파일을 업로드하는 api가 이미 구현되어 있었다. 그래서 먼저 채팅방에서 음성파일을 바로 녹음해 올리는 기능도 해당 api와 동일한 방식으로 구현하였다.
이를 위해 채팅방에서 녹음된 파일을 s3에 저장하는 api를 구현하였고, db에 저장된 녹음파일 url을 클라이언트로 리턴하는 소켓이벤트도 같이 구현하였다.
// 채팅방에서 녹음된 파일을 db에 저장하는 코드
const postTrack = async (req, res, next) => {
try {
// ---위 코드 생략
await chatService.createChat({
roomNum,
sendUserId,
receiveUserId,
chatText,
checkChat,
chatType,
sample,
});
res.sendStatus(200);
} catch (error) {
next(error);
}
};
// 이후 소켓 이벤트로 채팅방에 녹음파일 url을 전달하는 코드
socket.on(EVENT.FILE, async ({ receiveUserId, sendUserId, chatType }) => {
try {
const getChat = await chatService.getChatByIds({ receiveUserId, sendUserId, chatType });
io.to(roomNum).emit(EVENT.CHAT, getChat);
io.to(receiveUserId).emit(EVENT.LIST, getChat);
} catch (error) {
console.log(error);
}
});
api가 먼저 작동되고 status 200을 리턴받게 되면 소켓 이벤트가 실행되게 구현하였다. 전혀 문제가 없는 코드였지만 아이폰에서도 이상없이 구현하기 위해서 컨버팅 기능을 추가하면서 문제가 발생했다.
아이폰 환경에서 나타나는 에러들
아이폰에서만 발생하는 에러들에 대해서는 해당 링크에 정리해 두었다. 에러에 대한 것은 링크로 대체하고 해당 에러를 해결하기 위해서 컨버팅을 진행하였다. 또한 녹음 이후 바로 채팅방에서 녹음 파일이 재생될 수 있게 유효한 트랙 url을 리턴하기 위해서
<컨버팅 -> 트랙 url db에 저장 -> 소켓이벤트>
위와 같은 순서가 지켜지게 동기적으로 코드를 작성하였다.
채팅 리스트 페이지에 접속하게되면 사용자가 채팅중인 채팅방들의 목록이 나타나야 했다. 여기에는 상대방의 프로필사진, 닉네임이 들어가야하고 가장 마지막에 작성된 채팅의 내용과 해당 채팅을 사용자 본인이 아닌 상대방이 쓴것이라면 읽었는지 안읽었는지에 대한 정보도 갖고있어야 했다.
해당 기능을 구현하기 위해서 마지막 채팅 기록을 보낸사람이 누구인지 확인하여 유저에 대한 정보를 담았고, 채팅 읽음에 대한 유무는 db에 채팅마다 기록되게하여 판별하였다.
for (let i = 0; i < result.length; i++) {
if (result[i].sendUserId === Number(userId)) {
const qUserId2 = await User.findOne({
attributes: ["nickname", "contact", "profileImage", "introduce", "userId"],
where: { userId: result[i].receiveUserId },
});
result[i].dataValues.userId = userId;
result[i].dataValues.qUserId = qUserId2;
} else if (result[i].receiveUserId === Number(userId)) {
const qUserId1 = await User.findOne({
attributes: ["nickname", "contact", "profileImage", "introduce", "userId"],
where: { userId: result[i].sendUserId },
});
result[i].dataValues.userId = userId;
result[i].dataValues.qUserId = qUserId1;
}
}
이렇게해서 채팅 기능 구현을 완료하였다. 처음 시작단계에서 기획을 촘촘히하고 시작하지 않아 테이블 구조가 최적화 되지 않은 점이 아쉽지만 그래도 OAO 서비스에 적합한 최소한의 채팅 기능들을 모두 구현하였다.