What I Learned
1. 왜 Message 모델에 isFromCurrentUser를 계산 프로퍼티로 만들었는가?
struct Message: Identifiable, Equatable {
let senderNumber: Int
// ...
var isFromCurrentUser: Bool {
senderNumber == UserSession.shared.userNumber
}
}
Swift
복사
메시지를 생성할 때 isFromCurrentUser: true를 직접 넣으면 두 가지 문제가 생긴다.
1.
데이터 불일치: P2P로 받은 메시지를 변환할 때 실수로 true를 넣으면 남의 메시지가 내 메시지로 보임
2.
중복 로직: 메시지 생성하는 모든 곳에서 senderNumber == myNumber 비교를 해야 함
계산 프로퍼티로 만들면 senderNumber만 정확하면 항상 올바른 값이 나온다. 단일 진실 공급원(Single Source of Truth) 원칙
2. 왜 P2PPayload에서 Message로 변환하는 생성자를 만들었는가?
init(from payload: P2PPayload) {
self.id = payload.messageId
self.senderNumber = payload.senderNumber
self.senderStage = payload.senderStage
self.senderNickname = payload.senderNickname
self.text = payload.content
self.sentAt = payload.timestamp
}
Swift
복사
P2P로 데이터를 주고받을 때는 P2PPayload를 사용하고 UI에서는 Message를 사용한다.
두 모델의 역할이 다르기 때문
•
P2PPayload: 네트워크 전송용 (type, targetNumber 등 포함)
•
Message: UI 표시용 (displayName, isFromCurrentUser 등 포함)
변환 로직을 Message 내부에 두면 변환이 필요한 곳에서 Message(from: payload) 한 줄로 끝난다.
3. 왜 메시지 라우터를 별도로 분리했는가?
final class MessageRouter {
static let shared = MessageRouter()
private func routeMessage(_ payload: P2PPayload) {
switch payload.type {
case .publicMessage:
PublicChat.shared.receiveMessage(payload)
case .directMessage, .dmRequest, ...:
// DirectChat.shared.receiveMessage(payload)
break
default:
break
}
}
}
Swift
복사
처음에는 PublicChat에서 직접 P2P 리스너를 등록했다.
// 나쁜 예
P2PService.shared.onMessageReceived = { payload in
self.handleMessage(payload)
}
Swift
복사
문제는 onMessageReceived가 하나뿐이라는 것. DM 기능을 추가하면 PublicChat의 리스너를 덮어쓰게 된다.
MessageRouter를 두면
•
P2P 리스너는 하나만 등록
•
메시지 타입에 따라 적절한 Model로 분배
•
새로운 메시지 타입 추가 시 라우터만 수정하면 됨
4. 왜 240자 제한을 두었는가?
private let maxMessageLength = 240
func validateMessage(_ text: String) -> Bool {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
return !trimmed.isEmpty && trimmed.count <= maxMessageLength
}
Swift
복사
익명 근거리 채팅의 특성상 짧고 가벼운 대화가 적합하다.
긴 글을 쓰면
•
스크롤이 길어져서 다른 메시지가 묻힘
•
익명성과 어울리지 않는 깊은 대화 유도
•
P2P 전송 시 데이터 크기 증가
240자는 트위터(280자)보다 약간 짧은 수준으로, 한두 문장 정도의 메시지에 적합하다.
5. 왜 100개 메시지 제한을 두었는가?
private let maxMessages = 100
private func addMessage(_ message: Message) {
messages.append(message)
if messages.count > maxMessages {
messages.removeFirst(messages.count - maxMessages)
}
}
Swift
복사
메모리 관리 목적이다. 채팅이 활발하면 메시지가 계속 쌓이는데
제한이 없으면
•
메모리 사용량 증가
•
UI 렌더링 성능 저하 (긴 리스트)
•
오래된 메시지는 어차피 의미 없음 (실시간 대화니까)
100개면 스크롤해서 과거 대화를 볼 수 있으면서도 메모리 부담이 적다.
6. 왜 isActive 플래그로 unreadCount를 관리하는가?
var isActive: Bool = false {
didSet {
if isActive { unreadCount = 0 }
}
}
func receiveMessage(_ payload: P2PPayload) {
// ...
if !isActive {
unreadCount += 1
}
}
Swift
복사
사용자가 퍼블릭 채팅 화면을 보고 있으면 새 메시지가 와도 "읽지 않음" 표시를 할 필요가 없다. 이미 보고 있으니까.
•
isActive = true: 채팅 화면 진입 시 View에서 설정, unreadCount 초기화
•
isActive = false: 채팅 화면 이탈 시 View에서 설정
•
메시지 수신 시 isActive가 false면 unreadCount 증가
이렇게 하면 탭 바에 뱃지를 표시할 때 PublicChat.shared.unreadCount만 참조하면 된다.
Key Takeaways
1.
계산 프로퍼티: 파생 데이터는 저장하지 말고 계산하자
2.
변환 생성자: 모델 간 변환 로직은 모델 내부에
3.
라우터 패턴: 여러 목적지로 분배할 때 중간 계층 추가
4.
제한값 설정: 메모리와 UX를 고려한 합리적인 제한
5.
상태 플래그: View 상태를 Model이 알아야 할 때 간단한 플래그 사용
