즐겨찾기 값을 바꿔도 배열에서는 움직이지 않는다

보이스 화면이 즐겨찾기 목록과 일반 목록을 따로 조회하면, 한 항목의 isFavorite만 바꾸는 낙관적 갱신으로는 부족하다. 일반 목록의 배열 안에 남은 항목은 서버 재조회가 끝나야 사라지고, 즐겨찾기 목록에는 그때까지 나타나지 않는다. 화면을 빠르게 바꾸려면 값뿐 아니라 어느 목록에 속하는지도 함께 바꿔야 한다.

AI Commons에서는 쿼리 키의 favoritesOnly와 excludeFavorites를 읽어 갱신을 나눴다. 후속 보완까지 적용한 동작은 다음과 같다.

캐시의 목록 조건에 따른 갱신
목록 조건즐겨찾기 추가즐겨찾기 해제
즐겨찾기만없으면 삽입하고 total 증가있으면 제거하고 total 감소
즐겨찾기 제외있으면 제거하고 total 감소없으면 삽입하고 total 증가
두 조건 없음기존 항목의 플래그 변경기존 항목의 플래그 변경

일반 페이지 목록, 무한 목록, 별도 즐겨찾기 캐시가 같은 항목을 담을 수 있으므로 한 화면의 배열만 수정하는 방식으로는 맞출 수 없다. 변경은 관련 쿼리를 취소하고 각 캐시의 이전 값을 보관한 뒤, 목록의 성격에 따라 갱신한다.

첫 목록에서 지운 뒤에는 다른 목록에 넣을 원본이 없을 수 있다

캐시를 순서대로 수정하면서 매번 항목을 다시 찾으면 앞선 갱신이 다음 갱신의 입력을 지울 수 있다. 일반 목록에서 먼저 제거한 뒤 즐겨찾기 캐시에 넣으려는데, 전체 보이스 객체를 찾을 곳이 없는 경우다. 따라서 롤백용 목록 스냅샷과 별도로 이동할 객체도 변경 전에 확보한다.

const currentVoice = findVoiceInAnyCaches(qc, voiceId);
const newIsFavorite = currentVoice ? !currentVoice.isFavorite : true;
const voiceDataSnapshot = currentVoice;

다른 캐시에 넣는 항목은 이 원본에서 새 플래그를 적용해 만든다. 서버 요청이 실패하면 이전 캐시 스냅샷을 복원하고, 요청이 끝나면 관련 쿼리를 무효화해 서버 데이터를 다시 받는다. 다만 스냅샷 롤백이 겹치는 두 변경을 안전하게 합쳐 주지는 않는다. 별도 직렬화나 버전 비교가 없으므로 먼저 시작한 요청의 실패가 뒤 요청의 갱신까지 되돌릴 여지는 남는다.

빠른 이동의 순서는 서버 정렬을 근사한다

첫 수정은 새 즐겨찾기를 맨 앞에 넣었고, 후속 수정은 쿼리의 sort 조건으로 삽입 위치를 찾도록 바꿨다. 최신순은 updatedAt 내림차순, 나이순은 문자열 순서가 아닌 young·middle_aged·old의 의미 순서다. 이런 규칙을 브라우저에도 두면 즉시 이동한 항목의 위치를 서버 응답에 더 가깝게 만들 수 있다.

그 대가는 정렬 규칙의 중복 관리다. 문자열의 localeCompare가 DB의 정렬 규칙과 항상 같지는 않다. 무한 목록은 첫 캐시 페이지 안의 위치만 계산하므로 여러 페이지 전체의 정확한 위치도 보장하지 않는다. 페이지별 total이 잠시 어긋날 수 있고, 삽입 전에 언어 등 나머지 필터 조건을 모두 다시 검사하는 구현도 아니다. 서버 재조회는 여전히 최종 보정 단계다.

회귀 테스트 코드에는 두 분리 목록의 동시 갱신과 오류 롤백, 이름·최신·성별 삽입 위치가 있다. 비교 함수 테스트는 정렬 방향과 빈 배열·앞·중간·끝·동률의 삽입을 다룬다. 이 단위 검사로 실제 DB 정렬이나 모든 무한 페이지의 일치까지 입증할 수는 없다. 이 수정이 다룬 단위는 “항목의 플래그”에서 “그 항목을 담는 캐시의 소속과 위치”로 넓어졌다.