✅ [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에 넣어줘!
사용예제)
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
인 것!
정리 하자면,fromPromise
는promise타입을 Observable타입으로 바꿔주는 도구
이다.
즉, from과 비슷하다고 보면 된다.
쿠키에 있는 값이 첨부되어 백엔드로 날아감!