Final (31)
프로젝트 막바지에는 기능 구현에 쫓겨 블로그 정리를 제대로 하지 못했다. 그래서 이 글은 당시 GitHub 커밋 내역과, 프로젝트 기간에 직접 적어 둔 업무 내역 목록을 AI(Claude Code)에게 보여주고 그걸 바탕으로 내용을 다시 정리했다. 코드는 실제 커밋에 있던 것을 가져왔다.
진문님이 29일에 채팅방에 날짜와 시간 표시 기능을 추가했다. 그동안 내가 만든 신고, 차단, 실시간 갱신 코드와 이걸 합쳤더니, 메시지가 채팅 화면에 하나도 안 보이는 문제가 생겼다. 이날은 이걸 고쳤고, 이 코드도 팀원이 올려준 7월 1일 [Merge] 커밋에 들어 있다.
날짜별 메시지 묶기
진문님의 기능은 메시지를 날짜별로 묶어서, 날짜마다 맨 위에 “2024년 06월 29일” 같은 표시를 붙이는 것이다. 이를 위해 채팅 화면이 데이터를 읽는 방식이 바뀌었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var messagesByDate: [Date: [Message]] = [:]
var sortedDates: [Date] = []
func groupMessagesByDate() {
let calendar = Calendar.current
messagesByDate = Dictionary(grouping: messages) { message in
calendar.startOfDay(for: message.sentDate)
}
for (date, msgs) in messagesByDate {
messagesByDate[date] = msgs.sorted { $0.sentDate < $1.sentDate }
}
sortedDates = messagesByDate.keys.sorted(by: <)
}
Dictionary(grouping:by:) 는 배열을 기준값별로 묶어서 dictionary 로 만들어 준다. 여기서는 메시지의 보낸 시각을 그날 0시(startOfDay)로 바꾼 값을 기준으로 삼아서, 같은 날 보낸 메시지끼리 묶인다. 날짜 key 도 정렬해서 sortedDates 에 따로 둔다.
그리고 MessageKit 에 데이터를 넘기는 쪽도 messages 대신 이 묶음을 보도록 바뀌었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return sortedDates.count
}
func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int {
let date = sortedDates[section]
return messagesByDate[date]?.count ?? 0
}
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
let date = sortedDates[indexPath.section]
return messagesByDate[date]![indexPath.item]
}
기존에는 section 하나가 메시지 하나였는데, 이제는 section 이 날짜 하나, item 이 그날의 메시지 하나다.
합친 뒤 메시지가 안 보이던 문제
합치고 나서 채팅방에 들어가면 메시지가 하나도 안 보였다. print 를 찍어 보니 처음에는 아무것도 나오지 않았다.
원인은 함수 호출 순서였다. 화면은 이제 messagesByDate 를 보고 그리는데, 이 값은 groupMessagesByDate() 를 불러야만 채워진다. 그런데 내가 만든 쪽에서 메시지를 받으면 messages 배열만 갱신하고, groupMessagesByDate() 는 부르지 않고 있었다. messages 에는 메시지가 있는데 messagesByDate 는 계속 비어 있으니, 화면에는 아무것도 나오지 않은 것이다.
메시지를 넣는 길은 두 개다.
insertNewMessage: 메시지 하나를 추가하거나 바꿀 때updateMessages: 지난 글에서 만든subscribe가 메시지 목록을 통째로 돌려줄 때
그래서 두 함수 모두 마지막에 groupMessagesByDate() 를 부르도록 바꿨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private func insertNewMessage(_ message: Message) {
if let index = messages.firstIndex(where: { $0.id == message.id }) {
// 기존 메시지가 있을 경우 업데이트
var existingMessage = messages[index]
var updatedMessage = message
if let image = existingMessage.image {
updatedMessage.image = image
}
messages[index] = updatedMessage
} else {
// 새로운 메시지를 추가
messages.append(message)
}
messages.sort(by: { $0.sentDate < $1.sentDate })
groupMessagesByDate()
DispatchQueue.main.async {
self.messagesCollectionView.reloadData()
}
}
private func updateMessages(_ newMessages: [Message]) {
var updatedMessages = [Message]()
let existingMessagesDict = Dictionary(uniqueKeysWithValues: self.messages.map { ($0.id, $0) })
for newMessage in newMessages {
if let existingMessage = existingMessagesDict[newMessage.id] {
// 기존 메시지에 이미지 정보가 있다면 유지
var updatedMessage = newMessage
if let image = existingMessage.image {
updatedMessage.image = image
}
updatedMessages.append(updatedMessage)
} else {
updatedMessages.append(newMessage)
}
}
self.messages = updatedMessages
self.messages.sort(by: { $0.sentDate < $1.sentDate })
groupMessagesByDate()
DispatchQueue.main.async {
self.messagesCollectionView.reloadData()
}
}
updateMessages 는 새 목록을 받을 때 이미 받아 둔 이미지는 유지한다. 목록을 통째로 바꾸면 사진 메시지의 이미지가 사라지고 다시 받아야 하기 때문이다.
subscribe 결과를 받는 곳에서는 updateMessages 를 먼저 부른다.
1
2
3
4
5
6
7
8
9
10
11
12
13
let myUid = user?.uid // 현재 사용자가 있는 경우에만 uid를 전달
chatFirestoreStream.subscribe(id: id, myUid: myUid) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let messages):
self.updateMessages(messages) // Ensure updating messages array
self.loadImageAndUpdateCells(messages)
self.preloadProfileImages(for: messages)
case .failure(let error):
print(error)
}
}
신고와 차단의 메시지 찾기
section 의 뜻이 바뀌면서 신고와 차단에도 문제가 생겼다. 기존 코드는 길게 누른 메시지를 messages[indexPath.section] 으로 찾았는데, 이제 section 은 날짜라서 엉뚱한 메시지가 신고될 수 있었다. 그래서 날짜 section 으로 그날의 메시지 묶음을 꺼내고, 그 안에서 indexPath.row 번째를 찾도록 바꿨다.
1
2
3
4
5
6
7
let dateSections = Array(messagesByDate.keys).sorted()
let selectedDate = dateSections[indexPath.section]
guard let selectedMessages = messagesByDate[selectedDate] else {
print("선택한 날짜의 메시지를 찾을 수 없습니다.")
return
}
let selectedMessage = selectedMessages[indexPath.row]
같이 정리한 것
messages 에 didSet 을 붙여서, 배열이 바뀔 때마다 화면을 다시 그리고 맨 아래로 스크롤하게 했다. 메시지를 넣는 곳이 여러 군데라 매번 reloadData 를 부르는 걸 빠뜨리지 않으려는 것이다.
1
2
3
4
5
6
7
8
private var messages = [Message]() {
didSet {
DispatchQueue.main.async {
self.messagesCollectionView.reloadData()
self.messagesCollectionView.scrollToLastItem(animated: false)
}
}
}
스크롤 애니메이션은 animated: false 로 바꿨다. 메시지가 여러 개 들어올 때마다 화면이 위아래로 움직이는 게 거슬렸기 때문이다. 날짜 표시는 앱 폰트(ThemeFont)와 색으로 맞췄다.