채팅방이 이렇게 어렵다고?
솔직히 채팅 UI는 쉬울 거라고 생각했다.
메시지 보여주고, 입력받고, 전송하면 되는 거 아닌가.
아니었다.
키보드가 입력창을 덮는다
가장 먼저 부딪힌 건 키보드였다. 메시지 입력창을 탭하면 키보드가 올라오면서 입력창을 그대로 덮어버린다.
React Native에는
KeyboardAvoidingView라는 게 있다. 키보드가 올라오면 레이아웃을 밀어올려 주는 컴포넌트. 당연히 이걸 쓰면 될 거라고 생각했다.
Expo Router의 탭 구조 안에서는 동작을 안 한다.
탭바가 화면 하단 82px를 차지하고 있는데,
KeyboardAvoidingView가 그걸 모른다. 키보드 높이를 잘못 계산해서 레이아웃이 어긋난다.
결국 직접 처리했다.
Keyboard.addListener('keyboardWillShow', (e) => { const kbHeight = e.endCoordinates.height setPadding(kbHeight - TAB_BAR_HEIGHT) // 탭바 높이 차감 })
KeyboardAvoidingView 쓰지 말고, 키보드 높이를 직접 받아서 패딩으로 처리한다. 탭바 안에 있을 때와 전체 화면일 때 계산식이 다르다.
오른쪽 스와이프 답장
카카오톡처럼 말풍선을 오른쪽으로 스와이프하면 답장이 되는 기능.
PanResponder로 구현했다. 50px 이상 당기면 답장 모드 진입, 손을 떼면 spring-back. 말풍선 위에 인용 바가 뜨고, 전송하면 원본 메시지가 접혀서 보인다.
이건 구현 자체보다 세부 조정이 힘들었다. 50px 임계값이 너무 낮으면 스크롤할 때 자꾸 걸리고, 너무 높으면 답장 의도가 있어도 발동이 안 된다. 여러 번 테스트하면서 맞췄다.
히스토리가 안 올라온다
가장 황당한 버그였다.
방을 열면 메시지가 보인다. 그런데 어떤 방은 스크롤해도 위로 안 올라간다. 특정 시점에서 멈춘 것처럼 보인다.
이상한 건 방마다 증상이 달랐다. 어떤 방은 정상이고, 어떤 방은 안 됐다. 코드는 똑같은데.
원인은 쿼리였다.
// 이 쿼리가 문제 .order('created_at', { ascending: true }).limit(50)
ascending: true에 limit(50)을 걸면 가장 오래된 50개를 가져온다. 메시지가 50개 이하인 방은 전부 다 나오니까 정상처럼 보인다. 50개가 넘는 방은 최신 메시지가 아예 안 보인다.
방마다 증상이 달랐던 이유가 이거였다.
// 정답 .order('created_at', { ascending: false }).limit(50) // 가져온 다음 뒤집기 .then(messages => messages.reverse())
최신 50개를 내림차순으로 가져온 뒤, 화면에 뿌리기 전에 시간순으로 뒤집는다.
알고 보면 당연한 건데, 알기 전까진 어디서 막힌 건지 몰랐다.
Realtime과 낙관적 업데이트의 충돌
메시지를 보내면 즉시 화면에 보여주는 낙관적 업데이트를 구현했다. 전송 버튼을 누르자마자 내 메시지가 뜨고, 전송이 완료되면 서버 데이터로 교체된다.
문제는 Realtime INSERT가 API 응답보다 빠를 때가 있다는 것이다.
내 메시지가 DB에 들어가자마자 Realtime으로 내 앱에 되돌아온다. 그러면 낙관적 메시지와 Realtime 메시지가 둘 다 있게 된다. 같은 메시지가 두 개.
해결은
client_id로 매칭해서 교체할 때, 낙관적 메시지에 붙어있던 로컬 데이터(reply_to 같은 것들)를 함께 넘기는 것이다.
당연히 될 것 같은 것들이 안 됐다.
채팅 UI 하나 만드는 데 이렇게 많은 게 숨어있다는 걸, 만들기 전까진 몰랐다. 이것도 알게 됐다는 게 수확이다.