12/12 refreshToken

김하은·2022년 12월 12일
0

기존 로그인방식:
accessToken을 발급받으면 로컬스토리지에 저장, 만료시간이 1시간이라 1시간후에 발급받은 토큰을 직접지우고 재로그인하여 다시토큰을 받아와야했음.

accessToken
: 로그인--> 벡엔드에 로그인확인요청(로그인 API요청) -->메모리세션에 저장, 세션아이디를 보내줌 --> 이 세션아이디를 브라우저에 저장, 후에 유저정보가 필요한 API요청 시 이 세션아이디를 함께 보냄.

문제: 많은 유저들의 정보를 메모리에 저장하기에 메모리 부족.
so, scale-up(벡엔드 메모리 업그레이드) 또는 scale-out(벡엔드 컴퓨터 여러대준비) 두가지 방법중에 하나를 사용해 해결한다. scale-out의 방법이 훨씬 좋다. 다만 로그인 아이디가 세션에 저장되는데, 해당 컴퓨터 세션에 저장되어있지 않으면 재 로그인이 필요하게된다. ==> 이러한것을 상태가있는( 세션을 가지는 ) 스테이트 풀 이라고한다.

그렇다면 상태가 없는 스테이스 리스로 만들어야한다.

scale-out편하게 세션을 DB로 옮기기!!

DB에 세션 테이블을 만들어 세션 정보를 옮긴다. 이렇게 트래픽을 분산시킨다.

==> 라운드 로빈으로 원에서 돌리기
리스트 커넥션으로 적은 접속자가 있는 쪽으로 몰기.

그런데 백엔드쪽의 트래픽분산은 해결되었으나, DB의 문제는 해결되지 않았다.

DB에 모든 세션 데이터가 담김으로써 이곳의 트래픽이 몰린다.
이것을 세션테이블 병목현상 이라고 한다.

DB의 scale-out을 사용하는 방법은 달랐다.

백엔드의 경우 git clone등으로 프로그램 몇을 붙여넣기한다지만 DB는 워낙 양이 많아 여러DB에 같은 많은양의 정보를 일일이 담을 수 없기 때문이다.

따라서 DB의 경우에는 테이블을 잘라 나누는 방법을 사용한다. 수평 파티셔닝, 수직 파티셔닝 이 있는데 이때 수평 파티셔닝은 샤딩 이라고도 한다.

수평 파티셔닝 = 샤딩

파티셔닝으로 1 ~ 1000명 / 1001 ~ 2000 명
이렇게 데이터를 각각의 DB에 나눠 담으면 해당DB에 가 데이터를 긁어오면된다.
이렇게 될 경우 DB의 병목현상은 일어나지 않는다.

그런데! DB가 디스크에 저장된다. 즉, 디스크에서 긁어오는 Disk InOut 방식이다. 거리상 메모리보다 먼 것이 디스크라 매번 갔다오는 속도가 느리다.

따라서 redis라는 메모리에 저장하는 방법을 사용했다.
메모리기반데이터기반 저장소에 저장하는것이다.
또는 redis에도 가지않고 세션데이터를 객체로 만들어 암호화하고, DB까지 가지도 않고 벡엔드에서 복호화하여꺼내 가져온다.
이때 암호화하고 복호화하는 이것을 JWT라고한다. JSON WEB TOKEN 이라고 한다. 즉, 객체처럼 생긴 웹에서 사용하는 토큰이라는 말이다.


브라우저에 저장되는 공간
-로컬스토리지
-세션스토리지
-쿠키
-변수/state

토큰을 받아와 저장하는 과정: (Authentication) 인증

토큰을 사용하여 정보확인하는 과정(유저에대한 정보등 불러오기) : 인가(Authorization)


지금까지는 accessToken을 발급받으면 로컬스토리지에 저장하고 만료시간인 한시간이 지나면 토큰을 지우고 재발급 받아야했다.

RefreshToken 사용하기

과정: 브라우저 에서 로그인 --> 벡엔드로 JWT만듬(accessToken,refreshToken) --> 이 만들어진 토큰을 브라우저로 돌려줌.

Recoilstate에 저장. (글로벌스테이트에 저장)

