UIUX

로딩스피너, Optimistic UI 관점으로 바라보기, React Query

남희정 2023. 9. 7. 18:49

아무리 JavaScript 파일을 빠르게 들고온대도 서버에서 리소스를 가져오는 시간은 어쩔 수 없다. 로딩이 길어질수록 UX는 부정적으로 될 것이고 이탈자도 생길 것이다. 그리하여.. Weather API로 받아오는 시간이 꽤 걸리는 것 같아 로딩 스피너를 추가하게 되었다! 하지만 최선으로 구현했는지에 대해 의문이 남았다. Optimistic UI, React Query를 공부하면서 어떤 식으로 개선하는 게 효과적일지 생각해보아야겠다.


 

로딩스피너 테스트 영상

Roading Spinner

웹사이트/앱을 사용하는 동안 우리 모두가 직면할 수 있는 일반적인 상황은 진행 상태, 혹은 무언가가 로드될 때까지 기다리는 것이다. 이런 상황 중에 가장 간단한 해결책은 Loader나 Spinner를 사용하여 백그라운드에서 무언가가 로드되고 있음을 사용자에게 나타내주는 것이다. 

자주 접하는 요소라 친숙하니 더 이상의 설명은 패스하겠다.

 

Optimistic UI (Optimistic Update)

서버에서 응답이 오지 않더라도 UI 로딩을 없애고 사용자의 행동에 즉각적인 반응을 제공하는 디자인패턴이다. UX를 개선하기 위해 구현한 간단한 Trick이다.

 

기존 UI 접근 방식

사용자가 작업(양식 제출 또는 설정 업데이트)을 수행하면 애플리케이션은 서버가 요청을 처리할 때까지 기다린 다음 서버의 응답에 따라 UI를 업데이트한다. ➡️ 사용자 경험이 지연되고 느려질 수 있다.

 

낙관적 UI 접근 방식에서 애플리케이션은 작업이 성공할 것이라고 가정하고 예상 결과를 반영하도록 UI를 즉시 업데이트 한다. 예로, 사용자가 항목을 추가하기 위해 양식을 제출하면 애플리케이션은 서버의 확인을 기다리지 않고 항목을 즉시 추가할 수 있다. 서버가 결국 작업을 성공하여 확인하면, 사용자에겐 눈에 띄는 지연이 없다.

 

예로, 메시지를 제출하면 UI에 즉시 표시되는 채팅 앱과 돈을 로드하거나 보내는 지갑 앱에서 로딩 스피너를 표시하지 않고 UI의 잔액을 즉시 변경할 수 있는 경우에서 찾을 수 있다. 그리고 커머스에서 주문/결제시 결제가 완료되지 않았더라도 주문완료 페이지를 랜딩하는 기법도 자주 쓰인다고 함. 결제 실패시엔 따로 개인 연락으로 안내가 되는 방식.

 

이게 바로 로딩 스피너, 막대, 또는 어떤 종류의 로딩 UI도 없이 인터페이스와의 상호 작용에 대한 즉각적인 응답을 얻을 수 있는 낙관적 UI이다.

 

Optimistic UI

이것을 적용하기 위해선 전제 조건이 있다. 서버에서 실패할 경우가 매우 희귀해야한다는 것. 그렇지 않다면 Optimistic UI를 구현하는 것 자체가 큰 비용일 수 있다.

 

구현 방법

UI는 백그라운드 요청이 성공적으로 완료될 때까지 가짜이지만 올바른 데이터를 보유한다. 이는 사용자가 UI와 상호작용한 직후(버튼 클릭 또는 데이터 제출)와 데이터베이스의 실제 데이터를 변경하라는 요청이 이루어지기 전이나 요청 사이에 데이터를 검증하고 계산함으로써 완료된다.

 

이는 우리가 서버의 응답에 대해 낙관적이라는 것을 의미한다. 그래서 우리는 예측한 데이터로 UI를 업데이트하여 사용자의 행동에 대한 답변을 즉시 제공한다.

 

백그라운드 요청이 실패하면 어떻게 되는가?

굉장히 드문 경우일 것이다. 대부분의 경우 테스트되지 않은 코드를 프로덕션에 푸시하지 않을 것이기 때문. 그러나 만약 일어났다면 오류는 가장 효율적인 방법으로 처리되어야 하며 사용자에게 실패에 대해 알려야 하며 UI를 이전 상태로 다시 설정해야 한다. (카카오톡, 이메일, 문자 등 여러 알림 방식으로 신속한 대응을 해야될듯)

 

