Final (22)
프로젝트 막바지에는 기능 구현에 쫓겨 블로그 정리를 제대로 하지 못했다. 그래서 이 글은 당시 GitHub 커밋 내역과, 프로젝트 기간에 직접 적어 둔 업무 내역 목록을 AI(Claude Code)에게 보여주고 그걸 바탕으로 내용을 다시 정리했다. 코드는 실제 커밋에 있던 것을 가져왔다.
내용이 많아 이어서 작성한다. 채팅 화면에서 상대방이 누군지 알 수 있도록, 메시지 옆에 보낸 사람의 프로필 사진을 띄웠다. 마이페이지에서 설정한 유저의 profileImageUrl 을 그대로 쓴다.
채팅방에 메시지를 보낸 사람 찾기
프로필 사진을 띄우려면 먼저 이 채팅방에 누가 메시지를 보냈는지 알아야 한다. 채팅 데이터는 Firestore 에 이런 구조로 저장되어 있다.
channelscollection 안에 채팅방마다 document 가 하나씩 있고- 각 채팅방 document 아래
threadcollection 에 메시지가 하나씩 쌓인다 - 메시지마다 보낸 사람의 uid 가
senderId로 들어 있다
그래서 채팅방 이름으로 document 를 찾고, 그 안의 thread 를 전부 읽어서 senderId 만 모으는 ChatManager 를 만들었다.
1
2
let channelCollection = Firestore.firestore().collection("channels")
let db_channelName = "name"
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
class ChatManager {
func getSenders(channelName: String, completion: @escaping ([String]) -> Void) {
channelCollection.whereField(db_channelName, isEqualTo: channelName).getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
print("Error fetching channel: \(String(describing: error))")
completion([])
return
}
var senderIds = Set<String>()
for document in snapshot.documents {
let threadCollection = channelCollection.document(document.documentID).collection("thread")
threadCollection.getDocuments { (threadSnapshot, error) in
guard let threadSnapshot = threadSnapshot, error == nil else {
print("Error fetching thread: \(String(describing: error))")
return
}
for threadDocument in threadSnapshot.documents {
if let senderId = threadDocument.data()["senderId"] as? String {
senderIds.insert(senderId)
}
}
completion(Array(senderIds))
}
}
}
}
}
한 사람이 메시지를 여러 번 보내도 uid 는 한 번만 있으면 되니까 Set 에 담아서 중복을 없앴다.
프로필 사진 URL 모으기
ChatVC 에는 uid 별로 프로필 사진 URL 을 담아 둘 dictionary 와, 이미 받은 이미지를 담아 둘 dictionary 를 추가했다.
1
2
3
4
let chatManager = ChatManager()
private var profileImageUrls = [String: String]()
private var imageCache = [String: UIImage]()
기존의 fetchDisplayName 은 닉네임만 가져왔는데, 닉네임과 프로필 사진 URL 을 같이 가져오도록 fetchUserDataAndProfileImage 로 바꿨다. uid 를 넘기면 그 사람의 정보를, 안 넘기면 내 정보를 가져온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private func fetchUserDataAndProfileImage(for uid: String? = nil, completion: @escaping (String?, String?, [String: Any]?) -> Void) {
let userId = uid ?? user?.uid
guard let userId = userId else {
completion(nil, nil, nil)
return
}
let userManager = UserManager()
userManager.fetchUserData(uid: userId) { error, snapshot in
if let error = error {
print(error)
completion(nil, nil, nil)
return
}
guard let dictionary = snapshot?.value as? [String: Any] else {
completion(nil, nil, nil)
return
}
let currentName = (dictionary[db_nickName] as? String) ?? "Unknown"
let profileImageUrl = (dictionary["profileImageUrl"] as? String)
completion(currentName, profileImageUrl, dictionary)
}
}
이걸로 내 정보, 보낸 사람들 정보를 각각 가져온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private func fetchDisplayNameAndProfileImage(completion: @escaping (String?, String?) -> Void) {
fetchUserDataAndProfileImage { displayName, profileImageUrl, _ in
completion(displayName, profileImageUrl)
}
}
private func getSenderImage() {
chatManager.getSenders(channelName: channel.name) {[weak self] senderIds in
self?.fetchProfileImages(for: senderIds)
}
}
private func fetchProfileImages(for senderIds: [String]) {
senderIds.forEach { senderId in
fetchUserDataAndProfileImage(for: senderId) { [weak self] _, imageUrl, _ in
if let profileImageUrl = imageUrl {
self?.profileImageUrls[senderId] = profileImageUrl
self?.messagesCollectionView.reloadData() // Reload to update avatars
}
}
}
}
viewDidLoad 에서는 내 닉네임과 사진 URL 을 먼저 받고, getSenderImage() 로 채팅방 사람들의 사진 URL 을 받는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override func viewDidLoad() {
super.viewDidLoad()
configureColor()
fetchDisplayNameAndProfileImage { [weak self] displayName, imageUrl in
self?.currentDisplayName = displayName ?? "Unknown"
if let profileImageUrl = imageUrl, let userId = self?.user?.uid {
self?.profileImageUrls[userId] = profileImageUrl
}
self?.messagesCollectionView.reloadData()
DispatchQueue.main.async {
self?.messagesCollectionView.scrollToLastItem()
}
}
getSenderImage()
// ... 생략
}
채팅방에 들어온 뒤에 새 사람이 메시지를 보낼 수도 있다. 그래서 메시지를 받을 때마다 아직 이미지가 없는 사람의 URL 도 미리 받아 두도록 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
private func preloadProfileImages(for messages: [Message]) {
let uniqueSenderIds = Set(messages.map { $0.sender.senderId })
uniqueSenderIds.forEach { senderId in
guard imageCache[senderId] == nil else { return }
fetchUserDataAndProfileImage(for: senderId) { [weak self] _, imageUrl, _ in
if let profileImageUrl = imageUrl {
self?.profileImageUrls[senderId] = profileImageUrl
self?.messagesCollectionView.reloadData() // Reload to update avatars
}
}
}
}
listenToMessages 에서 메시지를 받으면 loadImageAndUpdateCells 다음에 이 함수를 부른다.
메시지 옆에 프로필 사진 띄우기
URL 을 모았으니 이제 실제 사진을 받아서 메시지 옆에 붙인다. 같은 사진을 여러 번 받지 않도록 캐시를 같이 썼다.
캐시란?
캐시는 한 번 가져온 것을 가까운 곳에 보관해 두고 다음에 다시 쓰는 것이다. 자주 보는 책을 도서관에 매번 빌리러 가지 않고 책상 위에 올려 두는 것과 같다.
채팅 화면은 스크롤할 때마다 cell 을 다시 그린다. 같은 사람이 메시지를 20개 보냈으면 같은 사진을 20번 그려야 하는데, 매번 인터넷에서 받으면 느리고 데이터도 낭비된다. 그래서 한 번 받은 사진은 imageCache 에 uid 를 key 로 넣어 두고, 다음부터는 거기서 꺼내 쓴다.
MessageKit 의 avatar 설정
MessageKit 은 메시지 옆의 동그란 사진(avatar)을 delegate 로 물어본다. 캐시에 사진이 있으면 그걸, 없으면 닉네임 앞 두 글자를 보여준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func avatarFor(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageKit.Avatar {
let sender = message.sender
let initials = String(sender.displayName.prefix(2))
if let cachedImage = imageCache[sender.senderId] {
return MessageKit.Avatar(image: cachedImage, initials: initials)
} else {
// Preloaded images should already be in the cache if available
return MessageKit.Avatar(initials: initials)
}
}
func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
return CGSize(width: 30, height: 30)
}
실제 사진은 cell 을 만들 때 Kingfisher 로 받아서 넣는다. 받는 데 성공하면 캐시에 넣고, 실패하면 이니셜을 보여준다.
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
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! MessageContentCell
let message = messages[indexPath.section]
configureAvatarView(cell.avatarView, for: message, at: indexPath, in: collectionView as! MessagesCollectionView)
return cell
}
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
let sender = message.sender
let initials = String(sender.displayName.prefix(2))
// 다른 사용자의 프로필 이미지 설정
if let imageUrl = profileImageUrls[sender.senderId], let url = URL(string: imageUrl) {
avatarView.kf.setImage(with: url, placeholder: nil, options: nil, progressBlock: nil) { result in
switch result {
case .success(let value):
self.imageCache[sender.senderId] = value.image
DispatchQueue.main.async {
avatarView.set(avatar: MessageKit.Avatar(image: value.image, initials: initials))
}
case .failure(let error):
print("Error downloading image: \(error.localizedDescription)")
DispatchQueue.main.async {
avatarView.set(avatar: MessageKit.Avatar(initials: initials))
}
}
}
} else {
avatarView.set(avatar: MessageKit.Avatar(initials: initials))
}
}
정리하면 흐름은 이렇다.
- 채팅방에 들어오면 보낸 사람들의 uid 를 모은다
- uid 마다 프로필 사진 URL 을 받아
profileImageUrls에 넣는다 - cell 을 그릴 때 URL 로 사진을 받아 avatar 에 넣고
imageCache에 보관한다 - 다음에 같은 사람의 cell 을 그릴 때는 캐시에서 꺼낸다
지금 보면 getSenders 는 thread 를 전부 읽어 오기 때문에, 메시지가 많은 방일수록 들어갈 때 읽는 양이 많아진다. 어차피 preloadProfileImages 가 받은 메시지 기준으로 사람을 모으니, 둘 중 하나로 합칠 수 있었을 것 같다.