※ 본 강의에서의 react query는 3버전으로 4버전과는 버전 차이가 있어 코드가 다를 수 있습니다.
useAuth
: provides functions for signin/signup/signoutuseUser
)useAuth
collects user data from calls to server (add to cache)useUser
user
data from React QuerylocalStorage
on initialization (사용자가 페이지를 새로고침 할 때 데이터를 유지하는 방법입니다.)useQuery
(변이가 일어나면 서버의 사용자 데이터가 변경될 겁니다. 그리고 React useQuery 훅을 사용해서 사용자 데이터를 항상 최신으로 유지해야 합니다. useQuery 인스턴스의 쿼리 함수는 로그인 한 사용자의 ID와 함께 서버에 요청을 보낼 겁니다. 그럼 서버가 그 사용자에 관한 데이터를 돌려보내줍니다.)null
if no user logged in (만약 로그인 한 사용자가 없다면 쿼리 함수는 null을 반환합니다.)setQueryData
(setQueryData로 직접 React Query 캐시를 업데이트 합니다.)localStorage
in onSuccess
callback (그리고 localStorage도 업데이트 합니다. 업데이트는 onSuccess 콜백에서 진행되며 useQuery까지 업데이트 합니다.)onSuccess
runs after: (onSuccess 콜백은 setQueryData와 쿼리 함수가 실행된 이후에 실행됩니다.)setQueryData
데이터의 흐름
// useUser.ts
...
async function getUser(user: User | null): Promise<User | null> {
if (!user) return null;
const { data }: AxiosResponse<{ user: User }> = await axiosInstance.get(
`/user/${user.id}`,
{
headers: getJWTHeader(user),
},
);
return data.user;
}
...
export function useUser(): UseUser {
// TODO: call useQuery to update user data from server
const { data: user } = useQuery(queryKeys.user, () => getUser(user));
...
문제 : user가 처음부터 정의되지 않았다면 거짓(falsy)의 값이 나와 여기에서 null이 반환되고 어떤 사용자 데이터도 가져오지 못합니다.
useAuth
(React Query와 Auth 통합하기)위의 문제를 해결하기 위해 updateUser 함수와 clearUser 함수가 필요합니다.
useAuth 훅으로 쿼리 캐시에 값을 설정할 수 있으면 좋겠습니다.
그래야 useQuery 함수를 실행할 때 사용할 값이 생기니까요
queryClient.setQueryData
updateUser
and clearUser
useAuth
already calls these functionsupdateUser 함수는 setQueryData로 실행할 것입니다. 그러기 위해서는 QueryClient가 필요합니다.
// useUser.ts
...
export function useUser(): UseUser {
const queryclient = useQueryClient();
const { data: user } = useQuery(queryKeys.user, () => getUser(user));
function updateUser(newUser: User): void {
queryclient.setQueryData(queryKeys.user, newUser);
}
function clearUser() {
queryclient.setQueryData(queryKeys.user, null);
}
return { user, updateUser, clearUser };
}
로그인하면 테스트용 사용자 정보를 나타내고 내비게이션 바에서는 테스트 사용자를 가져오고 로그아웃으로 전환할 수 있습니다. 아쉬운 점은 페이지를 새로 고치면 이 세션에서 데이터 보존을 안했으므로 로그아웃했다고 가정하게 됩니다. 쿼리 캐시에만 보존하고 있기 때문에 새로 고침하면 날아갑니다.
먼저 onSuccess 콜백을 실행하고 로컬스토리지 값으로 useQuery 함수를 초기화합니다.
// useUser.ts
...
export function useUser(): UseUser {
const queryclient = useQueryClient();
const { data: user } = useQuery(queryKeys.user, () => getUser(user), {
onSuccess: (received: User | null) => { // 쿼리함수나 setQueryData에서 데이터를 가져오는 함수
if (!received) { // falsy의 값을 받을 경우
clearStoredUser();
} else { // truthy의 값을 받을 경우
setStoredUser(received);
}
},
});
...
위처럼 코드를 작성해도 데이터 유지만 진행 한 것이고 로컬 스토리지에 데이터가 저장이 되는 것은 아니기 때문에 새로고침을 해도 로그아웃이 되는 것은 동일합니다.
Setting Initial Value
initialData
value to useQuery
placeholderData
or default destructured valuelocalStorage
// useUser.ts
...
export function useUser(): UseUser {
const queryclient = useQueryClient();
const { data: user } = useQuery(queryKeys.user, () => getUser(user), {
initialData: getStoredUser,
onSuccess: (received: User | null) => {
if (!received) {
clearStoredUser();
} else {
setStoredUser(received);
}
},
});
...
// index.ts
...
export function getStoredUser(): User | null {
const storedUser = localStorage.getItem(USER_LOCALSTORAGE_KEY);
return storedUser ? JSON.parse(storedUser) : null;
}
...
초기 데이터의 값을 함수(getStoredUser)로 설정합니다. 초기 데이터가 필요할 때마다 getStoredUser 함수를 실행하고 로컬스토리에서 JSON 형식의 데이터를 가져와서 객체로 구문 분석합니다.
useQuery
in useUserAppointmentsuser-appointments
user
being truthy// useUserAppointments.ts
...
async function getUserAppointments(
user: User | null,
): Promise<Appointment[] | null> {
if (!user) return null;
const { data } = await axiosInstance.get(`/user/${user.id}/appointments`, {
headers: getJWTHeader(user),
});
return data.appointments;
}
export function useUserAppointments(): Appointment[] {
const { user } = useUser();
const fallback: Appointment[] = [];
const { data: userAppointments = fallback } = useQuery(
'user-appointments', // 추후 쿼리 키 접두사 업데이트 예정
() => getUserAppointments(user), // 인수를 가지기 때문에 익명 함수 선언
{ enabled: !!user }, // !user boolean type 설정. !!user이 참이면 user도 참
);
return userAppointments;
}
if (!user) return null;
이유?
경쟁 상태(Race condition)가 있거나 고려하지 못한 요소가 있을 때를 대비해 보수적으로 프로그래밍 한 것 입니다. user.id가 없다면 서버에 연결을 시도하지 않도록 합니다.
user-appointments 쿼리는 아직 예약하지 않았기 때문에 데이터가 없습니다.
user 쿼리에 6 옵저버가 있습니다.
이 옵저버들은 바로 useUser를 실행하는 앱의 모든 컴포넌트가 쿼리를 ‘구독'하고 있습니다.
여러가지가 있습니다. 사용자 예약 현황, 사용자 정보가 각각 하나의 컴포넌트이고 상단의 로그인 정보도 하나의 컴포넌트 입니다. (사용자명과 로그인 및 로그아웃 버튼을 표시하는 내비게이션입니다.)
이처럼 모든 컴포넌트가 해당 쿼리를 참고하고 있습니다.
React Query의 장점은 새로운 데이터가 있을 때 새 데이터를 위해 서버에 핑을 실행하기 보다 캐시에서 데이터를 가져옵니다.
데이터가 만료(stale) 상태여도 React Query는 서버에 새로 연결하지 않습니다. 기존에 이미 실행되고 있다면 React Query가 서버로 중복되는 요청을 제거하기 위해 여러 요청이 있어도 동시에 실행되지 않습니다.
이미 진행 중인 요청을 구독한다면 해당 요청에 포함됩니다.
userAppointments
does not need onSuccess for useUser// useUser.ts
...
function clearUser() {
queryclient.setQueryData(queryKeys.user, null);
queryclient.removeQueries('user-appointments');
}
...
사용자가 로그아웃했을 때 useAuth에서 호출한 clearUser가 쿼리데이터를 null로 설정해서 onSuccess를 트리거할 뿐 아니라 clearStoredUser()를 통해 로컬 스토리지로부터 사용자를 지웁니다.
removeQueries를 추가로 실행해야 합니다.
쿼리키는 하나만 추가 가능합니다. 하나 이상의 쿼리 키에 removeQueries를 실행하려면 여러 번 동일하게 실행하면 됩니다.
useQuery
caches user data and refreshes from server
useUser
manages user data in query cache and localStorage
setQueryData
on signin / signoutonSuccess
callback manages localStorage
user appointments query dependent on user
state
removeQueries
reference
https://www.udemy.com/course/learn-react-query
https://tanstack.com/query/v4/docs/guides/dependent-queries
https://tanstack.com/query/v4/docs/guides/initial-query-data