react-query, TanQuery 서버 상태를 선언적으로 관리
- REACT & NODE
- 2024. 9. 3.
react-query 즉 Tan Query 를 통해 서버 상태를 선언적으로 관리하여 많은 문제 해결을 할 수 있습니다. 아래와 같은 시나리오를 가정해보죠.
현재는 리덕스 툴킷을 사용하여 프론트 측의 전역 상태에 스키마를 정의하고 정규화를 수행하면서 채팅앱을 구현하였습니다.
로그인한 내가 있고 나는 프로필 이미지와 성별, 닉네임, 이미지들, 게시물들, 등등의 정보를 가집니다.
일대일 채팅방은 채팅방 아이디, 마지막 메시지 타입, 마지막 메시지 내용, 채팅방 참여자 두명의 정보, 읽지 않은 갯수 등의 정보를 가집니다.
채팅 메시지는 아이디와 타입, 내용, 보낸사람 아이디, 보낸사람 닉네임, 받는사람 아이디, 받는사람 닉네임, 채팅방 아이디 등의 정보를 가집니다.
게시물은 아이디, 제목, 이미지, 작성자, 좋아요 누른 사람 등의 정보를 가집니다.
여기서 이미지는 서버측에서 보면 공통으로 관리되고 있으며 따라서 이미지의 정보는 사용자 프로필 이미지, 채팅 메시지 이미지, 게시물의 이미지 등이 모두 서버측에서는 같은 테이블에 저장됩니다.
서버는 mariadb 라서 RDB 이고 sequelize 라이브러리를 사용하여 가져오고 있습니다.
앱에서 채팅방 리스트를 보여주며 채팅방리스트에는 상대방 프로필 이미지와 최근 메시지 내용, 최근 읽지 않은 메시지 갯수 등이 표시됩니다. 채팅방 리스트에서 한개를 클릭하면 채팅방에 들어가서 채팅 메시지들이 나열됩니다.
내 메시지와 상대방 메시지가 좌우로 위치하며 시간 순으로 표시됩니다.
이미지 일 경우는 이미지가 보여질 것이고 텍스트이면 텍스트가 보여질 것입니다. 상대방 프로필 이미지가 또한 메시지의 앞에 보여질 것입니다.
이러한 시나리오를 가정할 경우, 채팅방, 채팅방 메시지, 사용자 (나 또한 사용자이죠), 이미지, 게시물 등이 서로 관계를 형성할 것입니다. 서버측에서는 RDB 를 사용하기에 일반적인 관계형 디비의 모델링을 통해 스키마가 정의되어 있고 sequelize 라이브러리를 통해 관계를 맺고 관련 데이터를 쿼리합니다.
프론트 측의 상태를 정규화 하고 스키마를 정의하였습니다. 우선 각각의 채팅방, 채팅방 메시지, 사용자, 나, 이미지, 게시물 등으로 타입스크립트에서 인터페이스를 통해 기본적인 타입을 정의하고 normalizr 라는 npm 패키지의 schema 를 통해 new schema.Entity<User>(...); new schema.Entity<UploadedImage>(...); 등과 같이 프론트 측의 스키마를 정의하고 리덕스 툴킷을 사용하여 기본적으로는 createEntityAdapter<User>() 등과 같이 하여 슬라이스와 셀렉터를 도출하고 필요시 커스텀한 셀렉터와 리듀서 및 thunk 등을 추가하여 구현하였습니다.
이렇게 구현하게 되면 채팅방에 입장했을 때 필요한 정보들이 내 정보, 상대방 정보, 채팅 메시지들 등등이 필요하고 그에 관련된 이미지들 및 내 정보 중에서도 프로필 이미지, 닉네임, 상대방 정보 중에서도 프로필 이미지, 닉네임 및 채팅 메시지 중에서도 이미지라면 이미지 정보 등이 필요합니다.
이럴 경우 이렇게 정규화된 프론트 측 전역 상태 (리덕스 스토어) 에 해당 정보를 fetching 하고 관련된 정보들을 서버측에서 가져오면 정규화를 하고 그 과정에서 루프도 돌고 if 문도 사용하면서 굉장히 좀 복잡하고 난해한 로직이 되어 버립니다. 또한 정규화 해서 넣은 정보들이 다시 또 다른 채팅방 입장시에 fetching 하고 관련 정보를 가져오면 중복되는 부분들도 있을 것이고 그러면 중복되는 것은 갱신이 되는 것이고 새로운 것은 추가되는 식으로 이렇게 복잡한 부분들이 생기게 됩니다.
이러한 것을 react-query (현재는 TanStack 의 Tan Query 라고 하더군요) 와 같은 서버상태 캐싱 fetching 라이브러리를 통해 서버 측의 엔드포인트에 이러한 정보들을 물려놓고 쿼리파라미터 등을 셋팅하고 적절히 캐싱되는 시점을 셋팅한 다음, 웹소켓 같이 실시간성의 채팅 이벤트가 발생하면 invalidate 시키고, 한번에 여러가지의 서버 엔드포인트의 정보가 조합되어야 할 경우 조합해주는 식으로 하여, 마치 물 줄기의 흐름을 바꾸듯이 하여 위와 같은 시나리오를 더 간단하고 완벽하게 문제없이 구현이 가능할까요?
TanStack Query (이전 이름은 react-query)와 같은 서버 상태 관리 라이브러리는 설명하신 시나리오에서 매우 유용할 수 있습니다. 특히 서버에서 데이터를 가져오고, 캐싱하며, 데이터가 변경될 때 자동으로 업데이트하는 기능을 제공합니다. 이를 통해 복잡한 상태 관리와 데이터를 일일이 정규화하여 다루는 문제를 상당히 간소화할 수 있습니다.
장점 및 이점
- 자동 캐싱 및 갱신:
- TanStack Query는 서버에서 데이터를 가져오면 해당 데이터를 자동으로 캐싱합니다. 캐싱된 데이터는 필요할 때마다 재사용되며, 서버에서 데이터를 다시 요청할 필요가 없습니다.
- 데이터가 변경되었을 때는 invalidate 기능을 사용하여 캐싱된 데이터를 무효화하고 새 데이터를 가져올 수 있습니다. 이로 인해 정규화된 상태를 일일이 업데이트할 필요 없이 서버 상태와 동기화된 최신 데이터를 유지할 수 있습니다.
- 쿼리 키를 통한 데이터 관리:
- TanStack Query는 각 서버 요청에 고유한 쿼리 키를 부여하여 데이터 관리를 단순화합니다. 이를 통해 특정 조건이나 파라미터에 따라 데이터가 다르게 캐싱되고 갱신될 수 있습니다.
- 예를 들어, 채팅방 ID와 사용자 ID를 쿼리 키로 사용하여 각 채팅방의 데이터를 독립적으로 관리할 수 있습니다. 특정 채팅방에 변화가 있을 때 해당 채팅방의 데이터만 갱신되도록 할 수 있습니다.
- 실시간 데이터 처리:
- WebSocket과 같은 실시간 통신을 통해 데이터를 받아올 때, 수신한 데이터를 기반으로 TanStack Query의 데이터를 갱신할 수 있습니다.
- 예를 들어, 새로운 메시지가 수신되면 해당 메시지 데이터가 포함된 쿼리를 invalidate하여 갱신된 메시지 목록을 다시 가져오게 할 수 있습니다.
- 비동기 데이터 조합:
- 서버에서 다양한 엔드포인트의 데이터를 가져와야 할 때 TanStack Query는 이러한 데이터를 조합하여 처리할 수 있습니다.
- 여러 API 호출을 동시에 관리하고, 각 호출이 완료되었을 때 자동으로 필요한 데이터를 조합하여 UI에 반영할 수 있습니다.
- 단순화된 데이터 요청:
- TanStack Query는 복잡한 데이터를 다룰 때도 단순화된 API를 제공합니다. 프론트엔드에서 서버 데이터를 가져오는 코드는 useQuery나 useMutation 훅을 통해 쉽게 작성할 수 있습니다.
구현 예시
아래는 TanStack Query를 사용해 구현할 수 있는 간단한 예시입니다.
import { useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function fetchChatRoomData(chatRoomId: string) {
return axios.get(`/api/chatrooms/${chatRoomId}`).then(res => res.data);
}
function fetchUserData(userId: string) {
return axios.get(`/api/users/${userId}`).then(res => res.data);
}
function ChatRoom({ chatRoomId }) {
const queryClient = useQueryClient();
// 채팅방 데이터 쿼리
const { data: chatRoomData, error: chatRoomError } = useQuery(
['chatRoom', chatRoomId],
() => fetchChatRoomData(chatRoomId)
);
// 상대방 사용자 데이터 쿼리
const { data: userData, error: userError } = useQuery(
['user', chatRoomData?.otherUserId],
() => fetchUserData(chatRoomData?.otherUserId),
{
enabled: !!chatRoomData?.otherUserId, // 채팅방 데이터가 로드된 이후에만 쿼리 실행
}
);
// WebSocket 등을 통해 실시간 데이터 업데이트 시 invalidate
const handleNewMessage = (newMessage) => {
queryClient.invalidateQueries(['chatRoom', chatRoomId]);
};
if (chatRoomError || userError) return 'Error loading data';
return (
<div>
<h2>Chat with {userData?.name}</h2>
{/* 채팅 메시지 리스트 표시 */}
</div>
);
}
요약
위에서 설명한 TanStack Query의 기능을 활용하면, 복잡한 전역 상태 관리 대신 서버 상태를 효율적으로 캐싱하고 갱신하면서 애플리케이션을 구현할 수 있습니다. 실시간 데이터와의 통합도 간단하며, 데이터의 중복 처리와 같은 문제를 해결할 수 있습니다. 이로 인해 보다 간단하고 유지보수하기 쉬운 코드베이스를 만들 수 있습니다.
이럴 경우에 채팅방 리스트, 채팅방 안에서 채팅메시지들, 상대방 사용자 정보, 관련 이미지 정보 등등이 선언적으로 서버 상태를 가져와 사용하는 방식이 되고 nested 된 서버로 부터 받은 정보들이 그대로 캐싱되고 필요한 시점에 갱신하는 방식이 되겠네요? 그러면 프론트 측에 정규화 모델링 한 것보다 중복 데이터 같은 것은 있을 수 있겠네요? 하지만 프론트 측에 정규화 하여 따로 ViewModel 을 구현하게 되면 각종 중복된 데이터는 업데이트, 없는 것은 추가 하는 방식과 서버에서 fetching 하면 normalize 작업을 하고 그렇게 어렵게 루프를 돌면서 normalize 한 것을 또 각각의 채팅방 채팅메시지 이미지 사용자 등등의 프론트 측 스키마에 넣어주는 비효율적이고 복잡한 작업을 하는 것 보다야 훨씬 나은 방법이겠네요. 그리고 서버와 프론트 측의 sync 도 훨씬 reliable 하고 잘 될 것이고. 어차피 어떤 화면에 필요한 데이터는 서버측에서 데이터를 nested 해서 내려주기 때문에 그 화면에 필요한 데이터는 대부분 해당 엔드포인트에서 거의 얻을 수 있으므로, 채팅방리스트, 해당 채팅방의 메시지들 (페이지네이션, infinite scroll 등등 기능 포함), 게시물 리스트 등등... 이런것이 엔드포인트 기준으로 nested 된 형태로 각각의 파라미터 마다 캐싱되어 선언적으로 UI 측에서 사용하고 적절하게 invalidate (갱신) 시키는 식으로 하는 것이 무조건 더 효율적이겠네요.
맞습니다, TanStack Query를 사용하여 서버 상태를 선언적으로 관리하는 방식은 특히 복잡한 애플리케이션에서 매우 효율적이고 간단한 접근 방식입니다. 설명하신 내용을 정리하고 강조하자면, 이 방법은 다음과 같은 장점과 특징을 가지고 있습니다:
1. 중복 데이터와 정규화 문제
- 중복 데이터: TanStack Query를 사용하면 서버에서 받아오는 데이터를 그대로 캐싱하므로, 동일한 데이터가 여러 쿼리에서 중복될 수 있습니다. 이는 프론트엔드에서 데이터를 정규화하여 중복을 제거하려고 할 때 발생하는 복잡성을 줄여줍니다. 중복된 데이터를 다루는 비용보다, 정규화 및 상태 관리를 위한 복잡한 로직을 작성하는 비용이 더 클 수 있기 때문에 이 방법이 더 효율적입니다.
- 정규화의 복잡성 제거: 서버로부터 받은 데이터를 프론트엔드에서 정규화하고 각기 다른 상태에 넣어주는 작업은 복잡하고 오류를 발생시키기 쉽습니다. 반면 TanStack Query를 사용하면 서버에서 받은 데이터를 그대로 UI에서 사용하게 되므로, 이런 복잡한 정규화 과정을 피할 수 있습니다.
2. 데이터와 UI의 동기화
- 데이터 일관성: TanStack Query를 사용하면 데이터가 변경될 때마다 자동으로 캐싱된 데이터가 갱신되고 UI가 업데이트됩니다. 예를 들어, 채팅방의 메시지가 추가되거나 사용자의 프로필 이미지가 변경될 때, 이 변화가 서버에서 반영되면 프론트엔드도 즉시 업데이트됩니다. 이로 인해 프론트엔드와 서버 간의 동기화가 더 신뢰성 있게 이루어집니다.
- 서버 기반 데이터 관리: 서버에서 필요한 데이터를 nested한 형태로 제공하고, 이를 프론트엔드에서 그대로 사용하면 서버에서 관리되는 데이터 모델에 의존하게 됩니다. 이는 데이터 모델링과 관련된 많은 문제를 서버 측에서 해결할 수 있게 하고, 클라이언트에서는 단순히 데이터를 보여주는 데 집중할 수 있게 합니다.
3. 엔드포인트 기반 데이터 관리
- 엔드포인트 기반 캐싱: 채팅방 리스트, 메시지 목록, 사용자 정보 등과 같은 데이터를 각각의 엔드포인트에서 가져오고, 이를 각각의 쿼리로 캐싱합니다. 이 캐싱된 데이터는 해당 엔드포인트의 데이터를 사용하는 모든 곳에서 재사용됩니다.
- 파라미터 기반 캐싱: 각 엔드포인트에 전달되는 파라미터(예: 채팅방 ID, 사용자 ID 등)에 따라 캐싱된 데이터가 다르게 관리됩니다. 이로 인해 특정한 조건에 따라 데이터를 갱신하거나 캐싱된 데이터를 재사용하는 것이 가능합니다.
4. 선언적 UI와 간단한 상태 관리
- 선언적 데이터 사용: TanStack Query는 데이터를 선언적으로 사용할 수 있게 해줍니다. 예를 들어, useQuery 훅을 사용하여 데이터를 가져오고 UI에서 이를 바로 사용할 수 있습니다. 이는 복잡한 상태 관리 로직을 작성할 필요 없이, 데이터가 필요한 시점에 필요한 데이터를 가져와 사용하는 방식입니다.
- 자동화된 갱신: 데이터가 변경되거나 새로 들어올 때, 수동으로 데이터를 갱신할 필요 없이 TanStack Query가 자동으로 캐싱된 데이터를 무효화하고 갱신합니다. 이는 특히 실시간 데이터가 중요한 채팅 애플리케이션에서 매우 유용합니다.
결론
결국, TanStack Query와 같은 서버 상태 관리 라이브러리를 사용하여 데이터를 관리하는 방식은 복잡한 애플리케이션에서 중복 데이터 관리, 정규화, 상태 동기화와 같은 문제를 해결하는 데 매우 효과적입니다. 프론트엔드에서의 복잡성을 줄이고, 서버와의 데이터 동기화를 더욱 신뢰성 있게 유지하며, 선언적인 코드를 통해 유지보수성을 높일 수 있습니다.
이러한 접근 방식이 특히 실시간성을 요구하는 채팅 애플리케이션에서 잘 맞아떨어지며, 결국 더 간결하고 유지보수가 쉬운 코드베이스를 만드는 데 기여할 것입니다.
'REACT & NODE' 카테고리의 다른 글
Reusing Logic with Custom Hooks (0) | 2024.10.06 |
---|---|
How to incrementally adopt Expo (1) | 2024.10.05 |
react-query vs redux, thunk (0) | 2024.08.28 |
React Object.is shallowEqual React.memo (1) | 2024.08.27 |
React Native 0.73.1 변경내용. 20231219 (0) | 2023.12.19 |