React Query - Authentication

박정호·2023년 1월 16일
0

React Query

목록 보기
10/14
post-thumbnail

🚀 Start

이제 로그인 기능에 대한 사용자 인증을 React Query를 통해 어떻게 처리하는지 알아보자.

우선 사용할 인증은 JSON Web Token이다.

JWT

JWT는 클라이언트가 사용자 이름과 비밀번호 정보를 보내고 이 정보들이 데이터베이스의 값과 일치하면 서버가 토큰을 보내는 방식으로 작동한다. (참고)



📡 Get User Data

기존 사용자 값을 사용하여 서버에서 데이터를 가져오기

만약 로그인이 되어 있다면 로그인된 사용자의 데이터를 얻기 위하여 어떤 사용자의 데이터를 원하는지 알려주기 위해 user.id를 보내야하므로 user값을 알아야한다.

  • queryKey : queryKeys.user

  • queryFn : getUser (사용자 데이터를 얻기 서버 요청을 하는 함수)

1️⃣ 사용자의 로그인 여부에 따라 사용자 객체 또는 null 값이 될 수 있다.

2️⃣ 로그인한 사용자가 없으면 서버에 가지 않고 null을 반환한다.

3️⃣ 로그인한 사용자가 있다면 서버로 이동하여 로그인한 사용자의 user.id 데이터를 가져온다.

4️⃣ user.id에 대한 데이터를 가져올 권한이 있는지 서버에 JWTHeader 포함시켜 확인 요청을 한다.

// useUser.ts
async const getUser = (user: User | null): Promise<User | null> => { // 1️⃣ 번
 if (!user) return null; // 2️⃣ 번
 const { data }: AxiosResponse<{ user: User }> = await axiosInstance.get( // 3️⃣ 번
   `/user/${user.id}`,
   {
     headers: getJWTHeader(user), // 4️⃣ 번
   },
 );
 return data.user;
}

...

export const useUser = (): UseUser => {
 const { data: user } = useQuery(queryKeys.user, () => {
   getUser(user);
 });


⭐️ setQueryData

🧐 하지만 애초에 user가 정의되어 있지 않다면?

만약 애초에 로그인을 하지 않았다면 위에서 요청할 때 사용하는 user값은 null값을 반환할 것이고 어떤 데이터도 얻지 못할 것이다.


쿼리 캐시 값을 설정하자!

인증처리를 할 때 쿼리 캐시 값을 설정해놓는다면 useQuery함수 실행 시 즉, 사용자 데이터를 가져올려할 때 사용할 user값이 생길 것이다.

👍 그리고 이때 사용하는 것이 바로 queryClient.setQueryData 이다.

setQueryData을 사용하면 쿼리키와 값을 가져와 쿼리 캐시에 해당 키에 대한 값을 설정할 수 있다.



🔐 When LogIn

signin 또는 signUp 함수가 실행되고 인증에 대한 요청이 정상적으로 반환되었을 때 setQueryData를 통해 쿼리캐시에 쿼리키와 값을 저장.

//useAuth.ts
async const authServerCall = (urlEndpoint, email, password) => {
    try {
       ...
      if ('user' in data && 'token' in data.user) { // 로그인 정상 동작
       		...
        updateUser(data.user); // 유저 정보 쿼리캐시에 저장.
      }
    } catch (errorResponse) {...}
  }

...

async const signin = (email: string, password: string): Promise<void> => {
    authServerCall('/signin', email, password);
  }
async const signup = (email: string, password: string): Promise<void> => {
    authServerCall('/user', email, password);
  }
    

updateUser의 실행으로 유저정보 캐시에 업데이트

// useUser.ts
const updateUser = (newUser: User): void => {
    queryClient.setQueryData(queryKeys.user, newUser);
  }

💡 useAuth는 signin, signup, signout 함수들이 서버와 통신하는 역할을 하는 커스텀 훅이다.



🔒 When Logout

signOut함수가 실행시 정상적으로 로그아웃이 완료될 때 setQueryData를 통해 쿼리캐시에 사용자정보를 없애기

//useAuth.ts
const signout = (): void => {
    // clear user from stored user data
    clearUser();
    toast({
      title: 'Logged out!',
      status: 'info',
    });
  }

clearUser의 실행으로 캐시에 존재했던 유저정보를 null로 설정

// useUser.ts
 const clearUser = () => {
    queryClient.setQueryData(queryKeys.user, null);
  }


✚ onSuccees

위에서 설정한 것들은 쿼리캐시에 사용자정보를 저장하여 사용자 데이터를 가져오기 위한 방법들이었다.

하지만 사용자정보가 캐시에만 저장됬을뿐 실제로는 데이터 보존이 이뤄지지 않아 새로고침을 하면 로그아웃되었다고 가정되는 경우가 발생한다.


사용자 데이터를 로컬스토리지에 저장

이를 해결하기 위하여 useQuery가 실행될 때 해당 로컬 스토리지의 초기 데이터를 사용하면 될 것이다.


onSuccess 사용

그렇다면 애초에 로컬 스토리지가 사용자 데이터로 채워져있어야 useQuery가 실행될 때 데이터를 사용할 수 있다는 것이므로 onSuccess 콜백으로 로컬 스트리지를 업데이트하면 된다.

export const setStoredUser = (user: User): void => {
  localStorage.setItem(USER_LOCALSTORAGE_KEY, JSON.stringify(user));
}
export const clearStoredUser = (): void => {
  localStorage.removeItem(USER_LOCALSTORAGE_KEY);
}

...

const { data: user } = useQuery(queryKeys.user, () => getUser(user), {
    onSuccess: (received: User | null) => {
      if (!received) {
        clearStoredUser();
      } else {
        setStoredUser(received);
      }
    },
  });


✚ InitialData

onSuccess는 setQueryData를 사용해서 캐시에 데이터를 저장하고, useQuery를 통해 데이터를 가져오는 동작을 한후에 실행된다.


💡 잠깐
앞서 공부할때도 map함수로 배열데이터를 출력하려 했지만, 오류가 나는 경우를 본적 있다. 왜냐하면 반드시 값이 완벽히 들어온 상태에서 데이터 출력이 일어나야하는데, 데이터가 하나씩 서버에서 배열로 들어오고 있는 상태이기 때문에 초기값으로 빈배열을 주어 일단 데이터를 출력하게 만들었었다.

onSuccess의 동작도 결국 로컬스토리지에 값을 저장하고, 삭제하는 동작만 할뿐이고, useQuery에서 사용하기 위한 초기값을 설정해주진 않았다.


initialData를 사용하자!

initialData는 초기 데이터를 캐시에 추가하고 싶을 때 사용한다.(참고)

export const getStoredUser = (): User | null =? {
  const storedUser = localStorage.getItem(USER_LOCALSTORAGE_KEY);
  return storedUser ? JSON.parse(storedUser) : null;
}

...

 const { data: user } = useQuery(queryKeys.user, () => getUser(user), {
    initialData: getStoredUser,
    ...
    },
  });




