Skip to content

[GMSS-300] 채팅방 메시지 애니메이션 성능 개선 및 전송 UX 개선 - #74

Merged
soyeonLee126 merged 17 commits into
developfrom
fix/chatting-room
Sep 30, 2026
Merged

soyeonLee126 merged 17 commits into
developfrom
fix/chatting-room

Conversation

@seunghee17

@seunghee17 seunghee17 commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

작업 개요

채팅방의 메시지 애니메이션 성능을 개선하고, 메시지 전송 UX를 다듬었습니다.

  • 내가 보낸 메시지는 서버 응답을 기다리지 않고 바로 화면에 그립니다(낙관적 표시). 위험 문구가 잠깐이라도 보이지 않도록, 온디바이스 위험 검사(사전 매칭)는 먼저 통과한 뒤에 그립니다.
  • 전송이 실패하면 말풍선을 지우고 보낸 내용을 입력칸에 되돌립니다.
  • 캐릭터 메시지는 길게 눌러야 답장합니다.
  • 새 메시지 등장 애니메이션과 스크롤 동작을 프레임 부담이 적은 방식으로 바꿨습니다.

작업 유형

  • feat: 새 기능
  • fix: 버그 수정
  • refactor: 리팩토링 (동작 변경 없음)
  • build: 빌드/의존성 설정
  • chore: 기타 (문서, 설정 등)
  • test: 테스트 추가/수정

변경 사항

낙관적 전송 표시

  • 전송하면 먼저 위험 검사(detectRiskInText)를 하고, 통과하면 입력칸을 비우고 임시 id(음수)로 내 말풍선을 그립니다. 서버 응답이 오면 실제 메시지로 교체합니다.
  • 검사하는 동안에는 isSending으로 중복 전송만 막습니다. 그 사이 사용자가 새로 입력했다면 검사를 통과해도 입력칸을 비우지 않습니다.
  • 임시 id가 서버 id로 바뀌어도 같은 목록 key를 쓰도록 ChatRoomState.localKeyByMessageId와 listKeyOf()를 추가했습니다. 교체되는 순간 아이템이 다시 그려지거나 애니메이션이 다시 돌지 않습니다.

전송 실패 시 입력 복구

  • 전송이 실패하면 임시 말풍선을 지우고 보낸 내용을 입력칸에 되돌립니다(revertOptimisticSend). 응답을 기다리는 동안 새로 입력했다면 그 입력을 덮어쓰지 않습니다.
  • 위험 감지로 차단(CRITICAL)되면 말풍선을 아예 그리지 않고, 입력칸도 그대로 둡니다.
  • GamssInputBar: 입력값이 밖에서 바뀌면(비우기, 복구, 글자 수 제한) 커서를 끝에 둡니다. 복구 시 전송 직전의 커서 위치나 한글 조합 범위가 되살아나지 않도록 rememberInputFieldValue()로 동기화합니다. 타이핑 중에는 덮어쓰지 않도록, 바깥 value가 바뀐 경우에만 맞춥니다.

길게 눌러 답장

  • ChatMessageBubble에서 캐릭터 메시지의 답장 동작을 탭에서 길게 누르기로 바꿨습니다(noRippleCombinedClickable). 짧게 탭하면 아무 동작도 하지 않습니다.

메시지 애니메이션 성능

  • 새 메시지 등장은 graphicsLayer 하나로 슬라이드와 페이드를 처리합니다. 그리기 단계에서만 움직여 매 프레임 재배치가 없고, CompositingStrategy.ModulateAlpha를 써서 오프스크린 레이어도 만들지 않습니다.
  • 기존 메시지 재배치 애니메이션(animateItem)을 제거했습니다. 새 메시지가 올 때마다 보이는 아이템 전부가 움직여 프레임 밀림이 컸습니다.
  • 바닥으로 이동할 때 부드러운 스크롤 대신 즉시 스크롤합니다. 등장 애니메이션과 겹치면서 생기던 프레임 경합을 줄였습니다.
  • 애니메이션 여부는 아이템 자신의 첫 컴포지션에서 판정하고 기록합니다(ChatMessageAnimation.claimShouldAnimate). 진입 시 이미 있던 메시지는 애니메이션하지 않습니다.
  • 다음 답장의 로딩 말풍선을 그 답장과 같은 id로 목록에 포함했습니다(displayMessages). 답장이 공개될 때 자리는 그대로 두고 내용만 바뀝니다.

