❗문제상황
프로젝트를 진행하면서 사용자 경험을 개선하면 좋을 것 같다는 의견을 받을 부분이 있었다.
프로젝트에서 모임의 모집여부를 변경할 때 선택 모달을 띄워서 변경할 수 있게 하는데 네트워크 환경에 따라 로딩이 생기는 경우 별도의 처리가 없어서 로딩 중인지, 선택이 안된 건지 사용자가 알 수 없고 로딩 동안 여러 번 누를 수 있어서 서버에 불필요한 api가 호출될 수 있었다.

따라서 이 문제를 해결하기 위해 2가지의 해결방법이 떠올랐다.
1. 모달에서 모집 여부를 변경한 경우 로딩 동안 '닫기' 버튼을 스피너로 변경하고 로딩동안 버튼을 클릭 할 수 없도록 비활성화하는 방법
2. 버튼을 클릭하면 로딩 여부와 상관없이 ui를 업데이트하는 낙관적 업데이트를 적용
이렇게 2가지의 방법이 있었는데 로딩 시간 동안 모달을 계속 띄워놓는 게 어색한 느낌이고 시간이 길어질수록 사용자 경험을 헤칠 것 같아서 2번 방법으로 낙관적 업데이트를 진행하기로 하였다. 낙관적 업데이트를 적용하여 로딩이 생겨도 사용자에게 즉각적인 ui 변화를 줄 수 있어서 사용자 경험을 향상할 수 있을 것 같았다.
👩💻낙관적 업데이트 사용해보기
tanstack query를 이용하면 onMutate를 통해서 낙관적 업데이트를 적용할 수 있다.

간단하게 번역해보면 onMutate함수는 mutation 함수가 받는 변수와 같은 변수를 전달받고, 성공을 가정하에 낙관적 업데이트를 수행하게 된다. 만약 실패한 경우 onError와 onSettled 함수 모두에게 반환된 값이 전달되며 롤백하는 데 사용할 수 있다.
- 공식문서에서 제공한 예시 코드
const queryClient = useQueryClient()
useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails,
// use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
이렇게 onMutate에서 새로운 데이터를 받아서 적용해 주면 간단하게 낙관적 업데이트를 적용할 수 있다.
👀프로젝트에 낙관적 업데이트 적용하기
const { mutate: updateRecruitmentStatus } = useMutation(
patchReadingClassChange,
{
onMutate: async ({ groupData }) => {
//이전 쿼리 값 저장
const oldData: GroupList | undefined = queryClient.getQueryData([
'recruitDetail',
String(groupId),
]);
//진행중인 refetch 취소
//refetchOnMount 등으로 의도치않은 refetch로
//낙관적 업데이트한 값을 덮어씌우게 하지 않기 위한 처리
await queryClient.cancelQueries(['recruitDetail', String(groupId)]);
//데이터의 타입을 확정하기 위한 타입가드
if ('recruitment_status' in groupData) {
//새롭게 업데이트한 값으로 쿼리 값을 업데이트
queryClient.setQueryData(['recruitDetail', String(groupId)], {
...oldData,
recruitment_status: groupData.recruitment_status,
});
}
//모달 닫힘 처리
setModal(false);
//실패한 경우 이전 데이터로 다시 쿼리 값을 롤백처리
return () =>
queryClient.setQueryData(['recruitDetail', String(groupId)], oldData);
},
onError: (error, variable, rollback) => {
//만약 실패한 경우 전달받은 롤백함수를 실행
if (rollback) return rollback();
},
},
);
✅낙관적 업데이트 적용 결과
- 낙관적 업데이트 적용 이전

적용 이전에는 로딩 동안 모달이 띄워져 있고 수정이 성공한 후에도 다시 데이터를 get 해오는 동안 ui가 업데이트되지 않았다.
- 낙관적 업데이트 적용 이후

낙관적 업데이트를 적용한 이후에는 아직 로딩 중이어도 바로 ui가 업데이트되는 것을 볼 수 있다!
처음 낙관적 업데이트를 사용해 보았는데 즉각적인 반응이 필요한 경우 사용자 경험을 향상하는데 좋은 방법인 것 같다!
onMutate를 통해서 간단하게 적용할 수 있는 것도 너무 좋았다!
'개발 공부 > React' 카테고리의 다른 글
[nextJS] 성능 최적화 하기(lighthouse, 빌드 용량 개선) (0) | 2023.07.25 |
---|---|
[nextJS]일정 시간 이상 걸리는 페이지 이동만 로딩 처리 (0) | 2023.07.23 |
무한스크롤 스크롤 위치 기억하기 (0) | 2023.07.10 |
color-thief 라이브러리 type 지정하기 (0) | 2023.07.02 |
[프로젝트 고민]input 불필요한 렌더링 줄이기 (0) | 2023.04.15 |
❗문제상황
프로젝트를 진행하면서 사용자 경험을 개선하면 좋을 것 같다는 의견을 받을 부분이 있었다.
프로젝트에서 모임의 모집여부를 변경할 때 선택 모달을 띄워서 변경할 수 있게 하는데 네트워크 환경에 따라 로딩이 생기는 경우 별도의 처리가 없어서 로딩 중인지, 선택이 안된 건지 사용자가 알 수 없고 로딩 동안 여러 번 누를 수 있어서 서버에 불필요한 api가 호출될 수 있었다.