✚ Dependent Queries

어떠한 서비스를 사용하든 분명 사용자에 대한 권한이 필요한 기능들이 있을 것이다.

쉽게 말해 사용자가 로그인했을 때만 해당 사용자에 대한 정보를 보여주거나, 어떠한 동작이 가능하고 만약 로그아웃 했을 때에는 정보가 출력되지 않고 동작이 비활성화되는 경우들이 있을 것이다.

예를 들어 로그인이 꼭 해야만 사용할 수 있는 예약서비스가 있다. 그렇다면 예약서비스는 로그인 서비스에 의존적일 것이다. 왜냐하면 로그인 서비스의 동작에 따라 예약서비스가 활성화될지, 비활성화될지 정해지기 때문이다.


이때 사용하면 되는 것이 바로 Dependent Queries이다. (참고)

만약 로그인서비스의 user 값이 존재한다면 true, 존재하지 않는다면 false가 되어 이 boolean값에 따라 예약서비스의 활성유무가 정해지는 것이다.

// useUserAppointment.ts (예약서비스 컴포넌트)
export function useUserAppointments(): Appointment[] {
  const { user } = useUser();

  const fallback: Appointment[] = [];
  const { data: userAppointments = fallback } = useQuery(
    'user-appointments',
    () => getUserAppointments(user),
    { enabled: !!user }, // 쿼리는 user값이 존재할 때까지 실행되지 않는다.
  );

  return userAppointments;
}

💡 잠깐) 왜 user값이 false일 때 null 값을 반환하도록 허용했을까?

왜냐하면 경쟁상태(Race Condition)이 있거나 고려하지 못한 요소들에 대비해 보수적으로 프로그래밍하는 것이다. 당연히 user값이 false라는 것은 null값을 갖는다는 것이지만, 어떠한 상황에 의해 user값의 변동이 될지 혹시 모르는거기 때문에...

👉 Race condition이란 두 개 이상의 프로세스가 공통 자원을 병행적으로(concurrently) 읽거나 쓰는 동작을 할 때, 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 같지 않고 달라지는 상황을 말한다.

Race의 뜻 그대로, 간단히 말하면 경쟁하는 상태, 즉 두 개의 스레드가 하나의 자원을 놓고 서로 사용하려고 경쟁하는 상황을 말한다.



✚ removeQueries

앞서 말했듯이 로그인시에만 예약서비스가 활성화되야하므로 반대로 로그아웃 시 예약쿼리를 제거하는 방법에 대해 알아보자.

removeQueries을 사용하자

removeQueries 캐시에서 쿼리를 제거하는 데 사용할 수 있다.

💡 잠깐) 그러면 아까 로그아웃할 때 setQueryData를 null로 설정하여 사용자 정보를 없애는 것이 아니라 removeQueries를 사용하면 되지 않았나요?

데이터를 유지하기 위해 로컬스토리지에 데이터를 업데이트하는 동작을 위해 onSuccess 콜백을 사용하였는데, 이는 setQueryData가 발생시키기 때문이다. 즉, setQueryData가 실행되어야 다음에 onSucceess이 실행된다. 반면 removeQueries 다음에는 onSuccess가 실행되지 않는다.


예약서비스에서는 로그인서비스에 대해 동작할 onSuccess가 필요없다!

왜냐하면 예약서비스에 대한 예약정보가 로컬스토리지에 저장되어있거나 삭제시키는 동작이 별도로 필요 없기 때문이다. 따라서, removeQueries를 사용하여 캐시에 존재하는 값만 삭제시켜주자.

로그아웃할 때 동작하는 함수인 clearUser에 removeQueries 추가. 이때 쿼리키는 예약쿼리에 대한 쿼리키인 'user-appointments'

 const clearUser = () => {
    queryClient.setQueryData(queryKeys.user, null);
    queryClient.removeQueries('user-appointments');
  }


user-appointment 가 로그아웃시 disabled 되는 모습

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글