refreshToken은 벡엔드에서 쿠키에 넣어 보내준다. 그러나 이 쿠키엔 옵션을 설정해줄 수 있는데, httpOnly라는 옵션과, secure라는 옵션이 있다.

httpOnly: http라는 리퀘스트를 통해서만 주고받을 수 있다.즉 자바스크립트로 건들수 없기에 document.cookie로 꺼낼 수 없다.
secure : https에서만 가능.

브라우저에서 Bearer accessToken 과 관련 API를 보내는데 이 과정이 바로 인가. 만약 accessToken의 만료가 되었다면 토큰만료에러가 뜸
--> 이때 refreshToken을 사용하게된다.!


실습하기

1.토큰만료를 인식한다.
2.FetchLoggedIn 등의 요청을 보내는데, 실패시 에러잡는데 if문으로 토큰만료시의 에러를 잡음.
restoreAccessToken을 통해 토큰재발급

3.재발급받을 시 받은 토큰으로 방금 실패했던 에러를 잡아 요청재시도.

---> 유저에게는 굉장히 빠르게 일어나기에 감지안됨.


구조도
useQuery날리기. 즉, axios처럼 날려 버튼클릭시 API요청 ==> 결과를 result에 담기.

|-- useQuery ==> 페이지 접속시 자동으로 data에 받아지고 리랜더링됨.
|-- useLazyQuery ==> 내가원할때! 버튼클릭등으로 직접실행하면 data에 받아지고
|리랜더링됨
|-- useApolloClient ==> 내가 원할때 요청.단, axios와 동일하게결과를 변수에 담아 사용가능.

const[myquery,{data}] = useLazyQuery()
==> 버튼을 클릭시 myquery가 실행되고, 그 데이터가 자동으로 data에 담김 --> 그 다음 리랜더링됨. axios처럼 변수에 담을 수는 없다. 자동으로 data에 담기고 담긴것으로 자동으로 리랜더링된다.

우리가 사용할것.
axios와 동일한 xonst client = useApolloClient()

client라는 변수에담아 불러올 수 있다.

export default function LoginSuccessPage() {

  const client = useApolloClient();

  const onClickButton = async () => {
    const result = await client.query({
      query: FETCH_USER_LOGGED_IN,
    });
    console.log(result);
  };

  return (
    <button onClick={onClickButton}>클릭하세요!!</button>
    // <>{data?.fetchUserLoggedIn.name}님 환영합니다</>
  );
}

client의 변수에 담아 버튼클릭시 쿼리가 실행되고 그 결과를 또 변수에담아 보여주기.

이 client도 Promise를 리턴하기에 async/await사용가능하다.

Promise이기에 마이크로큐에 들어가고, 실행중 await를 만나면 그것을 감싸고있는 async funcion도 같이 마이크로큐에 들어간다.


로그인 구현 심화:

좀 더 빨리 결과를 보기위해 만료시간이 짧게 정해진 LoginUserExample이라는 쿼리문을 사용한다.

아폴로 세팅부분에서 진행 ==> 아폴로의 에러부분을 잡기위함.

apollo요청시에 발생한 에러들을 받아올 수 있다.

@apollo/client/link/error라는 부분에서 onErrors라는 것을 imort하여 사용한다.

