Firebase 채팅 구현하기

김재현·2025년 3월 6일

이번 프로젝트를 진행하면서 Firebase로 실시간 채팅을 구현해보았습니다.

생각보다 신경쓸 부분이 많아서 오래걸렸지만 차근차근 작성해 보겠습니다.

데이터 모델 설정

data class ChatRoomDataEntity(
    val chatRoomId: String,
    val lastMessageData: LastMessageDataEntity,
    val unReadMessage: Int,
    val participant: List<String>,
    val chatRoomSession: List<Map<String, Boolean>>
)

data class LastMessageDataModel(
    val lastMessage: String,
    val lastSendAt: String,
    val lastMessageSender: String
)
  • 채팅방에 대한 데이터는 채팅방 고유 ID, 마지막으로 받은 메시지(시간, 내용), 안읽은 메시지 갯수, 참여자, 채팅방에 지금 들어와 있는지 이렇게 구성되어 있습니다.

  • 마지막으로 받은 메시지에 대한 내용을 가지고 오는 것보다 더 간단할 것 같아서 메시지를 보낼 때 Firebase에서 update 할 수 있도록 구성했고, 채팅방에 현재 상대방이 들어와 있는지 없는지 Boolean 타입으로 나타내어 false일 경우에는 안읽은 메시지로 나타내고 true일 경우에는 읽은 메시지로 나타낼 수 있도록 하였습니다.

  • 채팅방 List에서 안읽은 메시지를 나타낼 수 있도록 Firebase의 snapshotListener를 이용하여 실시간으로 갱신되어 update 할 수 있도록 unReadMessage를 구성했습니다.

chatRoomDB.collection(COLLECTION_CHAT_MESSAGE)
	.whereArrayContains("read", mapOf(recipientUid to false))
	.addSnapshotListener { unReadMessage, e ->
        chatRoomDB.update("unReadMessage", unReadMessage?.size())
}

// * unReadMessage 갯수 갱신

data class MessageDataModel(
    val uid: String,
    val chatRoomId: String,
    val messageId: String,
    val message: String?,
    val imageList: List<ImageDataEntity>?,
    val sendAt: String,
    val read: List<Map<String, Boolean>>,
    val type: MessageViewType
)

enum class MessageViewType(val messageType: Int) {
    TEXT_MESSAGE(0),
    IMAGE_MESSAGE(1)
}
  • 메시지는 보낸 사람의 uid(고유 id를 통해서 그 사람의 프로필 정보를 가져옴) 메시지 고유 id, 보낸 시간, 읽음 여부, 메시지 타입(imageType or textType)을 담았습니다.

  • 이미지 타입과 텍스트 타입 두가지 타입의 메시지를 보낼 수 있는데 메시지 타입일 때는 enum class로 타입을 나누어 multiView로 recyclerView를 나타낼 수 있도록 하였고 imageType일 때는 message 내용을 null로, textType일 때는 imageList를 null로 Firebase 채팅방에 보내줍니다.


채팅방과 채팅방을 구성하는 메시지에 대한 데이터 모델은 이렇게 설정하였습니다. 생각보다 처음 만들어보는 채팅이라서 그런지 생각해야할 부분이 많아서 데이터 모델만 해도 계속 추가하고 수정했었던 것 같습니다.
.
.
.

코드 구성

  • 메시지를 보낼 때 생각할 경우의 수가 있는데 기존에 첫번째 메시지를 보내서 채팅방이 존재하는지 아닌지 체크를 해서 채팅방이 존재한다면 메시지만 보내야 하는 경우의 수가 있고 채팅방이 없다면 메시지를 보내기 전에 채팅방을 형성해주고 보내야 한다는 경우의 수가 있습니다.

기존 채팅방이 없을 경우 (처음 메시지를 보낼 때)

  • 기존 채팅방이 없는 경우 서로 친구일 경우에 다음과 같이 상단에 친구 목록이 나타나게 되는데 채팅을 시작할 친구를 눌러서 채팅을 시작할 수 있습니다.

  • 채팅방을 들어가면 먼저 채팅방이 있는지 없는지 Boolean으로 확인을 하고 채팅방이 없다면 false를 반환, 있다면 true를 반환하게 합니다.

  • 채팅방이 있는지 없는지 체크하는 메서드는 LiveData로 observe하여 첫 메시지를 보내고 난 후 채팅방이 감지되면 바로 true를 반환합니다.

  • 체크 메서드가 false로 결과 값을 반환하는 경우에 첫 메시지를 보내면 다음과 같이 Firebase에 채팅방에 대한 데이터가 set 됩니다.


기존 채팅방이 있는 경우

  • 기존 채팅방이 있는 경우는 간단합니다. 기존 채팅방의 고유 id를 받아와서 하위 document에 데이터를 set 해주는 것으로 처리합니다.

  • 여기서 신경써야할 부분은 상대방이 지금 채팅방 안에 들어와 있는지 체크하여 Boolean 타입으로 반환시켜 줍니다. true일 경우는 상대방이 채팅방에 들어와 있는 경우로 생각하여 read의 value 값을 true로 서버에 업로드 시켜줍니다. 그럼 상대방은 메시지를 바로 읽었다고 값을 받게 되므로 바로 읽은 상태의 메시지가 채팅방에 나타납니다. 반대의 경우에는 value가 false이므로 읽지 않은 메시지로 채팅방에 표시됩니다.

신경쓰지 못한 점

  • 상대방이 채팅을 읽는 경우를 생각하지 못해서 데이터 모델도 다시 고치고 시간이 좀 걸렸던 것 같습니다.

  • 원래는 시간을 현재 사용하는 기기 시간인 LocalDateTime을 사용하여 작업을 진행하였는데 sortedDescending 이용하여 메시지 리스트를 adapter에 submitList하였을 때 순서대로 나오지 않는 경우가 발생하였습니다. 이 부분을 고치고자 한국 표준시를 이용하였고 이 부분에서 조금 아쉬웠던 점은 UTC를 활용하여 하지 못한 것 같아 다음에는 UTC+09와 같이 적용을 해볼까 생각하고 있습니다.

fun chatDateFormat() : String {
    val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS", Locale.KOREAN)
    formatter.timeZone = TimeZone.getTimeZone("Asia/Seoul")
    return formatter.format(Date().time)
}
// * 한국 표준 시를 가져오는 코드
  • 개인적으로 채팅이 정말 신경쓸 부분이 많은 작업이라고 들었는데 실제로 멀티뷰타입, 채팅방이 존재하는지, 상대방이 채팅방에 들어와 있는지 등등 생각할 부분이 많았다고 생각합니다. 덕분에 조금 좋은 경험이었다고 생각합니다.

0개의 댓글