[React Query] Section7 - React Query and Authentication

이해용·2022년 9월 3일
3
post-thumbnail

※ 본 강의에서의 react query는 3버전으로 4버전과는 버전 차이가 있어 코드가 다를 수 있습니다.

React Query and Auth

  • Dependent queries: 어떤 조건 하에서만 활성화
  • setQueryData: 실제로 캐시에 데이터를 설정하기 위해 사용
  • removeQueries: 캐시에서 쿼리를 삭제하기 위해 사용

JWT Authentication

  • JWT
    • server sends token on successful login (or user creation)
    • client sends token in headers with requests as proof of identity
  • Security
    • token contains encoded information such as the username and user ID
    • decoded and matched on the server
  • In this app, the JWT is stored in the user object
    • persisted in localStorage
    • your auth system may use a different way to persist data between sessions

Separation of Concerns

  • React Query: provide cache for server state on the client (리액트 쿼리의 책임은 클라이언트의 서버 상태를 관리하는 것입니다.)
  • useAuth: provides functions for signin/signup/signout
  • Conclusion: React Query will store data (via useUser)
  • useAuth collects user data from calls to server (add to cache)

Role of useUser

  • Returns user data from React Query
    • Load from localStorage on initialization (사용자가 페이지를 새로고침 할 때 데이터를 유지하는 방법입니다.)
  • Keep user data up to date with server via useQuery (변이가 일어나면 서버의 사용자 데이터가 변경될 겁니다. 그리고 React useQuery 훅을 사용해서 사용자 데이터를 항상 최신으로 유지해야 합니다. useQuery 인스턴스의 쿼리 함수는 로그인 한 사용자의 ID와 함께 서버에 요청을 보낼 겁니다. 그럼 서버가 그 사용자에 관한 데이터를 돌려보내줍니다.)
    • query function returns null if no user logged in (만약 로그인 한 사용자가 없다면 쿼리 함수는 null을 반환합니다.)
  • Whenever user updates (sign in / sign out / mutation) (useUser의 역할은 앱의 특정 인스턴스까지 로그인한 사용자를 추적하는 것입니다. 그리고 정보가 업데이트 되면)
    • update React Query cache via setQueryData (setQueryData로 직접 React Query 캐시를 업데이트 합니다.)
    • update localStorage in onSuccess callback (그리고 localStorage도 업데이트 합니다. 업데이트는 onSuccess 콜백에서 진행되며 useQuery까지 업데이트 합니다.)
      • onSuccess runs after: (onSuccess 콜백은 setQueryData와 쿼리 함수가 실행된 이후에 실행됩니다.)
        • setQueryData
        • query function

Why not store user data in Auth provider?

  • Definitely an option
  • Disadvantage is added complexity (단점은 복잡함이 추가됩니다.)
    • Separate Provider (Context) to create / maintain (React Query 캐시에서 분리된 Provider 관리도 포함해서요)
    • Redundant data in React Query cache vs dedicated Auth Provider (또한 불필요한 데이터가 생기는 것도 단점입니다. 사용자 변이를 허용하면 그 사용자 데이터를 React Query에 보관하고 싶을 텐데 Auth Provider에도 사용자 데이터를 입력해야 해요)
  • Starting fresh: store in React Query cache, forgo Auth Provider (새로운 애플리케이션을 개발한다면 당연히 React Query 캐시에 사용자 데이터를 저장하고 Auth Provider는 잊고 싶을 것입니다.)
  • Legacy project: may be more expedient to maintain both (하지만 Legacy project에서는 Auth Provider를 관리하고 React Query 캐시를 필요 위치에 추가하는 것이 더 합당합니다.)

useAuth, useUser와 useQuery

데이터의 흐름

  • useAuth 훅은 로그인 가입 로그아웃을 관리합니다.
  • useUser 훅은 React Query 가 작동하는 곳
  • useUser의 책임은 로컬 스토리지와 쿼리 캐시에서 사용자의 상태를 유지하는 것입니다.
  • useAuth의 책임은 이러한 함수들이 서버와 통신하도록 하는 것입니다.
// 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이 반환되고 어떤 사용자 데이터도 가져오지 못합니다.

Set query cache values in useAuth (React Query와 Auth 통합하기)

위의 문제를 해결하기 위해 updateUser 함수와 clearUser 함수가 필요합니다.