장, 단점

👍 향상된 UX 

낙관적 UI의 가장 큰 장점은 사용자에게 보다 반응성이 뛰어나고 원활한 경험을 제공한다는 점이다. 상호 작용이 더 빠르고 자연스러워져 사용자 만족도가 높아진다.

 

👍 인지되는 지연 시간 감소

사용자 동작 직후 UI가 업데이트 되므로 사용자는 자신의 작업과 애플리케이션 응답 사이의 지연이 줄어든다고 느낀다. 따라서 빠른 성능에 대한 인식을 줄 수 있다.

 

👍 참여도

인터페이스의 유동성으로 인해 사용자가 더 즐겁게 사용할 수 있고 애플리케이션과 더 자주 상호 작용하도록 장려할 수 있었다.

 

👍 오프라인 및 낮은 연결성 지원

낙관적 UI는 연결이 끊기거나 좋지 않아도 잘 작동할 수 있다. 사용자는 서버와의 연결이 일시적으로 끊긴 경우에도 애플리케이션과 계속 상호 작용할 수 있으며, 연결이 복원되면 변경 사항이 동기화할 수 있다.

 

👍 방해가 되는 알림 감소

UI에서 이미 성공을 가정하므로 사용자는 방해가 되는 경고나 확인 대화 상자가 더 적게 표시된다. 따라서 워크플로우가 더욱 간소화되고 중단 없이 진행될 수 있다.

 

👎 데이터 일관성

가장 중요한 과제는 클라이언트와 서버 간의 데이터 일관성을 유지하는 것이다. 클라이언트가 성공할 것으로 예상한 동작을 서버가 거부하면 데이터 불일치 또는 충돌이 발생하여 해결해야 할 위험이 있다.

 

👎 오류 처리의 복잡성

애플리케이션이 성공할 것으로 예상한 작업이 실패할 경우를 감지하고 처리해야 하므로 오류 처리가 복잡하다. 신중한 고려와 강력한 오류 처리 매커니즘이 필요하다.

 

👎 UX 저하

UI가 성공을 가정했다가 서버의 거부로 인해 되돌아가는 경우, 사용자는 기대치와 실제 결과의 불일치로 인해 혼란을 겪을 수 있다.

 

👎 복잡한 동기화 로직

성공적인 업데이트 및 롤백을 처리하기 위해 적절한 동기화 로직을 구현하는 것은 어려울 수 있다. 이러한 복잡성은 개발 노력과 버그 발생 가능성을 높인다.

 

👎 네트워크 및 서버 안정성

서버가 작업의 성공을 확인한다는 가정에 의존한다. 네트워크 문제, 서버 오류 또는 기타 안정성 문제가 있는 경우 사용자에게 예기치 않은 동작이나 데이터 불일치가 발생할 수 있다.

 

👎 보안 문제

애플리케이션의 특성에 따라 서버의 적절한 검증 없이 성공을 가정하면 잠재적으로 보안 취약성이나 무단 동작이 발행할 수 있다.

 

Summary

Optimistic UI로 실시간 또는 실시간에 가까운 애플리케이션에서 사용자 경험과 참여도를 크게 향상시킬 수 있다.

하지만 데이터 일관성을 보장하고 사용자에게 혼란을 주거나 오해를 불러일으키지 않도록 신중한 계획, 강력한 오류 처리 및 동기화 메커니즘이 필요하다. Optimistic UI의 접근 방식을 채택할지 여부는 애플리케이션과 사용자요구 사항을 기반으로 결정해야 한다. 

 

React Query

React Query는 API 호출과 관련된 데이터를 캐시하고 관리하는 라이브러리이다. React Query는 Optimistic UI를 지원하고, 데이터를 서버로부터 성공적으로 업데이트 하기 전에도 UI를 업데이트 할 수 있도록 한다. 

 

`onMutate`

React Query에서 낙관적 업데이트를 수행하는 방법은 쿼리 객체의 `onMutate` 옵션을 사용하는 것이다. 

onMutate 함수는 useMutation 훅에서 설정할 수 있는 옵션 중 하나이고, API 호출 전에 실행되는 함수이다.

 이 함수는 최적화된 업데이트를 위해 현재 데이터 캐시를 업데이트하거나, UI를 변경하는 등의 작업을 수행할 수 있다. `onMutate` 함수는 업데이트가 성공적으로 이루어지지 않았을 때 사용할 수 있는 rollback 메커니즘도 제공한다.