따라서 이 문제를 해결하기 위해 2가지의 해결방법이 떠올랐다.
1. 모달에서 모집 여부를 변경한 경우 로딩 동안 '닫기' 버튼을 스피너로 변경하고 로딩동안 버튼을 클릭 할 수 없도록 비활성화하는 방법
2. 버튼을 클릭하면 로딩 여부와 상관없이 ui를 업데이트하는 낙관적 업데이트를 적용
이렇게 2가지의 방법이 있었는데 로딩 시간 동안 모달을 계속 띄워놓는 게 어색한 느낌이고 시간이 길어질수록 사용자 경험을 헤칠 것 같아서 2번 방법으로 낙관적 업데이트를 진행하기로 하였다. 낙관적 업데이트를 적용하여 로딩이 생겨도 사용자에게 즉각적인 ui 변화를 줄 수 있어서 사용자 경험을 향상할 수 있을 것 같았다.
👩💻낙관적 업데이트 사용해보기
tanstack query를 이용하면 onMutate를 통해서 낙관적 업데이트를 적용할 수 있다.

간단하게 번역해보면 onMutate함수는 mutation 함수가 받는 변수와 같은 변수를 전달받고, 성공을 가정하에 낙관적 업데이트를 수행하게 된다. 만약 실패한 경우 onError와 onSettled 함수 모두에게 반환된 값이 전달되며 롤백하는 데 사용할 수 있다.
- 공식문서에서 제공한 예시 코드
const queryClient = useQueryClient()
useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails,
// use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
이렇게 onMutate에서 새로운 데이터를 받아서 적용해 주면 간단하게 낙관적 업데이트를 적용할 수 있다.
👀프로젝트에 낙관적 업데이트 적용하기
const { mutate: updateRecruitmentStatus } = useMutation(
patchReadingClassChange,
{
onMutate: async ({ groupData }) => {
//이전 쿼리 값 저장
const oldData: GroupList | undefined = queryClient.getQueryData([
'recruitDetail',
String(groupId),
]);
//진행중인 refetch 취소
//refetchOnMount 등으로 의도치않은 refetch로
//낙관적 업데이트한 값을 덮어씌우게 하지 않기 위한 처리
await queryClient.cancelQueries(['recruitDetail', String(groupId)]);
//데이터의 타입을 확정하기 위한 타입가드
if ('recruitment_status' in groupData) {
//새롭게 업데이트한 값으로 쿼리 값을 업데이트
queryClient.setQueryData(['recruitDetail', String(groupId)], {
...oldData,
recruitment_status: groupData.recruitment_status,
});
}
//모달 닫힘 처리
setModal(false);
//실패한 경우 이전 데이터로 다시 쿼리 값을 롤백처리
return () =>
queryClient.setQueryData(['recruitDetail', String(groupId)], oldData);
},
onError: (error, variable, rollback) => {
//만약 실패한 경우 전달받은 롤백함수를 실행
if (rollback) return rollback();
},
},
);
✅낙관적 업데이트 적용 결과
- 낙관적 업데이트 적용 이전

적용 이전에는 로딩 동안 모달이 띄워져 있고 수정이 성공한 후에도 다시 데이터를 get 해오는 동안 ui가 업데이트되지 않았다.
- 낙관적 업데이트 적용 이후

낙관적 업데이트를 적용한 이후에는 아직 로딩 중이어도 바로 ui가 업데이트되는 것을 볼 수 있다!
처음 낙관적 업데이트를 사용해 보았는데 즉각적인 반응이 필요한 경우 사용자 경험을 향상하는데 좋은 방법인 것 같다!
onMutate를 통해서 간단하게 적용할 수 있는 것도 너무 좋았다!
'개발 공부 > React' 카테고리의 다른 글
[nextJS] 성능 최적화 하기(lighthouse, 빌드 용량 개선) (0) | 2023.07.25 |
---|---|
[nextJS]일정 시간 이상 걸리는 페이지 이동만 로딩 처리 (0) | 2023.07.23 |
무한스크롤 스크롤 위치 기억하기 (0) | 2023.07.10 |
color-thief 라이브러리 type 지정하기 (0) | 2023.07.02 |
[프로젝트 고민]input 불필요한 렌더링 줄이기 (0) | 2023.04.15 |