스크롤 및 새 메시지 토스트

  • 바닥을 보고 있을 때만 새 메시지나 로딩 말풍선을 따라 내려갑니다. 내가 보낸 메시지는 항상 따라갑니다.
  • 위를 보고 있을 때 도착한 메시지는 미리 "본 메시지"로 기록합니다(markSeen). 그래서 토스트나 스크롤로 뒤늦게 보여도 등장 애니메이션 없이 나타납니다.
  • 위를 보고 있을 때 답장이 공개될 때마다 새 메시지 토스트를 갱신합니다. 로딩 말풍선이 맨 아래에 있어도, 로딩을 뺀 최신 답장을 기준으로 판단합니다.
  • 전송 실패로 임시 말풍선이 지워질 때는 새 메시지로 보지 않습니다. 직전 맨 아래 아이템의 key가 목록에서 사라졌는지로 판정하므로, 이미 읽은 메시지가 토스트로 뜨지 않습니다.

테스트

  • CRITICAL로 차단되면 말풍선을 그리지 않고 입력을 유지하는지
  • 전송 성공 후 서버 메시지의 listKeyOf가 임시 말풍선 id와 같은지
  • 위험 검사 중이거나 서버 응답을 기다리는 중에 새로 입력한 내용이, 검사 통과나 전송 실패 후에도 남는지

관련 이슈

  • Closes #

관련 작업 (Notion)

스크린샷 / 동작 화면

체크리스트

  • base 브랜치가 develop으로 설정되어 있다
  • 로컬 빌드 및 실행을 확인했다
  • 커밋 메시지가 컨벤션(feat:, fix: 등)을 따른다
  • 불필요한 로그/주석/디버그 코드를 제거했다
  • 리뷰어가 이해할 수 있도록 설명을 작성했다

리뷰 요청 사항

  • 위험 감지 순서: 리뷰를 반영해 위험 검사를 통과한 뒤에 말풍선을 그리도록 바꿨습니다. 앱 실행 후 첫 전송은 사전을 처음 읽는 만큼 말풍선이 늦게 뜰 수 있는데, 체감되는 수준인지 봐주세요.
  • 입력창 동기화: GamssInputBar의 입력값 동기화가 한글 조합 입력에 영향을 주지 않는지 봐주세요. 특히 빠른 연속 입력과 조합 중 전송 실패 후 이어서 입력하는 경우입니다.
  • 동작 변경: 캐릭터 메시지 답장이 탭에서 길게 누르기로 바뀌었습니다.