import {onErrors} from '@apollo/client/link/error'
const errorLink = onErrors(({graphQLErrors,operation,foword})=>{
	  if (graphQLErrors) {
      // graphQLErrors 가 있니?(여러개임)
      for (const err of graphQLErrors) {
        // 아폴로 클라이언트 독스에서 말한 방법.graphQLErrors들을 err에 담아 한번씩 반복하겠다
        // 1-2 :  해당에러가 토큰만료에러인지 체크하기(UNAUTHENTICATED 라는 에러가 들어옴.)
        if (err.extensions.code === "UNAUTHENTICATED") {
                    // 2-1 : refresh 토큰으로 accessToken 재발급 받기
})

graphQL에러를 캐치하고,
operation: 방금전 실패했던 쿼리를 의미
foword: 다시수정한 쿼리의미

이때 문제점: ApolloProvider에서만 client요청이 들어가 사용이 가능했다.

그런데 방금 작성한 부분은 그 밖에 위치하기에 axios처럼 작성해주는 방법이 있지만, 복잡하다.
다행이 제공되는 라이브러리가 있다.

yarn add graphql-request

사용하는 방식은 두가지가 있다.

방식1: request("엔드포인트",쿼리)
방식2: GraphQLClient방식

헤더부분에 옵션들 사용해야하기에 GraphQLClient방식을 사용한다.

refreshToken으로 accessToken재발급받는 방식.

GraphQLClient ("엔드포인트인 그래프큐엘주소")

refreshToken으로 accessToken을 쿠키에담아 받아올 때 httponly라는 옵션과 secure라는 옵션이있다. secure라는 옵션을 사용하기 위해서는 주소가 https여야하니 해당 주소를 https로 바꾼다.

--> 실습때는 아직 secure설정이 안되어있어 현 오프라인 그래프큐엘주소를 사용하였다.

그리고 쿠키에서 중요정보를 받아오는 것이니 credientials:"inclued"라는 옵션을 추가해준다.(중요정보 포함 이라는 의미)

기존 주소들 다 s를 붙인 https를 해줘야 credientials:"inclued"추가가 가능하다.

refreshToken으로 accessToken재발급받기부분의 라이브러리를 따로빼었다.

  • gql은 apollo/client에서도 import 가능하고, graphql-request에서도 import 가능하니 선택해 적용한다.
const RESTORE_ACCESS_TOKEN = gql`
  mutation restoreAccessToken {
    restoreAccessToken {
      accessToken
    }
  }
`;
export const getAccessToken = async () => {
  try {
    const graphQLClient = new GraphQLClient(
       "https://그래프큐엘주소"
      {
        // /graphql이라는 단일 앤드포인트를 가진 단일 rest-API의 post방식이다.
        credentials: "include",
      }
    );
    const result = await graphQLClient.request(RESTORE_ACCESS_TOKEN);
    const newAccessToken = result.restoreAccessToken.accessToken;
    return newAccessToken; // 받은 newAccessToken을 넘겨주기
  } catch (error) {
    if (error instanceof Error) console.log(error.message);
  }
};

따로함수를 만들어 살펴보니 이 함수또한 Promise를 리턴한다. 따라서 .then()으로 받을 수 있다. 새로받은 accessToken을 넣어준 변수 newAccessToken을 return 으로 넘겨주면 받는 쪽에서 사용이 가능하다.

.then()에 콜백함수를 담고 매개변수로 newAccessToken을 사용했는데 매개변수이니 아무거나 사용해도 된다는것을 명심.

콜백함수내에 기존작성한 부분들을 넣어주면되는데, 다 넣었는데도 빨간줄이 뜬다. 이부분을 일단 return fromPromise()로 감싸고, .flatMap(()=>foward(operation))이라고 넣는다.앞에서도 말했다시피 operation은 보냈는데 실패했던 쿼리정보들을 받고있고, foward는 그정보들을 refreshToken을 사용해 받아온 새 accessToken으로 다시 요청을 날리는것을 의미한다.

operation.SetContext 로 실패한 쿼리정보를 수정할 수 있고, 우리는 그중에 header부분의 accessToken을 새로 발급받은 토큰으로 바꿔줘야한다.

  // 3-1 : 재발급 받은 accessToken 으로 방금실패한 쿼리 정보 수정하기(기존의 accessToken 삭제 하기)
              if (typeof newAccessToken !== "string") return;

              operation.setContext({
                // 실패했던 쿼리정보 정보 수정
                headers: {
                  // 해더정보중 딱 하나만 바꿈. 모든 해더 정보 가져옴
                  ...operation.getContext().headers, // 기존데이터중 헤더 부분을 다 가져와(스프레드)(만료된 토큰이 추가되어있는 상태) .
                  Authorization: `Bearer ${newAccessToken}`, // 그 중 Authorization 만 새 accessToken으로 변경
                },
              });
            })
          ).flatMap(() => forward(operation));

일단 모든 header의 정보를 가져와야하니 headers하고 객체에 operation으로 받아온 header의 정보를 스프레드연산자로 전부 가져와 그중의 Authorization의 'Bearer ${newAccessToken}'으로 Bearer부분을 accessToken을 새로 발급받은 토큰으로 바꾼다.


