tanstackquery, Zustand, Throttling & Debouncing
다운로드 UI가 있을 때 또는 UX 저해시키는 불필요한 네트워크 요청을 제거하기 위해 사용
대용량 fetching을 중간에 취소하거나 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소시켜 불필요한 네트워크 비용을 줄일 수 있다.
export const getTodos = async (queryFnContext) => {
const { queryKey, pageParam, signal, meta } = queryFnContext;
// queryKey: 배열형태의 쿼리키
// pageParam: useInfiniteQuery 사용 시 getNextPageParam 실행 시 적용
// signal: AbortSignal 을 의미 (네트워크 요청을 중간에 중단시킬 수 있는 장치)
// meta: query에 대한 정보를 추가적으로 메모를 남길 수 있는 string 필드
const response = await axios.get("http://localhost:5000/todos", { signal });
return response.data;
};
useQuery({
queryKey: ["todos"],
queryFn: getTodos,
})
// example: <div onClick={(event) => {}}
: 서버 요청이 정상적으로 잘 될거란 가정하에 UI 변경을 먼저하고, 서버 요청 하는 방식. 혹시라도 서버 요청이 실패하는 경우, UI 를 원상복구(revert / roll back)
: 페이지 이동 전에 이동할 페이지의 쿼리를 백그라운드에서 미리 호출 (prefetching) 캐시 데이터가 있는 상태로 해당 페이지로 이동 시 로딩없이 바로 UI를 볼 수 있다.
: 다른 페이지 클릭 시 매번 Loading UI를 보여주기보다는 기존 UI를 유지하다가 서버로부터 새로운 데이터를 받아왔을 때 바꾸는 방식을 적용할 수 있습니다.
useQuery의 옵션 중 keepPreviousData를 true 로 바꾸면 이전 캐시데이터를 기반으로 isLoading 여부를 판단하게 함
: Data Fetching 이 일어날 때 마다 기존 리스트 데이터에 Fetched Data 를 추가하고자 할 때 유용하게 사용할 수 있는 hook
더보기 UI 또는 무한스크롤 UI 에 사용하기에 적합
const fetchProjects = async ({ pageParam = 0 }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
Intersection Observer 요소의 가시성 관찰
threshold 0.3 머리의 0.3만큼만 닿으면 새로 데이터를 불러옴
threshold 1 해당 네모박스 맨 끝까지 영역이 닿아야 다음 데이터를 불러오겠다.
: 상태관리 본연의 기능에 집중하여 복잡성을 줄이고 보다 간단하고 직관적인 상태관리 기능을 제공
yarn add zustand
// bearStore.js
import { create } from "zustand";
const useBearsStore = create(function (set) {
return {
bears: 0,
increase: () => {
set((state) => {
return {
bears: state.bears + 1,
};
});
},
init: () => {
set({
bears: 0,
});
},
};
});
export default useBearsStore;
// app.jsx
import React from "react";
import useBearsStore from "./zustand/bearsStore";
const App = () => {
// const bears = useBearsStore((state) => {
// return state.bears;
// });
// const increase = useBearsStore((state) => {
// return state.increase;
// });
const { bears, increase, init } = useBearsStore((state) => state);
return (
<div>
<h2>Zustand</h2>
<h4>{bears}</h4>
<button onClick={increase}>+1</button>
<button onClick={init}>초기화</button>
</div>
);
};
export default App;
: Immer는 JavaScript에서 상태를 쉽게 변경할 수 있게 해주는 라이브러리
원본 데이터를 변경하지 않고도 마치 직접 수정하는 것처럼 코드를 작성할 수 있으며, Immer가 자동으로 불변성을 유지한 새 상태를 만들어줌
yarn add immer
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
const useTodosStore = create(
immer((set) => ({
todos: [
{
id: 1,
title: "Learn Zustand",
tasks: [{ id: 1, task: "Read documentation", done: false }],
},
],
addTask: (todoId, newTask) =>
set((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
todo.tasks.push(newTask); // 불변성 유지: immer가 자동으로 처리
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
}),
toggleTask: (todoId, taskId) =>
set((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
const task = todo.tasks.find((task) => task.id === taskId);
if (task) {
task.done = !task.done; // 불변성 유지: immer가 자동으로 처리
}
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
}),
}))
);
export default useTodosStore;
import { produce } from "immer";
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useTodosStore = create(
persist(
(set) => ({
todos: [
{
id: 1,
title: "Learn Zustand",
tasks: [{ id: 1, task: "Read documentation", done: false }],
},
],
addTask: (todoId, newTask) =>
set(
produce((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
todo.tasks.push(newTask); // 불변성 깨짐: 직접 수정
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
})
),
toggleTask: (todoId, taskId) =>
set(
produce((state) => {
const todo = state.todos.find((todo) => todo.id === todoId);
if (todo) {
const task = todo.tasks.find((task) => task.id === taskId);
if (task) {
task.done = !task.done; // 불변성 깨짐: 직접 수정
}
}
// return { todos: state.todos }; // 변경된 참조가 기존 상태와 같아 리렌더링되지 않음
})
),
}),
{
name: "todos-storage", // 저장소 이름을 설정해요!
// getStorage: () => sessionStorage, // localStorage가 아닌 곳에 저정하고 싶다면!
}
)
);
export default useTodosStore;
: 짧은 시간 간격으로 연속해서 ㅂ라생한 이벤트들을 일정시간 단위로 그룹화하여 처음 또는 마지막 이벤트 핸들러만 호출되도록 하는 것 주로 무한 스크롤에서 사용
ex) 사용자가 스크롤을 시작할 때 처음에만 API호출이 이루어지고, 일정 시간 동안 추가 호출이 무시
ex) 사용자가 ㅇ비력 필드에 타이핑을 멈춘 후 일정 시간이 지나야 서버에 검색 요청이 이루어짐
ex) 여러번 클릭할 때 처음 클릭시 바로 API호출, 주어진 시간의 마지막 이벤트에도 API 호출이 이루어짐
: 짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정시간이 경과한 후에 한 번만 호출하도록 하는 것
주로 입력값 실시간 검색, 화면 resize 이벤트에서 사용
: 필요하지 않은 메모리를 계속 점유하고 있는 현상