12/15 Optimistic-UI

김하은·2022년 12월 19일
0

지난번까지는 리펙토링을 하여 좀 더 최적화해 사용자에게 빨리 보여주는 방법을 사용해보았다.
그러나 더이상 최적화할 수 없을 경우에는 시각적으로 속이는 방법을 사용할 수도 있다.

옵티미스틱-ui 라는 것을 사용한다.
긍정적 ui라고도 한다는 말을 보고 처음에는 의아해했다.

일단 좋아요 구현부터 예시로 보았다.

기존에는 좋아요1을 클릭하면 벡엔드로 해당 API요청이 날라가고 DB의 해당테이블에 저장, 그 결과를 받아 브라우저로 와서 1을 올려주는 식이었다.

받아와서 보여준다는 과정이라는 말에서 알 수 있듯이 느리게 그려진다. 실제로 개발자도구에서 네트워크를 slow-3G를 적용하면 진짜 엄청 느린것을 볼 수 있다.

개발을 한다면 모든 사용자의 입장에서 봐야한다.
따라서 느린 컴퓨터를 사용하고있는 경우도 당연히 봐야하기때문에 이러한 방법도 사용해가며 해당 구현이 어떻게 작동되는지를 보는것이다.

그렇다면 옵티미스틱을 적용하면 어떻게되나?

옵티미스틱-UI: 낙관적-UI, 긍정적-UI

옵티미스틱을 사용할 경우

좋아요를 누를시 API요청과 함께 일단 1이 증가되어진것으로 화면에 보여짐. --> 성공했을것이라고 가정하고 미리 올려주는 것이다.
그 후 실제로 해당결과를 DB에 저장하며 그 결과를 받아오면서 덮어쓰기가 진행되는데, 만약 실패시에는 원래값으로 초기화된다.

이름에서 알 수 있듯, 성공했겠지~ 라고 낙관적으로 미리 한단하여 해당부분을 실행을 하고 DB에서 저장되어 결과가 오면 그 값으로 다시 바꿔치기등이 일어나는것!

이렇게 좋은기능이 있다니!
그러나 남용해서는 안된다고한다.

어떤경우에 이용할까?

  1. 실패하더라도 타격이 거의 없을시
  2. 성공할 가능성이 99%가 넘을시

결제의 경우
포인트 충전 결제를 하면 결제 DB에 기록됨 --> 회원테이블의 해당회원포인트 증가 ---> 둘중 하나라도 실패하면 결제가 꼬인다. 따라서 두 로직을 하나로 묶는 트랜젝션이라는 과정을 거쳐서 하나라도 실패하면 다 실패처리하고 --> 재시도 알림을 보내거나 자동 재시도가 이루어진다.

따라서, 이 경우에는 데이터를 처리하는 과정의 결과의 성공확률이 있기에 단순히 좋아요 등의 수를 올리는 것과는 다르다.

==> 같은 이유로 게시물 등록시에도 작성내용을 보내는 과정에서 실패할 수도 있으니 적용하는것은 삼가야한다.

주로 좋아요, 싫어요, 구독 등에 사용한다.

==> 구현

import { gql, useMutation, useQuery } from "@apollo/client";
import {
  IMutation,
  IMutationLikeBoardArgs,
  IQuery,
  IQueryFetchBoardArgs,
} from "../../src/commons/types/generated/types";

const FETCH_BOARD = gql`
  query fetchBoard($boardId: ID!) {
    fetchBoard(boardId: $boardId) {
      _id
      likeCount
    }
  }
`;
const LIKE_BOARD = gql`
  mutation likeBoard($boardId: ID!) {
    likeBoard(boardId: $boardId)
  }
`;

