[TIL 0424] refreshToken 적용하기 / 새로고침시 토큰 유지하는 방법 / Promise와 Observable

zitto·2023년 4월 24일
1

TIL

목록 보기
64/77
post-thumbnail

💡 refreshToken 적용하기

[apollo-client의 쿼리 방식]

import { gql, useApolloClient, useLazyQuery, useQuery } from "@apollo/client";
import { wrapAsync } from "../../../src/commons/libraries/asyncFunc";
const FETCH_USER_LOGGED_IN = gql`
  query {
    fetchUserLoggedIn {
      email
      name
    }
  }
`;
export default function LoginPage(): JSX.Element {
  // 1. 페이지 접속 후 자동으로 데이터에 받아지고(data는 글로벌스테이트에 저장), 리렌더링됨
  // const { data } = useQuery<Pick<IQuery, "fetchUserLoggedIn">>(FETCH_USER_LOGGED_IN);
  // 2. 버튼클릭시, 데이터에 받아지고 (데이터는 글로벌스테이트에 저장) 그리고 리렌더링됨
  // const [실행할함수, { data }] = useLazyQuery(FETCH_USER_LOGGED_IN);
  // 3. axios처럼 사용하는 방법(데이터는 글로벌스테이트)
  // const client = useApolloClient();
  // const result = client.query(); //axios.get() 과 비슷
  const client = useApolloClient();
  const onClickBtn = async (): Promise<void> => {
    const result = await client.query({
      query: FETCH_USER_LOGGED_IN,
    });
    console.log(result);
  };
  // 5초 지난 뒤에 클릭하기
  return <button onClick={wrapAsync(onClickBtn)}>CLICK!</button>;
  // return <>{data?.fetchUserLoggedIn.name}님 환영합니다!</>;
}
  • useQuery : 페이지에 접속하면 자동으로 바로 실행되어 data라는 변수에 fetch해온 데이터를 담아주며, 리렌더링 된다.
  • useLazyQuery : useQuery를 원하는 시점에 실행(버튼 클릭시)후 fetch해온 데이터를 data변수에 담아준다.
  • useApolloClient : 원하는 시점에 실행 후 fetch해온 데이터를 원하는 변수에 담을 수 있다. 따라서 axios 같은 느낌으로 사용이 가능함.

[RefreshToken 실습]

refreshToken을 받아오기 위해서는 accessToken이 만료되어야 한다.
토큰 만료시간이 짧은 API를 이용한 실습하기 => loginUserExample

  • 설치명령어
    yarn add graphql-request

  • src/apollo/index.tsx 파일
import {
  ApolloClient,
  ApolloLink,
  ApolloProvider,
  fromPromise,
  InMemoryCache,
} from "@apollo/client";
import { createUploadLink } from "apollo-upload-client";
import { useEffect } from "react";
import { useRecoilState } from "recoil";
import { accessTokenState } from "../../../commons/stores";
import { onError } from "@apollo/client/link/error";
//분리한 함수를 import 해주기
import { getAccessToken } from "../../../commons/libraries/getAccessToken";
const GLOBAL_STATE = new InMemoryCache(); //아래쪽에서 리렌더되든말든 얘는 계속 유지됨
interface IApolloSettingProps {
  children: JSX.Element;
}
export default function ApolloSetting(props: IApolloSettingProps): JSX.Element {
  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  const errorLink = onError(({ graphQLErrors, operation, forward }) => {
    // 1. 에러캐치(토큰만료)
    if (typeof graphQLErrors !== "undefined") {
      //가급적 docs참고하기
      for (const err of graphQLErrors) {
        // 1-2. 해당에러가 토큰만료에러인지 체크(UNAUTHENTICATED)
        if (err.extensions.code === "UNAUTHENTICATED") {
          return fromPromise(
            // 2. refreshToken으로 accessToken재발급 받기
            getAccessToken().then((newAccessToken) => {
              // 3. 재발급 받은 accessToken으로 방금 실패한 쿼리의 정보 수정하고 재시도하기
              setAccessToken(newAccessToken ?? "");
              // 쿼리를 재생성
              operation.setContext({
                headers: {
                  ...operation.getContext().headers, // Authorization: Bearer "token" => 만료된 토큰 추가되어 있는 상태
                  Authorization: `Bearer ${newAccessToken ?? ""}`, //3-2. 토큰만 새것으로 바꿔치기 Authorization만 덮어쓰기 됨
                },
              });
            })
          ).flatMap(() => forward(operation)); // 3-3. 방금 수정한 쿼리 재요청하기
        }
      }
    }
  });
  const uploadLink = createUploadLink({
    uri: "https://backend-practice.codebootcamp.co.kr/graphql",
    headers: { Authorization: `Bearer ${accessToken}` },
    credentials: "include",
  });
  const client = new ApolloClient({
    link: ApolloLink.from([errorLink, uploadLink]), //순서중요
    cache: GLOBAL_STATE, // 컴퓨터의 메모리에다가 백엔드에서 받아온 데이터 모두 임시로 저장해놓기 => 나중에 알아보기
  });
}

uploadLink의 요청을 보내실 때 uri 경로를 http에서 https로 바꾸고,
민감한 정보 포함을 승인한다는 뜻의 credentials: “include” 옵션을 추가한다.

만일, credentials: “include” 이 없다면,
refreshToken을 쿠키에 못담을 뿐만아니라 쿠키에 담겨있는것들도 백엔드로 전송이 되지 않는다.