fromPromise와 flatMap

이것은 옵져버블과 관련이 있다.

옵져버블: 반응형 프로그래밍이라고 부름.

프로그래밍

|-- 함수형 프로그래밍
|-- 반응형 프로그래밍

함수형 프로그래밍 :
.map((el)=>).filter().find().replace()
.. 이런식으로 기능들을 체이닝하는것. 위의것은 만들어진것들이고, 함수현 프로그래밍은 우리가 만든 기능들을 체이닝해서 놓는것을 의미한다.
어떤 input에 대한 해당 out이 항상 동일하다.

반응형 프로그래밍:
Promise: 비동기작업 도와줘
옵져버블: 연속적인 비동기작업 도와줘 + α

연속적인 비동기작업?

페이지 클릭시 만약 첫번째로 3페이지를 그리고 바로 5페이지를 클릭했다.
페이지를 조회후에 응답이 오는데 클릭한순서대로 응답이 처리되는것이 아니라, 여러 문제로 벡엔드에서 맨마지막에 클릭한 5페이지의 응답이 먼저 처리되어 보내지고, 그다음에 처리되는 3페이지가 다시 리랜더링되면서 최종적으로는 사용자는 5페이지를 클릭했는데 3페이지를 보게되는 현상이 나타난다.
이때는 이전에 요청했던 3페이지 요청을 취소하는것이 필요하다.

이런것을 '연속적인 비동기작업' 이라고한다.

== Promise를 연속적으로 처리: Observable

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

이러한 옵져버블로 프로그래밍되어있는 라이브러리가 바로 apolloClient이다.따라서 아까 return fromPromise로 묶기전에는 빨간줄이 그려져있던것이다.


fromPromise

라이브러리:
Reactive-X =>rxjs라는 라이브러리가 있다. 반응형 프로그래밍을 쉽게 적용할 수 있다.
ApolloClient => 여기 자체에서 zen-observable 이라는 라이브러리를 사용해 설치하지않고 import만 하여 사용가능
zen-observable => 이것은 설치하여 적용시 사용하는 라이브러리

zen-observable 설치

yarn add zen-observable

yarn add @types/zen-observable --dev

zen-observable 사용하기

import { from } from "zen-observable";

zen-observable 에 from을 import한다.

버튼을 클릭시 옵져버블이 실행되게 만든다.
form()안에 쿼리를 여러개 적으면 연속적인 쿼리를 날리는 것이다. 따라서 안에는 배열로 적는다.그리고 .flatMap을 사용해 from으로 보낸 결과를 받아 적용시키는 것이다. 그리고 이 각각의 결과를 다시 from에 담아 subscribe를 통해 최종적으로 결과를 볼 수 있다.

(이때의 flatMap은 자바스크립트의 flatMap과는 다름)

export default function ObservarbleFlatMapPage() {
  const onClickButton = () => {
    // new Promise(()=>{})// 자바스크립트에서 제공되고있음
    // new Observable(()=>{ })// 아폴로 클라이언트가 사용하는 zen-observarble을 아폴로에서 import 하여 사용
    // prettier-ignore
    from(["1번 useQuery", "2번 useQuery", "3번 useQuery"]) // fromPromise와 같은말
    .flatMap((el:string) =>from([`${el} 결과에 qqq적용`,`${el} 결과에 zzz적용`]))
    .subscribe((el)=>console.log(el)) // 최종 실행해줘
  };

  return <button onClick={onClickButton}>클릭</button>;
}

그렇다면 fromPromise는? ==> Promise를 옵져버블로 바꿔줘 라는 의미이다. onError리턴타입이 옵져버블이기에 fromPromise를 적용한것.
그리고 flatMap을 사용해 받아온 결과를 최종적으로 적용시킨 것이다.


트러블슈팅

:어떠한 문제가 발생했을 시 그 원인을 찾아 제거하는것.
웹서비스 문제해결을 위해서는 벡엔드 + 네트워크 + 브라우저 전부 포인트 지점을 알아야 트러블 슈팅이 가능하다.

0개의 댓글