오. 매우 편할듯..!

 

 

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'] })
  },
})

🌟 이 코드는 `useMutation` 훅을 사용하여 데이터 업데이트 작업을 수행하고(updateTodo), 해당 작업이 실행될 때 수행할 동작을 정의한다.

 

1️⃣ const queryClient = useQueryClient()

queryClient라는 변수를 생성하고, 이 변수를 사용하여 React Query의 queryClient 인스턴스에 접근한다.

➡️ useQueryClient() 함수는 ReactQuery에서 제공하는 훅 중 하나로, 현재 컴포넌트 내에서 queryClient 인스턴스에 접근하기 위해 사용된다. 이 함수를 호출하면 현재 컴포넌트에 대한 queryClient 인스턴스를 반환한다. 

queryClient는 React Query가 데이터 쿼리와 관련된 작업을 수행하는 데 사용되는 핵심 객체이다. 이 객체를 사용하여 데이터를 가져오고 업데이트하며, 데이터 캐시를 관리하고 다시 불러오기를 트리거할 수 있다.

 

2️⃣ useMutation({ ... })

useMutation 훅을 호출하여 데이터 업데이트 작업을 설정한다.

3️⃣ mutationFn : updateTodo

데이터 업데이트 작업을 수행하는 함수를 나타낸다. 백엔드 API와 상호 작용하여 데이터를 업데이트한다.

4️⃣ onMutate

데이터 업데이트 작업이 시작될 때 호출되는 콜백 함수다. 

  • queryClient.cancelQueries({ queryKey: ['todos'] })
    todos라는 쿼리 키를 가진 모든 이전 요청을 취소한다. 이렇게 하면 Optimistic UI를 덮어쓰지 않도록 한다.
  • queryClient.getQueryData(['todos'])
    todos 쿼리의 이전 데이터를 스냅샷으로 저장한다.
  • queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
    Optimistic UI를 수행하여 todos 쿼리의 데이터에 새로운 항목 newTodo를 추가한다.

5️⃣ onError
데이터 업데이트 작업이 실패할 때 호출되는 콜백 함수이다. 이 함수는 이전 데이터 스냅샷을 사용하여 업데이트를 롤백한다.

6️⃣ onSettled 
데이터 업데이트 작업이 성공 또는 실패하면 호출되는 콜백 함수이다. 이 함수는 todos 쿼리를 무효화하여 데이터를 다시 불러올 것을 트리거한다.

 

Why?

React Query를 Optimistic UI와 연결짓는 이유는 모든 서버의 상태를 처리하기에, React에서 useState를 신경쓸 필요가 없다.

개발자는 API 호출만 하면 되는 것..!

React Query는 내장된 변이 함수, 자동 캐싱 무효화, 오류 처리 기능을 제공하여 프로세스를 간소화한다. 애플리케이션을 개선하고 데이터 업데이트를 수동으로 관리하는 데에 드는 시간과 노력을 절약할 수 있다.

 

하지만 모든 경우에 이 개념이 적합하게 적용되는 게 아니기에, 보안 및 데이터의 일관성 등 여러 가지 이유로 비 낙관적 UI를 사용하는게 나을 수 있음. 선택하기 나름이다 🧑‍🏫...

 

직접 적용하느냐 제공되는 매커니즘으로 편하게 관리하느냐 인듯한데.

직접 구현해보지 않고서는 어떻게 동작하는지 알 수 없어서 좀 아쉽긴 하다.. 기술 공부를 계속 해야함을 느낀다. 

 

🤔
내가 만든 스피너의 경우 Optimistic UI를 적용할 수 있는 방법이 따로 있을까 Skeleton UI라면 모를까
우선적으로 랜덤으로 노출 시키는 건 데이터의 신뢰성을 잃을 것 같다. 변경 없이 우선 width를 임의로 주고
데이터를 받아오고나서 width를 지정해주는 것? 

데이터의 일관성을 성립할 수 없는 API기에 날씨 API와는 별개로 생각해야된다는 것을 깨달았다!! 

width를 임의로 주어서 데이터가 생길시 max-content로 자연스럽게 변경 완료. 

 

 


Optimistic Updates

[React] optimistic update(낙관적 업데이트)

Optimistic UI: Enhancing User Experience in React with React Query