export default function OptimisticUIPage() {
  const [likeBoard] = useMutation<
    Pick<IMutation, "likeBoard">,
    IMutationLikeBoardArgs
(LIKE_BOARD);
  const { data } = useQuery<Pick<IQuery, "fetchBoard">, IQueryFetchBoardArgs>(
    FETCH_BOARD,
    {
      variables: { boardId: "특정 게시물 아이디" },
    }
  );
  const onClickLike = () => {
    void likeBoard({
      variables: { boardId: "특정 게시물 아이디" },
      //   refetchQueries: [ // 좋지 않은 방법인 리페치 법
      //     {
      //       query: FETCH_BOARD,
      //       variables: { boardId: "639996f31182750028ecfcf5" },
      //     },
      //   ],

      // DB수정될때까지 기다리는 방법 + 옴티미스틱 적용
      // 얘 자체가 data
      optimisticResponse: {
        likeBoard: (data?.fetchBoard.likeCount ?? 0) + 1, // 현제값 +1을 데이터 요청하고 바로 넣음. 일단 먼저 업데이트되게.(속임수) 숫자면(있으면 앞에꺼 없으면 0 +1)
      },
      update(cache, { data }) {
        // 그다음 요청보낸것 받아와 최종 마지막값으로 덮어쓰기
        // 캐시 직접수정하기 //
        // data에는 해당하는 response있음 즉, 여기서는 좋아요만 받음 data.likeBoard면 해당 데이터 받을 수 있음
        // chache.modify는 기존에 있는 값을 변경하는겅
        cache.writeQuery({
          // writeQuery는 기존에 없던것도 변경해 추가 할 수 있다.
          query: FETCH_BOARD,
          variables: { boardId: "특정 게시물 아이디" }, // 이 결과를 찾아서
          data: {
            // 여기의 data.fetchBoard의 data를 의미하는데 이것을 수정. 기존의 _id,_typename은 유지해주어야함
            fetchBoard: {
              _id: "특정 게시물 아이디",
              _typename: "Board",
              likeCount: data?.likeBoard,
            },
          },
        });
      },
    });
    };
  return (
    <>
      <div>현재 카운트(좋아요): {data?.fetchBoard.likeCount}</div>
      <button onClick={onClickLike}>좋아요 올리기!!</button>
      {/* 미리 올려놓고 글로벌 스테이트 바로 수정 */}
    </>
  );
}

기존캐시를 수정하기전(리페치 쿼리를 적용전) 에 옵티미스틱Response 로 옵티미스틱을 적용해 미리 +1을 시켜주면 된다.

slow-3G를 적용해보면 미세하게 빠르게 받아와지는것을 볼 수 있다.


스크랩핑과 크롤링

(Scraping && Crawling)
: 스크랩핑? --> 다른사이트의 정보를 가져와 한번 가져오기
: 크롤링 --> 일정주기로 게속 가져오기

단, 무단으로 가져오면 안됨

1 상업적목적 => 소송
2 가지고올때 실제 접속해 가지고와야하는데, 너무 많은 접속으로 해당 회사 서버에 부하를 주면 공격으로 판단하게됨

가지고 오는법 ...
브라우저 구동원리을 알 필요가 있다.

맨처음 document가 실행된다. 실제 개발자도구 네트워크 탭에서 맨위에 먼저 불러오는 것을 볼 수 있다.

Response로 html코드가 나오는데, 헤더부분으로보면 해당 부분 요청이 get요청이라고 나오고 리퀘스트헤더, 리스펀스헤더, 주소가 나온다... ==> 기존 rest-API와 같다

즉, 브라우저 주소창은 get방식의 API요청 도구이다. http 요청도구인 포스트맨에서 get으로 해당주소 요청을 한다면 같은 결과를 받을 수 있다(문자열) .

터미널에서 curl 하고 주소를 입력해도 같은 결과를 볼 수 있다.
curl또한 http요청 도구이다.

그런데 다 텍스트 형태로 보여주는데, 브라우저는 왜 아닐까?

브라우저는 결과로 받은 텍스트가 html코드이면, 자체에서 해석해 그림으로 나타내주는 기능이 있다! 따라서 같은 원리로 텍스트로 받아오나, 브라우저에서는 그것을 해석할 수 있기에 그림으로 그려줄 수 있는것!

정리: 브라우저 주소창은 get 메서드 요청도구!

스크래핑을 위해서는 결국 해당주소로 요청을 해야한다는 것!

알고리즘을 사용해 해당 부분만 잘라 가지고 올 수도 있으나, 쉽게 해주는 도구들이 존재한다.

cheerio라는 스크랩핑을 도와주는 도구와, 해당부분을 크롤링으로 쉽게 할 수 있게 하는 puppeteer이라는 도구가 있다.

axios는 스크랩핑하는 도구이고, 스크랩핑한 결과를 쉽게 조작할 수 있게 하는 도구가 cheerio다.

axios로 스크랩핑, cheerio로 조작.

그러면 이러한 기능은 어디에서 쓰이는 것일까?

Opengraph 실습

카톡이나, 메세지, 해당 수업시 사용하는 디스코드 등에서 링크를 보낼때 이미지와 해당 주소의 간단한 정보들이 자동으로 같이 넘어가 보여지는것을 볼 수 있었다.

이게 바로 opengraph 이다.

이것을 알기위해 개발자와 개발제공자로 나누어 보았다.

일단 개발 제공자에서는 meta태그에 og라는 애들을 넘긴다.
개발자 입장에서는 axios로 주소를 스크래랩핑해 가져와 meta태그의 og: 으로 시작하는 부분들만 변수에 넣고 보여주는 것이다.

og를 넣느냐 않느냐는 기능상의 문제는 없다.
다만 홍보등을 위해, 더 많은 노출을 위해서는 사용하는것이 좋다.

