0. 왜 처리 속도를 개선해야 하는지?
상대방이 메시지를 이미 읽었는데도 읽음 처리가 늦게 표시되면 사용자는 채팅이 느리거나 실시간 연결이 제대로 동작하지 않는다고 느낄 수 있겠죠...ㅠㅠ
기존 방식은 수신자가 메시지를 본 뒤 읽음 요청을 보내기까지 무조건 300ms를 기다렸습니다.
상대방이 메시지를 읽음
→ 300ms 대기
→ 서버에 마지막으로 읽은 메시지 위치 전송
→ 읽은 인원 조회
→ 읽음 처리 표시
이 300ms는 네트워크나 서버가 느려서 발생하는 시간이 아니라, 클라이언트가 요청을 모으기 위해 일부러 기다리는 시간이었어요.
따라서 네트워크 상태가 좋아도 읽음 표시가 바로 갱신되지 않았습니다.
그렇다고 대기 시간을 완전히 없애면 스크롤할 때 화면에 나타나는 메시지마다 요청을 보내게 됩니다.
10번 메시지 노출 → 요청
11번 메시지 노출 → 요청
12번 메시지 노출 → 요청
그래서 debounce는 유지하면서 대기 시간만 100ms로 줄였습니다. 100ms 안에 여러 메시지가 보이면 가장 큰 sequence 하나만 전송해 요청 수를 줄이고, 기존보다 약 200ms 빠르게 읽음 처리를 시작할 수 있다고 하네요...!!
한번 들어가볼까요..
1. 작업 내용
채팅에서 상대방이 메시지를 읽은 뒤 발신자 화면의 Read by N이 갱신되기까지 걸리는 시간을 줄였습니다.
기존 클라이언트는 읽은 메시지의 위치를 서버로 보내기 전에 항상 300ms 를 기다렸습니다. 이번 작업에서는 여러 요청을 하나로 합치는 기존 동작은 유지하면서, 기본 대기 시간을 100ms 로 줄였습니다.
구분
변경 전
변경 후
읽음 위치 debounce
300ms
100ms
100ms 안에 여러 메시지를 읽은 경우
가장 큰 sequence만 전송
기존 동작 유지
요청 중 더 큰 sequence를 읽은 경우
다음 요청으로 병합
기존 동작 유지
요청 실패 후 복구
focus·재연결 시 재시도
기존 동작 유지
작업 과정
useReadPosition의 기본 debounce를 300ms에서 100ms로 변경했습니다.
fake timer 테스트를 100ms 기준으로 수정했습니다.
진행 중 요청 병합과 실패 후 재시도 동작에 회귀가 없는지 확인했습니다.
Note
이번 작업은 클라이언트가 요청을 시작하기 전에 기다리는 시간만 줄였습니다. 서버 API, STOMP payload, 데이터베이스 구조는 변경하지 않았습니다.
검증 결과
2. Deep Dive 해본 것
Read by N이 갱신되는 과정
읽음 표시는 수신자의 화면에서 바로 변경되는 값이 아니에요. 수신자가 메시지를 본 뒤 서버 저장과 실시간 이벤트를 거쳐 발신자의 화면까지 전달돼요.
수신자 화면에 메시지가 50% 이상 보임
↓
100ms 동안 읽음 후보 병합
↓
PUT /read-position 요청
↓
DB의 읽음 위치 전진
↓
커밋 후 익명 STOMP 이벤트
↓
발신자가 read-summary 재조회
↓
Read by N 표시
이 과정에서 300ms debounce는 가장 앞에 붙는 고정 대기 시간이었어요. 이를 100ms로 줄여 네트워크 요청을 기존보다 약 200ms 빠르게 시작할 수 있게 했습니다.
debounce가 필요한 이유
사용자가 스크롤하면 여러 메시지가 짧은 시간 안에 연속으로 화면에 나타날 수 있어요.
10번 메시지 노출
→ 11번 메시지 노출
→ 12번 메시지 노출
각 메시지마다 요청하면 서버에 10, 11, 12를 모두 보내야 해요. debounce를 적용하면 100ms 동안 관찰한 값을 모아 가장 큰 12만 보낼 수 있어요.
방식
장점
단점
즉시 전송
가장 빠르게 요청 시작
스크롤 중 요청이 많이 발생
300ms debounce
요청을 충분히 병합
읽음 표시가 늦게 갱신
100ms debounce
요청 병합을 유지하면서 대기 감소
즉시 전송보다는 100ms 늦음
Important
이번 변경의 핵심은 debounce를 없애는 것이 아니라, 요청 병합 효과를 유지할 수 있는 범위에서 고정 대기 시간을 줄이는 것입니다.
읽음 위치를 관리하는 세 가지 상태
useReadPosition은 최신 읽음 위치가 누락되거나 과거 값으로 돌아가지 않도록 세 가지 상태를 사용해요.
상태
의미
desired
클라이언트가 지금까지 관찰한 가장 큰 sequence
acknowledged
서버가 저장했다고 응답한 가장 큰 sequence
inFlight
읽음 위치 요청이 진행 중인지 여부
예를 들어 sequence=12를 전송하는 중에 사용자가 sequence=15까지 읽으면 다음과 같이 처리해요.
12 전송 시작
→ 요청 처리 중 15 관찰
→ desired에 15 보관
→ 12 전송 완료
→ acknowledged는 12
→ desired가 더 크므로 15 전송
진행 중인 요청을 중복 실행하지 않으면서, 그사이에 읽은 최신 메시지도 놓치지 않는 구조예요.
실패한 읽음 위치를 버리지 않는 이유
네트워크 문제로 요청이 실패했을 때 desired를 제거하면 사용자가 실제로 읽은 위치가 사라질 수 있어요. 그래서 실패한 최대 sequence를 메모리에 남기고 다음 시점에 다시 시도해요.
브라우저 창이 다시 focus됐을 때
숨겨진 페이지가 다시 visible 상태가 됐을 때
STOMP 연결이 복구됐을 때
새로고침처럼 메모리 상태가 사라지는 경우에는 서버에 저장된 값을 기준으로 읽음 summary를 다시 조회해요.
클라이언트와 서버가 나눠서 보장하는 것
클라이언트의 debounce만으로 읽음 위치의 정확성이 보장되지는 않아요.
클라이언트 는 짧은 시간 동안 읽은 메시지를 가장 큰 sequence 하나로 합쳐요.
서버 는 기존 값보다 큰 sequence만 저장해 읽음 위치가 뒤로 가지 않도록 해요.
데이터베이스 커밋 후 익명 STOMP 이벤트를 발행해요.
발신자 클라이언트 는 이벤트를 무효화 신호로 사용해 실제 읽음 인원을 REST로 다시 조회해요.
3. 알게된 것
작은 고정 지연도 사용자 경험에 영향을 준다
이번 작업은 네트워크나 서버 처리 속도를 개선한 것은 아니었습니다. 요청 전에 붙어 있던 고정 대기 시간을 200ms 줄이는 작업이었습니다.
하지만 읽음 표시는 여러 단계를 거쳐 갱신되므로, 첫 단계의 작은 지연도 전체 체감속도에 직접 영향을 준다는 것을 알게 됐습니다.
debounce는 단순한 지연이 아니다
처음에는 debounce를 요청을 늦추는 기능으로만 생각했습니다. 실제로는 짧은 시간 안에 반복되는 입력을 하나의 최신 값으로 합쳐 요청 수를 줄이는 역할도 한다는 것을 알게 됐습니다.
따라서 시간을 줄일 때는 다음 동작이 유지되는지 함께 확인해야 해요.
여러 입력 중 최댓값 선택
마지막 입력을 기준으로 타이머 재시작
진행 중인 요청과 다음 요청 분리
실패한 값 보존 및 재시도
실시간 이벤트가 항상 최종 데이터일 필요는 없다
읽음 위치 STOMP 이벤트는 완성된 readCount를 전달하지 않아요. 읽음 상태가 변경됐다는 사실만 알리고, 실제 값은 권한이 있는 발신자가 REST로 다시 조회해요.
이 구조를 사용하면 이벤트 payload를 단순하게 유지하고 읽은 사용자의 신원을 노출하지 않으면서, 이벤트가 유실돼도 새로고침이나 재연결 시 DB 값을 기준으로 복구할 수 있어요.
시간 기반 테스트는 경계를 나눠야 한다
단순히 “나중에 요청이 전송된다”만 테스트하면 기본 debounce가 정확히 100ms인지 보장하기 어려워요.
99ms → 요청 없음
100ms → 요청 1회
경계 직전과 경계를 나눠 검증하면 debounce 시간이 다시 늘어나거나, 의도치 않게 즉시 전송되는 회귀를 막을 수 있다는 것을 알게 됐습니다.
번외로..
Important
가장 최적의 시간대를 어떻게 검증할 수 있을까 그럼?
테스트 상황:
메시지 하나만 읽기
천천히 스크롤하기
빠르게 스크롤하기
계속 스크롤하기
느린 네트워크
연결 실패 후 재연결
등의 환경에서 디바운스 대기 시간을 다르게 가져가면서 테스트 해보고!
읽음 위치 누락과 역행이 없는 값만 남깁니다.
서버가 감당할 수 있는 요청 수 기준을 만족하는 값만 남깁니다.
남은 값 중 전체 체감 지연이 가장 짧은 값을 선택합니다.
debounce를 더 줄여도 속도 차이는 거의 없고 요청만 늘어난다면 그 직전 값을 선택합니다.
다음과 같은 순서를 통해 맞춰나가면 된다고 합니다~~!!
예를 들어 100ms에서 50ms로 줄였을 때 읽음 표시는 10ms만 빨라졌지만 요청 수가 두 배가 된다면, 100ms가 더 적절한 선택 ㅇㅇ
0. 왜 처리 속도를 개선해야 하는지?
상대방이 메시지를 이미 읽었는데도 읽음 처리가 늦게 표시되면 사용자는 채팅이 느리거나 실시간 연결이 제대로 동작하지 않는다고 느낄 수 있겠죠...ㅠㅠ
기존 방식은 수신자가 메시지를 본 뒤 읽음 요청을 보내기까지 무조건 300ms를 기다렸습니다.
상대방이 메시지를 읽음
→ 300ms 대기
→ 서버에 마지막으로 읽은 메시지 위치 전송
→ 읽은 인원 조회
→ 읽음 처리 표시
이 300ms는 네트워크나 서버가 느려서 발생하는 시간이 아니라, 클라이언트가 요청을 모으기 위해 일부러 기다리는 시간이었어요.
따라서 네트워크 상태가 좋아도 읽음 표시가 바로 갱신되지 않았습니다.
그렇다고 대기 시간을 완전히 없애면 스크롤할 때 화면에 나타나는 메시지마다 요청을 보내게 됩니다.
10번 메시지 노출 → 요청
11번 메시지 노출 → 요청
12번 메시지 노출 → 요청
그래서 debounce는 유지하면서 대기 시간만 100ms로 줄였습니다. 100ms 안에 여러 메시지가 보이면 가장 큰 sequence 하나만 전송해 요청 수를 줄이고, 기존보다 약 200ms 빠르게 읽음 처리를 시작할 수 있다고 하네요...!!
한번 들어가볼까요..
1. 작업 내용
채팅에서 상대방이 메시지를 읽은 뒤 발신자 화면의
Read by N이 갱신되기까지 걸리는 시간을 줄였습니다.기존 클라이언트는 읽은 메시지의 위치를 서버로 보내기 전에 항상 300ms를 기다렸습니다. 이번 작업에서는 여러 요청을 하나로 합치는 기존 동작은 유지하면서, 기본 대기 시간을 100ms로 줄였습니다.
sequence만 전송sequence를 읽은 경우작업 과정
useReadPosition의 기본 debounce를 300ms에서 100ms로 변경했습니다.Note
이번 작업은 클라이언트가 요청을 시작하기 전에 기다리는 시간만 줄였습니다. 서버 API, STOMP payload, 데이터베이스 구조는 변경하지 않았습니다.
검증 결과
sequence만 전송함sequence를 다음 요청으로 전송함2. Deep Dive 해본 것
Read by N이 갱신되는 과정읽음 표시는 수신자의 화면에서 바로 변경되는 값이 아니에요. 수신자가 메시지를 본 뒤 서버 저장과 실시간 이벤트를 거쳐 발신자의 화면까지 전달돼요.
이 과정에서 300ms debounce는 가장 앞에 붙는 고정 대기 시간이었어요. 이를 100ms로 줄여 네트워크 요청을 기존보다 약 200ms 빠르게 시작할 수 있게 했습니다.
debounce가 필요한 이유
사용자가 스크롤하면 여러 메시지가 짧은 시간 안에 연속으로 화면에 나타날 수 있어요.
각 메시지마다 요청하면 서버에
10,11,12를 모두 보내야 해요. debounce를 적용하면 100ms 동안 관찰한 값을 모아 가장 큰12만 보낼 수 있어요.Important
이번 변경의 핵심은 debounce를 없애는 것이 아니라, 요청 병합 효과를 유지할 수 있는 범위에서 고정 대기 시간을 줄이는 것입니다.
읽음 위치를 관리하는 세 가지 상태
useReadPosition은 최신 읽음 위치가 누락되거나 과거 값으로 돌아가지 않도록 세 가지 상태를 사용해요.desiredsequenceacknowledgedsequenceinFlight예를 들어
sequence=12를 전송하는 중에 사용자가sequence=15까지 읽으면 다음과 같이 처리해요.진행 중인 요청을 중복 실행하지 않으면서, 그사이에 읽은 최신 메시지도 놓치지 않는 구조예요.
실패한 읽음 위치를 버리지 않는 이유
네트워크 문제로 요청이 실패했을 때
desired를 제거하면 사용자가 실제로 읽은 위치가 사라질 수 있어요. 그래서 실패한 최대 sequence를 메모리에 남기고 다음 시점에 다시 시도해요.새로고침처럼 메모리 상태가 사라지는 경우에는 서버에 저장된 값을 기준으로 읽음 summary를 다시 조회해요.
클라이언트와 서버가 나눠서 보장하는 것
클라이언트의 debounce만으로 읽음 위치의 정확성이 보장되지는 않아요.
sequence하나로 합쳐요.sequence만 저장해 읽음 위치가 뒤로 가지 않도록 해요.3. 알게된 것
작은 고정 지연도 사용자 경험에 영향을 준다
이번 작업은 네트워크나 서버 처리 속도를 개선한 것은 아니었습니다. 요청 전에 붙어 있던 고정 대기 시간을 200ms 줄이는 작업이었습니다.
하지만 읽음 표시는 여러 단계를 거쳐 갱신되므로, 첫 단계의 작은 지연도 전체 체감속도에 직접 영향을 준다는 것을 알게 됐습니다.
debounce는 단순한 지연이 아니다
처음에는 debounce를 요청을 늦추는 기능으로만 생각했습니다. 실제로는 짧은 시간 안에 반복되는 입력을 하나의 최신 값으로 합쳐 요청 수를 줄이는 역할도 한다는 것을 알게 됐습니다.
따라서 시간을 줄일 때는 다음 동작이 유지되는지 함께 확인해야 해요.
실시간 이벤트가 항상 최종 데이터일 필요는 없다
읽음 위치 STOMP 이벤트는 완성된
readCount를 전달하지 않아요. 읽음 상태가 변경됐다는 사실만 알리고, 실제 값은 권한이 있는 발신자가 REST로 다시 조회해요.이 구조를 사용하면 이벤트 payload를 단순하게 유지하고 읽은 사용자의 신원을 노출하지 않으면서, 이벤트가 유실돼도 새로고침이나 재연결 시 DB 값을 기준으로 복구할 수 있어요.
시간 기반 테스트는 경계를 나눠야 한다
단순히 “나중에 요청이 전송된다”만 테스트하면 기본 debounce가 정확히 100ms인지 보장하기 어려워요.
경계 직전과 경계를 나눠 검증하면 debounce 시간이 다시 늘어나거나, 의도치 않게 즉시 전송되는 회귀를 막을 수 있다는 것을 알게 됐습니다.
번외로..
Important
가장 최적의 시간대를 어떻게 검증할 수 있을까 그럼?
등의 환경에서 디바운스 대기 시간을 다르게 가져가면서 테스트 해보고!
다음과 같은 순서를 통해 맞춰나가면 된다고 합니다~~!!