최근 사이드 프로젝트를 하면서 React-Query를 접하게 되었습니다.
도입한 이유는 사실 단순히 궁금해서였습니다.
많이들 사용하는 것 같던데 정작 저는 한번도 사용해보지 않아 그 이유가 궁금했습니다.
React-Query를 사용하며 배우고 느꼈던 것들을 정리해보겠습니다.
(주니어의 시각에서 작성한거라, 보완이 필요한 부분이 있을지도 모르겠습니다!?🫨🙀)
✅ React-Query란 ?
서버 상태 관리를 위한 라이브러리.
데이터 패칭, 동기화, 캐싱 등 기능을 제공해 개발자가 서버 데이터를 손쉽게 관리할 수 있다.
✅ 효용
효용1. 상태 관리의 단순화
1) React-Query 없이 관리할 땐
일반적으로 useEffect와 useState를 사용해 직접 서버 데이터를 저장하고,
loading과 error 상태는 클라이언트에서 관리해야 한다.
아래의 코드 예시처럼, 데이터 페칭시 관리해야 할 클라이언트 상태가 필요하게 되어 state 수도 많아진다.
const Component = () => {
const [data, setData] = useState(null); // 서버 데이터 저장
const [loading, setLoading] = useState(false); // 로딩 상태 저장
const [error, setError] = useState(null); // 에러 상태 저장
// 컴포넌트가 마운트될 때 처음 데이터 패칭
useEffect(async() => {
try {
setLoading(true); // 로딩 상태 활성화
const response = await axios.get('/api/...');
setData(response.data); // 데이터 저장
setError(null); // 에러 상태 초기화
} catch (err) {
setError(err); // 에러 상태 저장
} finally {
setLoading(false); // 로딩 상태 비활성화
}
}, []);
return <></>
};
2) React-Query를 사용하면
React Query는 비동기 통신에 필요한 상태(isError, error, isLoading 등..)을 내부적으로 관리해주기 때문에,
개발자가 별도로 클라이언트 상태를 직접 관리할 필요가 없어진다.
<동일한 코드를 useQuery로 패칭하는 예제 코드>
코드가 현저히 감소함을 볼 수 있다
const fetchData = async () => {
const { data } = await axios.get('/api/...');
return data;
};
const Component = () => {
const { data, error, isLoading, refetch } = useQuery('fetchData', fetchData); // useQuery 훅을 사용하여 데이터 패칭
return (<></>);
};
효용2. 서버 상태와 클라이언트 상태의 명확한 구분
뿐만 아니라 서버 상태와 클라이언트 상태를 명확히 구분할 수 있게된다.
React Query는 서버 상태만을 처리하고, 클라이언트 상태는 useState나 상태관리 라이브러리로 구분하여 관리할 수 있다.
이를 통해 UI관련된 상태값 정도만 클라이언트가 갖고 있게 된다.
효용3. key를 통한 데이터 업데이트
1) React-Query 미사용시 : 수동 데이터 관리
예로들면, 상품 상세 페이지에서 데이터를 수정하여 서버에 PUT요청을 보내 수정 완료되고 나면,
수정된 데이터를 새로 불러오기 위해 상품상세에 대한 GET 요청을 다시 실행해야 한다.
또한 상품 리스트 항목도 데이터 최신화를 위해 다시 GET요청을 보내야한다.
이렇게 데이터 최신화를 위해 연관된 데이터를 직접 수동으로 일일히 관리해줘야 한다.
2)React-Query를 사용시 : 자동 데이터 관리
React Query를 사용하면, 상품 상세 페이지 정보를 수정한 후 product와 관련된 쿼리 키를 무효화하기만 하면
관련된 모든 데이터 쿼리(get:상품 상세, getMany:상품 리스트)가 자동으로 다시 페칭된다.
데이터를 업데이트한 후 특정 쿼리 키를 무효화하여, 연관된 데이터들이 자동으로 다시 페칭되어 최신 상태로 유지되는 것이다.
이렇게 쿼리키만 잘 관리하면, 직접 데이터를 업데이트 하는 코드를 줄일 수 있다!
효용4. 캐싱 기능을 사용할 수 있다.
1) staleTime (기본값 0)
staleTime은 데이터를 얼마나 오래 "fresh" 한 상태로 유지할지에 대한 시간이다.
이 시간동안은 데이터가 fresh한 상태로 간주되서 네트워크 요청을 새로 하지 않고 캐싱된 데이터를 사용한다.
이 시간이 지나면 데이터는 "stale" 상태가 된다.
시간이 지난 후 화면 출력시, stale된 캐시 데이터를 일단 사용하여 화면에 출력해주고
이후 새로운 api호출을 통해 데이터를 refetch 시킨다.
이러한 캐싱 기능을 이용해
업데이트가 자주 필요 없는 데이터는 staleTime을 설정하여 해당 시간동안 네트워크 요청을 줄일 수 있게된다.
// 1. 쿼리 클라이언트에 설정
new QueryClient({
defaultOptions: {
queries: {
staleTime: 0 // 쿼리가 최신 상태에서 더 이상 최신이 아닌 상태로 전환하는 시간
}
}
})
// 2. api마다 각각 설정
// 유저 데이터 쿼리 (1분 동안 stale 상태로 유지)
const { data: userData } = useQuery('user', fetchUserData, {
staleTime: 60000, // 1분
});
// 상품 데이터 쿼리 (5분 동안 stale 상태로 유지)
const { data: productData } = useQuery('products', fetchProductData, {
staleTime: 300000, // 5분
});
2) cachetime (gc time)
cachetime 은 데이터가 메모리에 유지될 시간을 의미한다.
1) staleTime은 설정 시간이 지나도 데이터를 삭제하지는 않고, stale된 데이터가 그대로 남아있는 반면
2) cachetime은 설정된 시간이 지나면, 메모리에서 데이터를 아예 삭제하게 된다.
그래서 화면에 일단 빈 데이터가 출력되고 > 쿼리를 사용되는 컴포넌트가 마운트되서 > 쿼리가 다시 사용될때 > api를 새로 호출하여 데이터 refetch 하게 된다.
// 예: 유저 데이터 쿼리 - 5분 동안 캐시에 유지
const { data: userData } = useQuery('user', fetchUserData, {
staleTime: 60000, // 1분 동안 fresh
cacheTime: 300000 // 5분 후 메모리에서 삭제
});
✅ 배운점
1. 쿼리 키 관리 관리의 중요성
리액트 쿼리를 사용하면서, 쿼리 키 관리가 중요하다는 것을 느꼈다.
쿼리 키를 어떻게 구조화하느냐에 따라 개발 효율이 크게 달라졌다.
쿼리 키 관리 방식이 코드의 일관성과 유지보수성에 큰 영향을 끼쳤다.
1️⃣ 문제
처음에는 쿼리 키를 직접 입력하는 방식으로 구현했다.
예를들어, 상품 목록 페이지를 구성할 때 페이지마다 쿼리 키를 일일히 지정해줬다.
직접 입력하다보니 규칙이 없어 쿼리 키 형식을 일관되게 유지하는 것이 어렵고,
프로젝트가 커지면 쿼리 키가 다양해지면서 유지보수 하기도 어려워진다.
// 상품 목록
const { data } = useQuery(["product", "getMany", "1"], () => fetchProductList(1)); // 1페이지
const { data } = useQuery(["product", "getMany", "2"], () => fetchProductList(2)); // 2페이지
// 쿼리키 ["product", "getMany", "3,4,5..."]
// 상품 상세
const { data } = useQuery(["product", "get", "detailId1"];, () => fetchProductDetail("detailId1"));
const { data } = useQuery(["product", "get", "detailId2"];, () => fetchProductDetail("detailId2"));
// 쿼리키 ["product", "get", "detailId3,4,5,.."]
2️⃣ 해결 - Query Key 팩토리 함수 사용
Query Key 팩토리 함수를 사용하면 위 문제를 해결할 수 있다. (※ Query Key 팩토리 함수 - 참고1, 참고2, 참고3)
팩토리 함수는 쿼리 키를 일관되게 생성해주는 함수로, 코드에서 일관된 쿼리키 형태를 유지 할 수 있게 해준다.
덕분에 쿼리 키 입력을 일일히 직접 관리하는게 아닌, 자동화 하여 유지보수성을 높일 수 있었다.
<코드 예제>
아래의 코드에서 ProductQuery는 쿼리 키를 생성하는 팩토리 역할을 한다.
"getMany와 get" 메서드를 통해 "상품 목록과 상품 상세"의 쿼리 키를 일관성 있게 관리할 수 있게 되었다.
prodQuery나 id를 인수로 전달해 유연하게 쿼리 키를 생성할 수 있어, 이후에도 동일한 패턴으로 관리할 수 있다.
<쿼리키 결과>
: 팩토리를 통해 생성된 쿼리 키는 다음과 같이 구조화되어 있다.
// 상품 목록 쿼리키
["product","getMany",{"include":2,"page":1,"size":3,"sort":"-createdAt"}]
["product","getMany",{"include":2,"page":2,"size":3,"sort":"-createdAt"}]
// 상품 상세 쿼리키
["product","get","66d429ff444bc2d5e2334aee"]
["product","get","66d42a06444bc2d5e2334afa"]
["product","get","66d42fe8444bc2d5e2334bb0"]
2. 쿼리 키 무효화 관리
1️⃣ 문제
위에서 쿼리키를 수동으로 작성하고 관리하는 것이 비효율적이라고 했듯이,
쿼리키를 무효화 할 때도 일일히 처리하는 것은 좋지 않은 방법이라 생각됐다.
예를 들어, 상품 리스트 쿼리 키가 페이지별로 생성되는 구조에서
상품 리스트 3페이지에서 항목 하나를 삭제 한 후, 3 페이지 쿼리키를 removeQueries를 사용하여 직접 제거를 했더니
그럼 그 뒤에있는 4,5.. 페이지도 다시 refresh 시켜줘야 하는 상황이 생겼다.
이렇게 연관된 쿼리키를 수동으로 직접 관리하기 어렵다.
2️⃣ 해결
쿼리키를 관리 할때 범주를 잡아 쿼리키를 설정하고, 범위 중 하나가 업데이트되면 해당 범주 전체를 한번에 무효화 시켜버려야 한다.
client.invalidateQueries({ queryKey: ProductQuery.all })
const ProductQuery = {
all: ["product"] as const,
getMany: (prodQuery: ProductQueryParams) => [...ProductQuery.all, "getMany", prodQuery] as const,
get: (id: string) => [...ProductQuery.all, "get", id] as const,
};
const CategoryQuery = {
all: ["category"] as const,
getMany: (categoryQuery: CateQueryParams) => [...CategoryQuery.all, "getMany", categoryQuery] as const,
get: (id: string) => [...CategoryQuery.all, "get", id] as const,
};
상품(목록,상세) 관련 쿼리 키가 한 곳에서 집중되어 관리되어
상품 목록을 업데이트(추가,삭제,수정) 후 상품 관련 쿼리키를 한번에 invalidate 시킬 수 있다.
ProductQuery.all을 상품 관련된 쿼리키 생성 함수인 getMany, get에 공통으로 적용해주어
ProductQuery.all 만 invalid하면 연관된 getMany, get도 한번에 invalid 시킬 수 있다.
이렇게 그냥 쿼리 키만 잘 관리해주면 된다.
일일히 상품 목록 api를 다시 불러오고, 관련된 상품 상세 등 데이터를 수동으로 refetch 하지 않아도 된다.
✅ 느낀점
사용해보니 "이 좋은걸 왜 이제 알았을까" 하는 생각이 들었고, 아직 장점이 더 많은 것 같다고 생각된다.
그렇다면 무조건 이 기술을 사용하는게 좋은걸까?
아직 거기까진 모르겠다.
앞으로 사이드 프로젝트를 진행하며 좀 더 리액트 쿼리와 친해져보고싶다!
https://www.youtube.com/watch?v=MArE6Hy371c
#fake-store
'프론트엔드' 카테고리의 다른 글
Webpack 설정으로 알게된 publicPath, historyApiFallback (0) | 2025.03.16 |
---|---|
requestAnimationFrame를 활용해 애니메이션 최적화 해보자 (1) | 2025.02.02 |
FTP와 SFTP에 대해, RSA에 대해 (0) | 2024.10.04 |
css scope, scope 무시하기 (0) | 2024.09.22 |
Axios interceptor (1) | 2024.08.10 |