graphQLErrors : 에러들을 캐치
operation : 방금전에 실패했던 쿼리가 뭐였는지 알아둠.
forward : 실패했던 쿼리들을 재전송

  • src/commons/libraries/getAccessToken.ts
    재사용될 수 있는 부분이라 따로 빼놓기
import { gql, GraphQLClient } from "graphql-request";
import { IMutation } from "../types/generated/types";
const RESTORE_ACCESS_TOKEN = gql`
  mutation {
    restoreAccessToken {
      accessToken
    }
  }
`;
export const getAccessToken = async (): Promise<string | undefined> => {
  try {
    const graphQlClient = new GraphQLClient(
      "https://backend-practice.codebootcamp.co.kr/graphql",
      { credentials: "include" }
    );
    const result = await graphQlClient.request<
      Pick<IMutation, "restoreAccessToken">
(RESTORE_ACCESS_TOKEN);
    const newAccessToken = result.restoreAccessToken.accessToken;
    return newAccessToken;
  } catch (error) {
    if (error instanceof Error) console.log(error.message);
  }
};

▼ API 요청 순서 확인

1️⃣ 토큰 만료로 API 요청 실패

2️⃣ restoreToken 요청

3️⃣ AccessToken 재발급 받은 뒤 API 재요청



💡 새로고침시 토큰 유지하는 방법

쿠키에 없는 이유?
새로고침 시 엑세스토큰 받아오는 방법
로컬이 아닌 재발급받아서 리코일에 넣어줌

  • src/commons/stores/index.ts
import { atom, selector } from "recoil";
import { getAccessToken } from "../libraries/getAccessToken";
export const accessTokenState = atom({
  key: "accessTokenState",
  default: "",
});
export const restoreAccessTokenLoadable = selector({
  key: "restoreAccessTokenLoadable",
  get: async () => {
    const newAccessToken = await getAccessToken();
    return newAccessToken;
  },
});
  • src/components/commons/apollo/index.tsx
import {
  accessTokenState,
  restoreAccessTokenLoadable,
} from "../../../commons/stores";
import { onError } from "@apollo/client/link/error";
import { getAccessToken } from "../../../commons/libraries/getAccessToken";
const GLOBAL_STATE = new InMemoryCache(); //아래쪽에서 리렌더되든말든 얘는 계속 유지됨
export default function ApolloSetting(props: IApolloSettingProps): JSX.Element {
  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  const loadable = useRecoilValueLoadable(restoreAccessTokenLoadable);
  useEffect(() => {
    // 1. 기존방식(refreshtoken 이전)
    // const result = localStorage.getItem("accessToken") ?? "";
    // setAccessToken(result);
    // 2.새로운 방식(refreshtoken 이후)
    // void getAccessToken().then((newAccessToken) => {
    //   setAccessToken(newAccessToken ?? "");
    // });
    // 3. 최종방식(함수 공유)
    void loadable.toPromise().then((newAccessToken) => {
      setAccessToken(newAccessToken ?? "");
    });
  }, []); //새로고침했을 때 꺼내서 recoil에 넣어줘!

💡 Promise와 Observable

사용예제)
1. 연속적인 페이지 클릭
2. 연속적인 검색어 변경

✅ observable & flatmap 실습

flatMap은 apollo-client에서 지원하는 flatMap(자바스크립트의 메소드와는 다름)

  • 설치명령어
    yarn add zen-observable
    yarn add @types/zen-observable --dev

  • index.tsx
// import { Observable } from "@apollo/client";
// import { reject } from "lodash";
// import { resolve } from "path";
import { from } from "zen-observable";
export default function ObservableFlatMapPage(): JSX.Element {
  const onClickBtn = (): void => {
    // new Promise((resolve, reject) => {});
    // new Observable((observer) => {});
    // from을 hover해보면 observable이 나온다.
	// Promise 세개가 Observable로 적용됨.
    from(["1번 useQuery", "2번 useQuery", "3번 useQuery"]) //fromPromise
      .flatMap((el: String) =>
        from([`${el} 결과에 aaa 적용`, `${el} 결과에 bbb 적용`])
      )
      .subscribe((el) => {
        console.log(el);
      }); //최종적으로 여기서 6번찍힘.
  };
  return <button onClick={onClickBtn}>CLICK</button>;
}
  • src/components/commons/hocs/loginCheck.tsx
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useRecoilValueLoadable } from "recoil";
import { restoreAccessTokenLoadable } from "../../../commons/stores";
export const LoginCheck = (Component: any) => (props: any) => {
	const router = useRouter();
	const loadable = useRecoilValueLoadable(restoreAccessTokenLoadable);
  	useEffect(() => {
    	void loadable.toPromise().then((newAccessToken) => {
      		if (newAccessToken === undefined) {
        		alert("로그인 후 이용 가능합니다!");
        		void router.push(`/section30/30-01-login-refreshtoken`);
      }
    });
  });
  return <Component {...props} />;
};

fromPromise란 ❓

→ onError 라는 함수는 return타입으로 Observable타입을 받고 있다.
하지만, 우리가 리턴해주는 값은 promise이기 때문에
Observable타입으로 바꿔줄 도구가 필요하다.**
해당 도구 역시 아폴로에서 지원해주고 있으며, 그 도구가 바로 fromPromise 인 것!
정리 하자면, fromPromisepromise타입을 Observable타입으로 바꿔주는 도구 이다.
즉, from과 비슷하다고 보면 된다.

  1. 반응형프로그래밍(reactice extension javascript)
  2. zen-observable

쿠키에 있는 값이 첨부되어 백엔드로 날아감!

profile
JUST DO WHATEVER

0개의 댓글