useAuth 훅으로 쿼리 캐시에 값을 설정할 수 있으면 좋겠습니다.

그래야 useQuery 함수를 실행할 때 사용할 값이 생기니까요

  • React Query acting as a provider for auth
  • Use queryClient.setQueryData
  • Add to updateUser and clearUser
    • useAuth already calls these functions

updateUser 함수는 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 };
}

QueryClient

로그인하면 테스트용 사용자 정보를 나타내고 내비게이션 바에서는 테스트 사용자를 가져오고 로그아웃으로 전환할 수 있습니다. 아쉬운 점은 페이지를 새로 고치면 이 세션에서 데이터 보존을 안했으므로 로그아웃했다고 가정하게 됩니다. 쿼리 캐시에만 보존하고 있기 때문에 새로 고침하면 날아갑니다.

localStorage에서 사용자 데이터 유지하기

먼저 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);
      }
    },
  });

...

위처럼 코드를 작성해도 데이터 유지만 진행 한 것이고 로컬 스토리지에 데이터가 저장이 되는 것은 아니기 때문에 새로고침을 해도 로그아웃이 되는 것은 동일합니다.

useQuery를 위한 localStorage의 initialData

Setting Initial Value

  • Use initialData value to useQuery
    • For use when you want initial value to be added to the cache
    • For placeholder, use placeholderData or default destructured value
  • Initial value will come from localStorage
// 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 형식의 데이터를 가져와서 객체로 구문 분석합니다.

initialData

Dependent Queries (의존적 쿼리: userAppointments)

  • Separate query for user appointments
    • Change more frequently than user data
    • A bit artificial, but good for demonstrating dependent queries
  • Call useQuery in useUserAppointments
    • For now, use the key user-appointments
    • Will change when we start looking at query key prefixes
  • Make the query dependent on 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-appointments 쿼리는 아직 예약하지 않았기 때문에 데이터가 없습니다.

user

user 쿼리에 6 옵저버가 있습니다.

이 옵저버들은 바로 useUser를 실행하는 앱의 모든 컴포넌트가 쿼리를 ‘구독'하고 있습니다.

여러가지가 있습니다. 사용자 예약 현황, 사용자 정보가 각각 하나의 컴포넌트이고 상단의 로그인 정보도 하나의 컴포넌트 입니다. (사용자명과 로그인 및 로그아웃 버튼을 표시하는 내비게이션입니다.)

이처럼 모든 컴포넌트가 해당 쿼리를 참고하고 있습니다.

React Query의 장점은 새로운 데이터가 있을 때 새 데이터를 위해 서버에 핑을 실행하기 보다 캐시에서 데이터를 가져옵니다.

데이터가 만료(stale) 상태여도 React Query는 서버에 새로 연결하지 않습니다. 기존에 이미 실행되고 있다면 React Query가 서버로 중복되는 요청을 제거하기 위해 여러 요청이 있어도 동시에 실행되지 않습니다.

이미 진행 중인 요청을 구독한다면 해당 요청에 포함됩니다.

Remove userAppointments Query (쿼리 클라이언트 removeQueries 메서드)

  • Make sure user appointments data is cleared on sign out
    • queryClient.removeQueries (를 통해 특정 쿼리에 대한 데이터를 제거해야 합니다.)
  • Why not use removeQueires for user data?
    • setQueryData invokes onSuccess (removeQueries does not) (사용자 데이터를 변경해서 onSuccess 콜백을 발생시킬 때 onSuccess 콜백이 로컬스토리지에 데이터를 유지하며 setQueryData가 onSuccess를 발생시키기 때문입니다. 다시 말해 onSuccess는 setQueryData 다음에 실행되고 removeQueries 다음에는 실행되지 않습니다. 따라서 user 데이터에 removeQueries 가 아닌 setQueryData를 사용하는게 중요합니다.)
  • 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를 실행하려면 여러 번 동일하게 실행하면 됩니다.

Summary 요약: React Query와 Auth

  • useQuery caches user data and refreshes from server

    • refreshing from server will be important for mutations
  • useUser manages user data in query cache and localStorage

    • set query cache using setQueryData on signin / signout
    • onSuccess callback manages localStorage
  • user appointments query dependent on user state

    • Remove data on signout with 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

profile
프론트엔드 개발자입니다.

0개의 댓글