그런데 해당주소을 스크래핑할때 CORS문제가 걸릴수 있어 그부분은 벡엔드에서 진행하게된다. 우리는 CORS가 안걸리는 gmarket 사이트를 얘로 들어 실습하였다
(CORS를 우회하여 접근하기위해서는 다른 벡엔드로 보내어 그 벡엔드에서 받는 방법이 있다고했다!)

// 개발자 일때 ==>  디스코드(개발자)

import axios from "axios";

export default function OpengraphDeveloperPage() {
  const onclickEnter = async () => {
    // 1. 체팅데이터가 주소가 있는지 찾기 (ex, http://~~ 나 http://~~ 로 시작하는것) // .find()라거나 정규표현식 사용.
    // 찾았다고 가정
    // 2. 해당 주소로 스크래핑하기
    const result = await axios.get("https://www.gmarket.co.kr"); // 이 작업은 벡엔드에서 하게됨. 이유: CORS문제. 네이버의 경우 CORS문제에 걸려 지금 프론트에서는 실습 어려움. 벡엔드에서는 가능.
    console.log(result.data); // html코드가 문자열형태로 담긴것을 확인가능
    // 3. 메타테그에서 오픈그래트(og: ) 찾기
    console.log(
      // <meta 로 스플릿하여 쪼개고, 필터로 og가 들어있는 것만 아오게 거름 //또는 라이브러리 cheerio사용
      result.data.split("<meta").filter((el: string) => el.includes("og:"))
    );
  };

  return (
    <>
      <button onClick={onclickEnter}>체팅입력후 엔터치기</button>
    </>
  );
}
//  제공자일때 ==> 네이버(제공자)
import Head from "next/head";

export default function OpengraphProviderPage() {
  //  {/*  페이지 별로 og가 바뀔 수 있기에 페이지별로 head로 묶기 */}
  return (
    <>
      <Head>
        <meta property="og:title" content="중고마켓" />
        <meta
          property="og:description"
          content="나의 중고마켓에 오신것을 환영합니다!"
        />
        <meta property="og:image" content="http://~~~~~~" />
      </Head>
      <div>
        중고마켓에 오신것을 환영합니다(여기는 바디부분이므로 미리보기와는
        상관없음)
      </div>
    </>
  );
}

=> axios로 API를 요청하면 html을 받아올 수 있고, meta태그에서 og부분만뽑으면 미리보기 구현이 가능하다.
그런데 이 미리보기를 제공하는 사이트에서도 og태그를 만들어 주어야 스크랩핑시 그것만뽑아 그려주기 가 가능하다. 즉, 서로 합이 맞아야한다.

CORS문제로 되는사이트와 안되는 사이트가 있어 이 과정은 벡엔드에서 진행된다는점!!


서버사이드랜더링

스크랩핑 => 해당주소 get요청 => html코드 확인 후 content부분까지 잘 나오는 것을 보았다.

이번에는 동적으로 요청해보았다.

해당 상품 게시판 API를 사용하여 useQuery를 이용해 작성해보았다.

브라우저에서는 잘 실행되는것을 보았다. 그런데 포스트맨에서는 useQuery를 통해 넣어준 값들이 제대로 받아와지지 않는다.

포스트맨 등은 html코드는 다운받아오고 안의 js의 실행은 하지 않는다.

브라우저는 다운받아온 초기데이터인 html은 같으나 2차적으로 스크립트까지 읽고 해석해 그려주는 것이기에 useQuery도 그때 그려주게 되는것이다.

이렇게될경우 브라우저가 아니라면 스크래핑 해왔을때 미리보기시 내부에 데이터가 존재하지 않게된다.

이때 적용하는것이 서버사이드랜더링!

getServerSideProps 라는 함수를 현 페이지 함수의 바깥쪽 맨 아래에 적어준다. 해당 함수 이름은 이미 정해져 그대로 읽기 때문에 변경하면 안된다.

원래방식대로라면 먼저 html을 보내고 브라우저에서 2차적으로 useQuery를 실행하는데 이번엔 서버 사이드 랜더링으로 보내줄것이기에 useQuery는 사용하지 않는다.

export const getServerSideProps = async () => {
  // 이 페이지에서만 가능. 이름 바꿀 수 없음.
  console.log("여기는 프론트 서버입니다");
  // 1. 여기서 API요청

  // 2.받은 결과를 리턴. 리턴값이 페이지로 들어가 페이지가 받아온 API데이터를 가지고 화면에 그려주게됨 그다음 내용 모두채워놓고 완벽히 그려 보내준다.
};

getServerSideProps는 서버에서 실행되는 것이다. 여기서 데이터를 백엔드로요청해받아 그것을 가지고 해당페이지로 넘겨 완벽한 html을 만들어 브라우저에 돌려주는것! 아예 서버에서 처음부터 그려보내기에 포스트맨 등에서도 같은 결과를 받을 수 있다.(content가 원하는대로 받아진 데이터로 채워진형태)

그런데 이때 API요청은 아폴로세팅부분에 들어가지 않는 부분이기에(페이지로 들어가기 전이니) 기존에 refreshtoken을 적용했던 부분과 동일한 방법으로 graphQLClient.request라는것을 사용해 요청한다.

 // 1. 여기서 API요청
  // 첫접속시 시작하는 부분. 아폴로 세팅 없는 상태//getAccessToken때랑 비슷
  const graphQLClient = new GraphQLClient(
    "그래프큐엘주소(=해당벡엔드주소)"
  );
  const result = await graphQLClient.request(FETCH_USEDITEM, {
    // 요청한다. FETCH_USEDITEM을 벡엔드로
    // 여기서는 variables라는 단어 따로 적지 않고 알맹이만
    useditemId: "특정상품게시판 아이디", // 보내고 받음.
  });
 // 2.받은 결과를 리턴. 리턴값이 페이지로 들어가 페이지가 받아온 API데이터를 가지고 화면에 그려주게됨 그다음 내용 모두채워놓고 완벽히 그려 보내준다.
  return {
    props: {
      // 반드시 props라는 단어로
      aaa: {
        name: result.fetchUseditem.name,
        remarks: result.fetchUseditem.remarks,
        images: result.fetchUseditem.images,
      },
    },
  };

반드시 props라는 이름으로 보내주어야한다.!

해당 부분은 _app.tsx의 Componet 부분의{...pageProps} 를 통해 해당페이지의 props로 들어간다.

따라서 입력시에는 props.aaa.name이런 식으로 작성하면된다.

export default function OpengraphProviderPage(props: any) {
  console.log(props);
  //  {/*  페이지 별로 og가 바뀔 수 있기에 페이지별로 head로 묶기 useQuery로 받아올 수 있음 그러면 받아온것으로 오픈그래프 페이지를 바꿀 수 있다.==. 다이나믹 오픈그래프*/}
  // const { data } = useQuery<
  //   Pick<IQuery, "fetchUseditem">,
  //   IQueryFetchUseditemArgs
  // >(FETCH_USEDITEM, {
  //   variables: { useditemId: "특정상품게시판 아이디" },
  // });

  return (
    <>
      <Head>
        <meta property="og:title" content={props?.aaa.name} />
        <meta property="og:description" content={props.aaa.remarks} />
        <meta property="og:image" content={props.aaa.images?.[0]} />
      </Head>
      <div>
        중고마켓에 오신것을 환영합니다(여기는 바디부분이므로 미리보기와는
        상관없음)
      </div>
    </>
  );
}

서버사이드랜더링을 하면 => 다이나믹 opengraph가 가능하다.

또다른것 : 검색엔진 최적화

검색 프로그램은 API요청하는 도구를 통해 스크랩핑하여 가져온다. 따라서 제대로된 정보를 주어야 검색엔진에 더 잘 노출될 수 있다.

이럴경우 서버사이드 랜더링을 적용하지 않고 보내는것은 결국 빈 것을 보내는것이다. 게시판 상세보기의 경우만 봐도 현재 useQuery를 적용하고있어 2차적으로 실제 내용이 그려지므로 검색엔진이 스크랩핑을 하지 못한다. 따라서 해당페이지에 서버사이드 랜더링을 적용해 받아와야 한다.

즉 , 검색이 잘되는 페이지를 원한다면.(검색이 잘되는 페이지들은 ) 서버사이드랜더링을 해야한다.

==> 데이터까지 다 받아와 보여주기에 첫페이지 접속시 느릴수 밖에없다.(체감상) 기존의 방법이 먼저 보여주고 필요한 내용을 받아와 채우는 식이기에 체감상 빠르다고 느끼게된다.
따라서, 검색엔진에 잘 노출되야하는 페이지들에 서버사이드랜더링을 적용한다.
(물론.. 관리자 페이지에는 적용하지 않기)


예전에는 html, json, xml를 백엔드에서 다 보내주었다. 요즘에는 프론트엔드에서는 html, 벡엔드에서는 json방식으로 보내주고 xml은 잘 사용하지 않는다.

xml : JavaScriptXml = JSX : 리엑트에서 사용하는 html과 똑같이 만든 태그! (리엑트에서 리턴하는것)

html: 하이퍼 텍스트 마크업 랭귀지 : 꺽쇠를 사용해 만드는 언어

http통신: 하이퍼 텍스트 트랜스퍼 프로토컬

0개의 댓글