- noRippleClickableIfNotNull 대신 noRippleCombinedClickable을 사용해 onClick은 비활성화하고 onLongClick에만 콜백을 연결
- 문서 주석에 길게 눌렀을 때 콜백이 호출됨을 명시
- 전송 즉시 내 메시지 말풍선을 낙관적으로 그리고, 서버 응답에 맞춰 임시 id를 실제 메시지로 교체
- 전송 실패 시 입력칸 복구 대신 실패한 말풍선에 재전송 버튼을 표시하고 ChatRoomViewModel.onRetrySend로 재시도 가능하도록 처리
- GamssSentChatBubble/ChatMessageBubble에 onRetryClick 옵션을 추가해 시간 대신 재전송 버튼을 노출
- ChatMessageAnimation이 메시지 ID 리스트 대신 Message 리스트를 받아 참조 동일성 비교로 불필요한 재매핑을 방지
- markDisplayed()가 SideEffect로 매 리컴포지션마다 호출되어도 메시지 목록이 바뀌지 않았다면 스킵하도록 처리
- 신규 메시지 진입 애니메이션을 부모 SideEffect 대신 아이템 자신의 첫 컴포지션 시점에 판정·기록하도록 변경해 경쟁 상태로 애니메이션이 누락되던 문제 수정
- 다음 캐릭터 답장의 로딩 말풍선을 별도 아이템 대신 메시지 목록에 같은 id로 포함시켜, 실제 메시지로 전환될 때 자리 전체가 사라졌다 재등장하지 않고 내용만 자연스럽게 바뀌도록 개선
- 전송 완료 콜백 대신 마지막 메시지 id 변경을 기준으로 스크롤 로직을 단순화해, 내가 보낸 메시지가 낙관적으로 그려지는 즉시 하단으로 스크롤되도록 수정
- 임시 메시지가 서버 메시지로 바뀌어도 같은 목록 key를 유지하도록 처리
- 초기 메시지는 진입 애니메이션 대상에서 제외하고 새 메시지만 애니메이션되도록 개선
- 답장 로딩 표시와 실제 답장 전환을 바닥 스크롤 판정에 반영
- 새 메시지 등장을 그래픽 레이어 기반 슬라이드·페이드 애니메이션으로 변경
- 기존 메시지 재배치 애니메이션을 제거해 목록 전체 움직임을 줄임
- 하단 이동을 즉시 스크롤로 바꿔 등장 애니메이션과의 프레임 경합을 완화
- 전송 실패한 임시 말풍선을 제거하고 보낸 내용을 입력칸에 되돌리도록 변경
- 실패 메시지 재전송 UI와 관련 상태·콜백을 제거
- 입력값 외부 변경 시 커서를 끝으로 보정하도록 입력바 상태 처리 개선
- displayMessages lazy 초기화를 Compose 메인 스레드 전용으로 명시하고 LazyThreadSafetyMode.NONE 적용
- 치명적 위험 감지 시 전송 중단 후 말풍선/입력값이 원복되는지 검증하는 테스트 추가
- 테스트용 RiskLexiconRepository를 주입 가능하도록 FixedRiskLexiconRepository 및 파라미터 추가
- 메시지 애니메이션 상태를 채팅 목록과 스크롤 상태가 공유하도록 이동
- 바닥을 따라가지 않는 새 메시지를 미리 확인 처리해 뒤늦게 보여도 애니메이션되지 않도록 개선
- 입력값이 외부에서 복구될 때 커서와 한글 조합 상태가 이전 전송 값으로 되살아나지 않도록 동기화 로직을 분리
- 전송 차단·실패로 임시 말풍선이 제거되는 경우 새 메시지 처리와 스크롤 애니메이션 기록을 건너뛰도록 조정
- 로딩 표시가 아닌 실제 최신 답장을 기준으로 토스트 상태를 갱신
- 이미 화면에 보이는 답장은 안내하지 않도록 가시성 판단 유지
@seunghee17 seunghee17 changed the title fix: 채팅방 메시지 애니메이션 성능 개선 및 전송 UX 개선 [GMSS-300] 채팅방 메시지 애니메이션 성능 개선 및 전송 UX 개선 Sep 29, 2026
@seunghee17
seunghee17 marked this pull request as ready for review September 29, 2026 03:38
Comment thread feature/chat/src/main/java/com/gamss/android/feature/chat/ChatRoomViewModel.kt Outdated
Comment thread feature/chat/src/main/java/com/gamss/android/feature/chat/util/ChatScrollState.kt Outdated
- 위험 신호 검사가 끝나기 전에는 낙관적 말풍선을 그리지 않아 CRITICAL 문구가 화면에 노출되는 문제 방지
- 검사 대기 중 사용자가 입력을 바꾼 경우 검사 통과 후에도 입력칸을 덮어쓰지 않도록 처리
- 말풍선 제거 판정을 목록 길이 비교 대신 직전 바닥 아이템의 key 존재 여부로 변경해 안정성 확보
- 화면 밖에서 도착한 메시지를 미리 "본 메시지"로 기록하는 markAlreadySeen 추가
- 전송 응답 대기 중 입력이 바뀐 경우 실패 처리에서 입력칸을 덮어쓰지 않도록 검증
- 테스트 저장소에 전송 응답 지연 장치를 추가해 입력 경합 상황을 재현
- 채팅 메시지 애니메이션의 중복 표시 처리 API 주석을 실제 함수명에 맞게 정리
- 채팅방 테스트가 `RiskLexiconRepository`를 직접 주입하도록 정리
- 경고 용어 렉시콘 저장소 생성을 공통 헬퍼로 분리
- 테스트용 ViewModel 팩토리의 불필요한 `RiskLexicon` 매개변수 제거
@soyeonLee126
soyeonLee126 merged commit f31ac66 into develop Sep 30, 2026
6 checks passed
@soyeonLee126
soyeonLee126 deleted the fix/chatting-room branch September 30, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants