사용자 경험(UX)이라고 함은, 사용자가 서비스를 이용하면서 직/간접적으로 느끼는 종합적인 경험을 말한다. 그 말은~ 디자인이 예쁜 것뿐만 아니라 기능이 제대로 동작을 하는지, 목표 기능까지 도달하는데 복잡하진 않는지 등등이 포함된다는 것! 이번 포스팅에서는 그 중에서도 서버와의 통신, 그러니까 네트워크 로직에 따른 UX를 고려해볼까 한다.
서버와의 통신은 기본적으로 (1) 요청하고 (2) 받는 것으로 이루어진다. 너무 당연한 이야기지만, 여러가지를 고려하다보면 국소적으로 바라보게 되어 요청과 응답의 구분이 모호해질 때가 있다. 항상 명심할 것! 통신은 요청하고 받는 것이다!
용어 활용에서도 알 수 있듯, 여기에서 요청하고 응답받는 주체는 클라이언트(프론트엔드)이다. 서버 입장이라면 요청을 받고 응답을 주겠지만, 나는 어디까지나 FE이니까~ 언급 정도만 하고 넘어가겠다.
프론트엔드에서 백엔드쪽으로 요청을 줄 때, 더 크게는 어느 쪽이든 데이터를 다룰 때 흔히 CRUD라고 하는 큰 구분이 있다. Create, Read, Update, Delete. 데이터를 생성하기 위해 요청을 하거나, 데이터를 얻기 위해 요청하거나, 데이터를 수정하기 위해 요청하거나, 데이터를 삭제하기 위해 요청한다.
이때 성격이 다른 구분이 하나 있다. 그것은 바로 Read. Read는 데이터를 요청하기만 하는데에 비해, 나머지 Create, Update, Delete는 데이터에 영향을 준다. 이 성격을 잘 구분을 해두어야 한다. API로 통신을 할 때면, 데이터를 얻는 요청과 데이터에 변화를 주는 요청으로 나누기 때문. 보통 데이터를 얻는 요청을 Query라 표현하고, 데이터에 변화를 주는 요청을 Mutation이라고 표현한다.
이 두 개를 구분하는 이유는 Side effect를 적절히 관리하기 위함! Mutation은 데이터를 변화시키기 때문에, 데이터를 변화시키지 않고 받는 Query와는 다르게 써야 한다. 그 말은? 구현도 다르게 되어있다는 뜻이 되겠다! 이를 알고 가는 것과 무지한 상태로 다루는 것은 큰 차이가 있다는 것을 좀 더 빨리 깨달았으면 좋았을 것을...
Query와 Mutation은 다르게 써야한다. 다르게 쓴다는 말은 요청하는 순간이 다르다는 말도 포함한다. 페이지가 로드부터 이후 어떤 요청이 오가는지 유스케이스를 따져보자.
API 통신 Usecase
- 페이지 최초 로드: 데이터 요청(Query) -> 데이터 응답 -> 데이터 표현
- 페이지 최초 로드 이후: 데이터 요청(Query) -> 데이터 응답 -> 데이터 표현
- 페이지 최초 로드 이후: 데이터 조작 요청(Mutate) -> 데이터 응답 -> 데이터 표현
상황을 들어 각각에 대한 예시를 들어보자. 현재 Todolist 서비스를 만드는 상황이라고 해보자. 계정이 필요하며, 계정에 대해 할일 목록을 읽고, 수정하고, 삭제할 수 있다.
1번의 경우, 처음으로 할일 목록을 가져오는 것이다.
계정에 로그인한 후, 내 할일 목록 페이지에 들어가면 내 할일 목록을 받아옴.2번의 경우, 나머지 할일 목록을 가져오는 것이다.
할일 목록은 무한스크롤 기능이 구현되어 있어, 끝 목록에 다다르면 다음 페이지를 받아옴.3번의 경우, 할일을 수정/삭제하는 것이다.
할일을 끝냈거나, 할일 내용을 수정하거나, 할일을 삭제함.
내 할일 목록 페이지에 들어온 사람의 목적은? 내 할일 목록을 보기 위해서! 이때의 데이터는 내 할일 목록이 되겠다. 이 데이터는 특별한 이유가 없다면 페이지 최초 로드시에 데이터를 가져올 것이다. 이를 fetch라고 한다.
목록을 불러오고 나면 사용자가 취할 수 있는 행동은 어떤 게 있을까? 데이터를 다시 불러올 수도 있고, 데이터를 조작할 수도 있을 것이다.
데이터를 다시 불러오는 이유는 크게 두 가지의 이유가 있다. 데이터의 오류가 있을 때, 새 데이터를 기대할 때. 이때의 경우엔 서버가 가지고 있는 데이터를 다시 요청하기만 하면 된다. 이 과정을 refetch라고 한다.
데이터를 조작하고 난 후의 흐름을 좀 더 생각해보자. 사용자는 데이터를 조작하고 나면, 예를 들어 할일A의 이름을 할일B로 바꿨다면, 클라이언트는 서버로 변경 요청을 보냈을 것이다. 그리고 서버 쪽에서는 데이터가 정상적으로 변경되었다고 해보자. 그러면 이때, 클라이언트가 가지고 있는 할일A에 대한 데이터와, 서버가 가지고 있는 할일A에 대한 데이터가 같지 않게 된다. 이를 데이터 불일치라고 말하고, 변경이 되지 않은 클라이언트의 할일A 데이터를 Stale한 상태라고 부르게 된다.
헷갈리면 안 되는 것이, 사용자가 할일A 데이터의 이름을 할일B로 바꿨다고 해서 클라이언트가 가지고 있는 데이터가 달라지는 것이 아니다. 사용자는 행동을 취한 것이이고 클라이언트는 서버로 변경 요청을 하는 것뿐이다. 그렇기 때문에 클라이언트가 가지고 있는 데이터를 Stale data라고 부르는 것이다.
이렇게 클라이언트가 Stale한 상태가 되면 서버에 요청해서 새로운 데이터를 받아와야 하는데, 이 과정을 데이터 동기화라고 한다. 데이터 동기화를 하는 과정에서, 이전에 데이터를 요청하는 Query가 존재했을 것이다. 이 Query에게 지금 너가 가지고 있는 데이터는 stale한 데이터야. 라고 알리는 것을 재검증(invalidate)이라고 말한다. 재검증을 당한(?) 쿼리는 다시 값을 가져오게 된다.
앞서 말한 refetch와 비슷해서 헷갈릴 수 있는데, 둘의 영역은 다르다. refetch는 실제로 다시 데이터를 불러오는 동작을 말하고, 재검증은 유효하지 않은 데이터(오래된 데이터)라고 알리는 것이다. 쿼리는 invalidate를 통해 오래된 데이터임을 알게 되면 다시 가져오는 것, 즉 refetch를 진행하는 것이다.
조금 더 생각해보아야 할 것은 데이터를 추가로 불러오는 경우이다. 무한 스크롤 기능이 있는 리스트가 그렇다. 특정 스크롤 위치에 다다르면 다음 데이터를 불러오는 기능인데, 이 기능은 데이터를 조작하지도, 그렇다고 페이지 최초 로드에 가져오는 것도 아니다.
이 무한 스크롤이 가지는 특징 중에 하나가 바로 받아온 데이터를 저장한다는 것이다. 다른 fetch들은 (일반적으로) 서버에서 받아온 데이터를 그대로 이용해서 UI 표시를 해주면 되는데, 이건 받아온 데이터를 따로 저장한 뒤 표시해주어야 한다. 이것이 다른 fetch와 다른 점이다.
그렇다면~ 받아온 데이터를 저장하는 상태가 있어야 한다는 말이다. 그런데 이건, 다른 fetch들은 받아온 데이터를 저장할 일이 없다는 말이 된다! 왜일까?
위에서 언급했듯 Query와 Mutation의 동작은 다르다는 것을 알았다. Query는 데이터를 받아오기만 하고, Mutation은 데이터를 조작하기만 한다. 그런데 이런 생각을 가질 수도 있다. 만약, Query로 Todos라는 데이터를 받아오고, Mutation으로 Todos를 조작한다면, Todos도 바꿔줘야 하는 거 아닐까? Query와 Mutation의 흐름을 아는 사람은 웃긴 말이겠지만, 나는 웃지 못했었다...
이 생각의 기저는 받아온 데이터를 '쓴다'라는 생각에서 나오는 것이다. 당연히 사용자에게 데이터를 보여주기 위해서는 데이터를 받아오고, 사용자가 데이터를 수정을 하면, 내가 받아온 데이터가 있으니 그 데이터를 수정한다는, 받아온 데이터를 쓰겠다라는 것이다. 하지만 이것은, 응용SW의 개념에 가깝고, 웹에서는 받아온 데이터를 조작하는 일은 흔하지 않다!
나는 응용SW의 구조에 익숙해져 있는 사람이라, 자연스럽게 데이터는 저장을 해야한다! 라는 마인드가 탑재되어 있었다카더라,, 당연히 Todos를 바꿔줘야 한다고 생각했다. 그러기 위해선 받아온 데이터를 저장하는 TodosStore가 있어야 한다고 생각했고, 아래와 같이 데이터 흐름이 간다고 생각했다.
내가 생각했던 Todos의 흐름
최초 로딩: Query Data -> TodoStore
이후 변경: TodoStore 변경 -> Mutation
그렇다보니,, 네트워크 로직에서 돌아다니는 데이터와 클라이언트 내에서 서버 데이터의 구분이 모호해졌었다. 직접 받아온 데이터와 보관하는 데이터는 다르게 구분지어야 하고... 그런데 또 동기화는 해야하고... 동기화된 데이터를 다시 받아오고...(?)
여담은 접어두고, Todos의 흐름은 이렇게 되어야 맞다.
옳게된 Todos의 흐름
Query는 서버에서 받아오기만.
Mutation은 서버에 변경 요청만.Mutation 발생시, Query에서는 다시 받아오기.
이게 무슨 말이냐면, 굳이 TodoStore 같은 걸 생성해서 따로 관리해줄 필요가 없다는 것이다. Mutation이 데이터를 조작하면, Query는 서버에 갱신된 데이터를 가져오고, UI는 계속 새로운 데이터를 반영한다. 즉,, Mutation과 Query는 독립적이고, 각자의 역할에만 충실하면 문제가 없다!
무한스크롤 같은 기능은 여기에 Store가 낀 모양새인 것이다. Mutation도 아니겠다. Query -> Store -> UI의 흐름인 것. 여기에서 하는 저장 역시 데이터 조작을 위함이 아닌 UI에 뿌려주기 위한 저장임을 알아두자.
물론, 어느 것이든 그렇듯 케바케다. 저장을 한다는 것은 (1) 이전 데이터가 필요하다거나 (2) 다른 컴포넌트에서도 필요한 데이터일 수도 있다는 것이다. 사용자 관련 데이터(User Auth)를 예시도 들 수 있겠다. 사용자의 접근 권한에 따라 보여줄 데이터가 다르다거나, 매번 서버에 사용자 인증을 요구하는 패킷을 보내지 않아 요청을 최소화할 수 있는 등등..
아무튼 요지는 받은 데이터를 따로 저장할 필요가 있다면 저장하는 건 당연하다는 것! 네트워크 로직의 핵심은 그저 통신을 하는 것뿐이다. 요청하고, 응답받고!
조금 많이 돌아왔긴 했지만, 정리를 해보면 다음과 같다.
Query와 Mutation
- Query와 Mutation은 독립적이며, 서로에게 영향을 끼치지 않는다.
- Query로부터 받아온 데이터를 저장하는 것은 네트워크 로직이 아니라 다른 로직이다.
- Mutation은 수정 요청을 보내는 것으로 역할을 다한 것.
받은 데이터를 따로 저장할 필요가 있다면 저장한다고 말했었다. 사실 이 케이스에 가장 적합한 기능이 바로 캐시(Cache)이다. 요청에 따라 받은 데이터를 따로 저장해두고, 나중에 같은 호출이 있을 때 미리 저장된 데이터를 반환하는 것을 캐싱이라고 한다.
캐싱의 이점은 무엇보다 불필요한 네트워크 통신이 발생하지 않는다는 점과, 사용자에게 빠르게 데이터를 보여줄 수 있다는 점이지만, 이 캐싱의 가장 큰 이슈는 데이터 불일치이겠다. 가지고 있는 데이터가 오래전의 데이터라면 최신의 정보를 보장받지 못할 수 있기 때문이다. 그렇기 때문에 이 캐시의 최신화가 중요한데, React Query에서는 이것을 staleTime과 cacheTime이라는 개념으로 관리하고 있다.
그 전에,, React Query는 주기를 가진다는 것을 알아두면 좋을 것 같다. 일반적으로 쿼리는 한 번 요청으로 끝이 나는데, React Query는 한 번 요청으로 끝나는 것이 아니라, 보여지는 화면의 최신화를 보장하기 위해 데이터 패칭하는 트리거를 설정해놓았다. 브라우저가 포커스를 가졌을 때, 새로운 인스턴스가 마운트될 때, 네트워크가 재연결됐을 때, 아니면 특정 주기마다 패칭을 진행한다. 마지막을 제외한 트리거들은 토글처럼 켜고 끌 수 있지만, 마지막은 staleTime와 cacheTime이라는 개념을 좀 알아야 한다.
캐시 데이터는 stale
이라는 상태와 fresh
라는 상태로 관리가 된다. stale은 위에서 언급했듯 오래된 데이터를 의미하고, fresh는 최신 데이터로 봐도 무방하다는 의미이다. 이때, staleTime이라는 것이 끼어드는데, staleTime
은 이 fresh 상태에서 stale 상태로 변하는데 걸리는 시간을 의미한다.
즉, staleTime이 지나지 않은 데이터는 fresh한 상태이므로 네트워크에서 Fetching을 하지 않고 캐시를 반환하며, staleTime이 지난 데이터는 stale한 상태이므로 네트워크에서 Fetching을 진행해서 새로운 데이터를 반환 및 캐시를 재지정한다.
위의 설명만 들어보면 cacheTime이라는 단어가 더 어울릴 것 같지만, 그렇지 않다. cacheTime
은 해당 쿼리가 비활성화(inactive)된 시점부터 캐시를 유지할 시간을 의미한다. 이게 무슨 소린고,, 하면 다음과 같다. 해당 쿼리가 비활성화가 된다는 소리는, 현재 보여지는 화면에서 해당 쿼리가 쓰이지 않고 있다는 뜻이다. 이때부터 일정 시간까지 다시 해당 쿼리가 쓰이지 않게 된다면 이 캐시는 삭제된다. 이 일정 시간을 cacheTime이라고 한다.
React Query쪽에서도 이 용어에 대해 논의가 좀 있었는지 23월 11월 기준으로 gcTime
이라는 이름으로 바꾸었다^~^ 아마 Garbage Collect의 약자가 아닐까 싶다.
이 두 시간을 어떻게 설정해야 하는지에 대해서 논쟁이 오가긴 하지만, 일반적으로 staleTime은 cacheTime 보다 짧게 설정해야 한다고 말한다.
만약 staleTime이 infinity이고, cacheTime이 0이라고 가정하면, 일단 저장된 캐시가 있다면 그 캐시를 계속 사용할 것이고, 보여지는 화면에서 해당 쿼리를 사용하지 않는 순간 바로 캐시가 삭제될 것이다. 그렇기 때문에 staleTime을 infinity로 설정했으므로 캐시가 계속 살아있을 것처럼 기대됐지만, 쿼리가 다시 사용되기 시작한다면 네트워크 fetching이 일어나게 된다. 이는 staleTime에 기대하는 역할을 벗어났기 때문에~ 의미가 없다고 보는 것.
그러므로,, staleTime은 cacheTime보다 짧게 지정해줘야 합리적이라는 것이다.
내 생각으론 그 쿼리가 자주 변경되는 데이터인지, 아니면 간헐적으로 변경되는 데이터인지에 따라 어떻게 설정할지가 달라질 것 같다. 만약 나의 액션에 의해 데이터가 바뀌는 게 아니고 외부의 요인으로, 예를 들어 다른 사람이 새로운 글을 올린 것을 받는 상황인지, 아니면 나만의 할일 목록을 만들어서 사용하는 상황인지에 따라 staleTime을 어떻게 설정할지가 달라질 것 같다. 이건 서비스의 상태에 따라서 달라지니까 잘 파악을 해봐야 할 것!
이제 우리가 지금까지 알아봤던 것들이 기초가 되어, 서비스를 이용하면서 발생한 네트워크 로직에 따라 어떤 피드백을 제공할 수 있을지 알아볼 수 있다.
캐시까지 알아보았으니, 이제 Optimistic Update에 대해서 말할 수 있겠다. 지금까지의 Mutation은 데이터를 조작하는 요청만 담당한다고 말했다. 그 후 갱신된 데이터를 받아오는 것은 Query가 맡아서 하고. 그런데 만약 데이터 조작 요청과 데이터 갱신 사이의 시간이 엄~청 길면 어떻게 될까? 당연히 사용자 입장에서는 답답할 것이다. 특히나 당신이 한국인이라면 더!
그래서 데이터 조작 요청이 어차피 성공함을 기대하며, 사용자가 요청하는 순간 클라이언트 데이터를 먼저 바꿔버린다. 그 후 응답에 따라 다시 롤백을 할지 그대로 냅둘지 결정하는 것이 바로 Optimistic Update
이다. 이를 적용한 UI가 Optimistic UI이고.
이런 형태의 UI는 사실 익숙하다. 대표적으로 인스타그램과 페이스북의 좋아요 기능이 있겠다. 원래라면 좋아요를 누르고 서버에서 응답이 올 때까지 반응이 없겠지만, 좋아요를 누르자마자 반응이 생기는 것을 알 수 있다. 먼저 좋아요 반응을 한 뒤, 뒤에서 확인하는 절차이다.
이 기능을 구현하려고 생각해보면, 피치 못할 생각이 든다. 그러면,, Fetch한 데이터를 저장해야겠는데? 당연히 서버에서 응답받지 않은 데이터를 수정하려면, 이 데이터를 보관하고 있는 query에 접근을 해서, 해당 아이템을 찾은 다음, 관련된 속성값을 바꾼 후, UI에 적용해야 할 것이다.
이를 위해 Store을 생성하고 동기화하고 하다보면... 또 나처럼 수렁에 빠진다(지옥의 동기화). 다행히도 우리는 이미 저장된 데이터를 알고 있다. 그것은 바로 캐시!
Mutation이 발생하면 보관 중인 캐시에 접근을 해서, 캐시 데이터를 직접 바꾸는 것이다. 응답이 왔을 때 성공적이면 해당 데이터로 변경하거나 또는 그대로 놔두거나, 아니면 실패를 했다면 이전 데이터로 다시 캐시를 설정하면 된다. React Query에서는 캐시 데이터가 바뀌면 해당 쿼리키를 쓰는 쿼리에게 refetch를 요구하기 때문에, 직접 바꾼 캐시가 화면에 뿌려지게 된다. 이 부분에 대한 이해는 코드를 보면 더 이해가 잘 될 것 같으니, 나중에 코드를 보면서 한번 더 언급해보겠다.
위에서 통신은 (1) 요청하고 (2) 응답받는다고 했다. 그리고 이 과정은 네트워크 레이어에서 일어나기 때문에 요청이 가는 시간과 응답이 오는 시간, 즉, Loading이 발생하게 되는 것은 필연적이다.
로딩 시간은 UX에 꽤 큰 영향을 미친다. 로딩이 길어질수록 유출이 높아지고, 사용자가 느끼는 감정이 부정적으로 변하고, 이에 서비스 느낌이 불편함에 가깝게 된다. 이러면 서비스는 나락의 길을 걷게 되는 것...!
이를 방지하기 위해 중요한 것은 효율적이고 빠른 통신이겠지만, 앞서 말했듯이 로딩은 필연적이고, 아무리 빠른 통신이라고 할지라고 사용자의 네트워크 환경에 따라 불안정할 수 있다. 이에 유저에게 "너가 요청한 행동을 지금 처리 중이니까 기다려!" 라든지, "너 네트워크 환경이 좀 불안정한 것 같아, 좀 늦게 뜰 수도 있어!" 등등 로딩 상태에 따른 피드백을 주는 것이 중요하다. 어쨌든 느리면 열받긴 하겠지만...
로딩 상태에 따른 피드백은 UX에 대한 이야기이므로, 여기에서는 다루지 않고 좋은 포스팅을 링크해두는 것으로 갈무리하겠음!
UX 로딩에 대한 포스트들
로딩을 거치고 응답을 받았는데, 정상적인 응답이 아닌 에러를 받을 수도 있다. 검색한 결과가 없다든지, 로그인을 할 수 없다든지, 인터넷에 연결이 안 됐다든지.
하지만 의외로 많은 서비스에서는 에러에 대한 피드백을 친절하게 하지 않는다(!) 에러는 어쩔 수 없이 일어난 거니까 머,, 대충 다시 해봐~ 이런 식의 대처가 많다. 또, 에러는 개발자의 입장에서 맞춰진 경우도 많다. 어차피 해결하려면 어떤 에러가 발생했는지 알아야 하니까 개발자가 알아볼 수 있도록 작성하는 경우도 있다.
중요한 것은 에러를 마주하는 것은 사용자라는 것을 잊으면 안 된다. 사용자는 문제를 마주쳤을 때 어떤 문제가 발생했는지, 왜 발생했는지, 해결 방법은 뭔지가 궁금할 뿐이다. 에러 코드니 뭐니 하는 건 도움이 안 된다.
이 역시 좋은 포스트들이 많아서,, 링크로 대체!
UX Error에 대한 포스트들
현재 로딩 중인지, 에러가 났는지 어쩐지를 알아야 그에 따른 화면을 보여줄 수 있을 것이다. 이를 위해 React Query에서는 현재 쿼리 상태를 제공해준다. status라는 변수로 상태를 알려주는데, 다음과 같다.
Status의 종류
success
: 쿼리가 성공했고, 데이터가 있음error
: 쿼리가 실패했고, 오류 데이터가 생성됨pending
: 쿼리에 데이터가 없음
이 Status를 기준으로 여러가지 State Flag를 제공해주고 있으므로, 이에 대해서 알아보자!
+) 추가적으로,, React Query에서는 상태를 '온라인'과 '오프라인'에 따라 제공하는 상태가 나뉜다. 아래에서 이야기하는 것은 온라인에 관한 상태이다.
기본적으로 네트워크를 통해서 데이터를 가져오는 동안의 로딩은 isLoading으로 제공하고 있고, 캐시 데이터가 있는데 다시 가져오기를 시도하는 경우엔 isFetching으로 제공하고 있다. 이 외에도 많은 상태 Flag를 제공하고 있는데, 좀 더 예시로 설명하자면 다음과 같다.
최초로 Todos 불러오기
const todos = useTodos() // { // data: undefined // isLoading: true, // isFetching: true, // isRefetching: false, // }
그 이후 Todos 불러오기
const todos = useTodos() // { // data: [...] // isLoading: false, // isFetching: true, // isRefetching: true, // }
최초로 Todos를 불러올 때는 data가 undefined로 정의되어 있고, isLoading flag가 true로 설정되어 있다. 그 다음 Todos를 불러올 때는 data가 존재하고, isLoading flag는 false이지만 isFetching과 isRefetching이 true가 되었다.
즉, 다음과 같이 Flag가 세워지는 것이다.
Flag가 설정되는 상황
isLoading
: 최초로 데이터를 가져올 때isFetching
: 쿼리가 실행이 될 때(최초 포함)isRefetching
:isLoading === false && isFetching === true
이렇게 최초로 데이터를 가져올 때와, 굳이 쿼리가 실행될 때의 상태를 구분해준 이유는 뭘까? 상황을 예로 들어 알아보자!
만약 팀원들과 공유하고 있는 할일 목록이 있다고 가정해보자. 메인 페이지는 이 할일 목록을 받아와서 보여주는 페이지이다. 그러면 사용자가 제일 처음 들어왔을 때는 가지고 있는 데이터가 없을 것!
따라서 데이터를 가지고 오고 있다는 로딩 상태를 띄울 것이다. 스피너를 표시해준다고 해보자. 그러면 데이터를 성공적으로 가지고 오면 UI에 뿌려줄 것이다. 얼마 후 팀원이 할일 목록에 할일을 추가했다고 알려왔다.
그러면 React Query는 다시 데이터를 가져오려고 할 텐데, 이때는 내가 기존에 가지고 있던 할일 목록이 존재할 것이다. 사용자 입장에서 새로운 데이터를 가지고 오는데 이미 보여지고 있는 데이터가 사라지고, 가져오고 있다는 스피너만 뜨고 갱신된 데이터가 반영된 UI를 보는 과정이 복잡하게 느껴질 것이다. 일종의 'Blinking'이 발생하는 것이다.
이번엔 새로운 데이터를 가져오는데 기존의 데이터가 유지된 채 위에 스피너만 뜬다고 가정해보자. 그러면 내가 보고 있던 데이터를 계속 보고 있는 채 데이터가 로딩이 된다는 사실을 알 수 있다. 그러면 'Blinking' 없이 데이터를 볼 수 있게 되는 것이다.
isLoading과 isFetching을 구분하면서 로딩 UI를 표시해주는 것은 서비스 유스케이스에 따라 다르겠지만, 포인트는 기존의 데이터 없이 불러오는 로딩과, 기존의 데이터가 있는 상태로 불러오는 로딩은 구분될 여지가 있다는 것이다.
추가로 위에서 언급한 staleTime 역시 관련이 있다. staleTime 전에 호출이 된다면 캐시를 넘겨준다고 했고, 후에 호출이 된다면 네트워크 통신을 통해 다시 받아온다고 했다. 그러므로~ staleTime 동안에는 네트워크 통신을 하지 않았으니/데이터를 가지고 있으니 isLoading flag가 false일 테고, 이 이후에는 cacheTime이더라도 네트워크 통신이 발생했으니 isLoading flag가 true로 변할 것이다.
장황하게 이야기를 풀어놓았지만,, 정리하자면 아래와 같다.
isLoading과 isFetching
isLoading
: 해당 쿼리가 가지고 있는 데이터가 없고, 데이터를 가져오고 있을 때.isFetching
: 해당 쿼리가 실행될 때.
+추가) isPending은?
- v5로 넘어오면서
isLoading
이isPending
으로 바뀌었다.
- v3에서는
isInitialLoading
이었고, v4에서는isLoading
이었다.
그래서 다음과 같이 분기 렌더링을 처리해주는 것이 일반적이다.
패칭 상태에 따른 분기 렌더링
const todos = useTodos() return ( <div> {todos.isRefetching && <Spinner />} {todos.isError && <Error {...error} />} {todos.data && todos.data.map((todo) => <Todo {...todo} />)} {todos.isLoading && <Spinner />} </div> )