react-apollo-client

신형기·2021년 4월 21일

사전 지식

블록킹은 순서대로 실행된다.
논 블로킹은 순서대로 안 실행된다.
프로그래밍에 랜덤이라는건 없다.
논블로킹은 순서대로 실행되지 않는데 
그것마저도 규칙이있다. 
=> 순서대로 실행되지 않는데 그것마저도 규칙이있는것.
그게 "이벤트 루프다"
동기가 순서대로 실행되는건 '실행컨텍스트'.

프로토타입 

실행컨텍스트 안에 this 랑 scope 다.

zshrc 에서 code 자꾸 안먹힐때 ㅠ..

vim ~/.zshrc <- 터미널에서 입력 
파일 제일 아래에 다음 코드를 추가

code () { VSCODE_CWD="$PWD" open -n -b "com.microsoft.VSCode" --args $* ;}
 
그리고 변경된 .zshrc 파일을 한번 실행 시킨다.

-> $ source ~/.zshrc 
끝.

React Setup

step1. Make react new Project

$ npx create-react-app [project] template=typescript

[Optional] What is Tail Wind?

super super Ui frame work 

try : $ npm install tailwindcss
market: tailwind => Search => Tailwind CSS InteliSense down!
When you Expect Autocomplete css word !. CSS InteliSenese is power plugin !  

important => However, tail wind is available from node version 12 

detail documentation search url  => [https://tailwindcss.com/docs/installation] 

what is apollo client ?

Apollo Client?
자바 스크립트를 위한 상태 관리 라이브러리다. 
GraphqlQL쿼리를 작성하면 ApolloClient는 데이터를 가져오고 캐싱을 해주고 UI를 업로드 해 준다.  
GraphQL의 모든 요청은  POST로 하기 때문에 브라우저에서 캐싱이 되지 않습니다. 물론 캐싱뿐만이 아니라 많은 기능을 제공하기 때문에 사용한다.

장점
개인적으로는 데이터 관리 & 유지보수가 뛰어납니다. 모든 상태를 전역으로 관리할 필요도 없으며 

GraphQL 쿼리 형태 그대로 데이터를 가져올 수 있습니다. 

import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';

const GET_DOGS = gql`
  {
    dogs {
      id
      breed
    }
  }
`;
GraphQL 플레이 그라운드와 같은 형식으로 작성하면 쉽게 가져올 수 있습니다.

  const { loading, error, data } = useQuery(GET_DOGS);
로딩 에러 데이터 등등 관리를 쉽게 할 수 있습니다. 

data	GraphQL 쿼리 결과가 포함 된 객체입니다. 기본값은 undefined입니다.
loading	로딩 상태를 나타냅니다. 불리언 값으로 나타납니다.
error	graphQLErrors및 networkError속성 의 런타임 오류를 받습니다.
variables	쿼리가 호출 된 변수를 포함하는 객체
 

install tailwind post frefix

post frefix 가 뭐냐면. border-radius 같은 경우는 fire fox 에서 [ 특정 브라우저에서 사용 할 수 없는데]
이럴경우 -moz-border-radius 처럼 앞에 -moz 라는 접두어를 붙여야 작동한다.
이러한 일을 자동으로 처리해 주는 녀석이 tailwind 에서 post frefix 이다.
설치 방법은 아래와 같다.
$ npm install tailwindcss@latest postcss@latest autoprefixer@latest

설치를 하고나서. root 디렉토리에 file 을 하나 추가해 주자.
File name 은 postcss.config.js 라고 해주고,
내용은 아래와 같다.

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

Tailwind 를 확장할땐. (커스터마이즈)
tailwindcssnpm 패키지 를 설치할 때 포함 된 Tailwind CLI 유틸리티를 사용하여 
프로젝트에 대한 구성 파일을 생성 해줘야 한다.
$ npx tailwindcss init
내용은 자동으로 채워지고, 이렇게 해두면, tailWind 를 커스터마이징 할 수 있다.

정리하면 , tailwind 를 커스터 마이즈 하기 위해 Tailwind config 파일이 필요하고,
tailwind 를 일반 css 파일로 *빌드 하기 위해 postcss config 파일 이 필요하다.
이렇게 해두면 쉽게 커스터마이징 할 수 있고, 방법은 
다음 url 에서 자세히 살펴볼 수 있다.
url : https://tailwindcss.com/docs/extracting-components

test 하기 위해 . root > src 폴더에 css 파일을 하나 만들어 주자.
 => tailwind.css 

그런다음 공식 문서에 있는 다음 코드를 작성해 준다.

@tailwind base;
@tailwind components;
.btn { // 사용자 정의 
  @apply px-4 py-2 bg-blue-600 text-white rounded;
}
@tailwind utilities;

위의 코드들은 post css 가 이 파일을 보고 Tailwind 가 갖고 있는 모든 클래스 이름으로 
바꾸어 준다 + tailwind config 파일을 들여다 보고 새 클래스 이름이 있다면 그것도 추가해준다.

아직 끝난게 아니다. 사용자 정의용으로 만든 tailwind.css 파일을 => tailwind 에 적용하는 것이 남았다.

방법은 다음과 같다.

1. package.json 에 script 부분을 보면 start, build, test, eject 부분에 tailwind 용 build 를 추가해
줘야 한다. 추가하면 해당 명령어로 인해 tailwind  
[ 이부분은 자유 네이밍 이 가능 ] 그뒤에 : "tailvind build 인풋파일경로 를 -o 뒤엔 아웃풋 파일 경로를"
"tailwind:build": "tailwind build ./src/styles/tailwind.css -o ./src/styles/styles.css",


 npm run tailwind:build

> nuber-eats-clone-front@0.1.0 tailwind:build /Users/herings/project/back-end/nuber-eats-clone-front
> tailwind build ./src/styles/tailwind.css -o ./src/styles/styles.css

  
   tailwindcss 2.0.2
  
   🚀 Building: src/styles/tailwind.css
  
   ✅ Finished in 3.88 s
   📦 Size: 3.75MB
   💾 Saved to src/styles/styles.css

   실행하고 나선 위와 같은 완료 문구를 볼 수 있는데 size 가 꽤 크다.
   그러나 쓰이지 않는 class 를 줄일 수 있기 떄문에 걱정하지 않아도 된다.

build 후엔 본인이 만들어 놓은 styles 폴더 하위에 tailwind.css 파일 말고도 styles.css 파일이 생성된것을
확인 할 수 있다. 또 .btn 클래스가 tailwind 에 맞게끔 소스가 바뀌었다.
이게 커스텀후 tailwind 에게 맞는 소스로 바꾸는 작업이다.

이제 필요한 곳에서 해당 css 를 import 하면된다.
그러나 변경 할때마다 build 를 해줘야 하는 귀찮은점이 있다. 이렇땐 package.json 에서 작업을 해줘야 한다.
    script 부분의 start 를 다음과 같이 변경해 준다.
    "start": "npm run tailwind:build && react-scripts start",
이렇게 하면 변경이 이루어질 때마다 build 가 이뤄진다는 걸 보장 할 수있다.

apollo ?

Apollo Client는 클라이언트 애플리케이션에서 GraphQL과의 데이터 교환을 도우며,
특별히 리액트에서 사용하는 라이브러리를 리액트 아폴로(React Apollo)라고 부른다.

설치부터 하자.
$ npm install @apollo/client graphql

위의 @apollo/client:이 단일 패키지에는 Apollo Client를 설정하는 데 필요한 거의 모든 것이 포함되어 있다. 
여기에는 메모리 내 캐시, 로컬 상태 관리, 오류 처리 및 React 기반 뷰 레이어가 포함된다.
graphql:이 패키지는 GraphQL 쿼리 구문 분석을위한 로직을 제공한다.

먼저 클라이언트 파일을 손봐야 한다.
공식 문서에 있는 아래 코드에서 uri 를 백엔드 uri 로 바꿔주어야 한다.

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  // uri: 'https://48p1r2roz4.sse.codesandbox.io',
  uri: 'http://localhost',
  cache: new InMemoryCache()
});

isolation error ?

-isolation error 에 빠지면 재시작이 답이다.

Local state ?

local state 는 server 에는 없지만 application 에는 있기를 바라는 그런 state 다.
예를 들어 로그인, 로그아웃 같은 경우.

apollo.ts 에 가면 client 를 관장하는 곳이 있다.
이곳으로 와서 cache 부분을 손봐야 한다.
cache: new InMemoryCache() 안에 다음과 같이 꾸며줘 보도록 한다.

cache: new InMemoryCache({
  typePolicies: {
    Query: { // query 에는  
      fields: { // field 들이 있고 field 들은 function call read 가 있어야 한다.
        isLoggedIn: { // 그중 하나는 isLoggedIn 이 된다.
          read() { // read 는 field 의 값을 반환하는 함수다.
            return false; 
          }
        }
      }
    }
  }
})

이렇게 해놓고 App.tsx 는 다음과 같이 바꿔 주도록 한다.

import { gql, useQuery } from '@apollo/client';
import React from 'react';
import { LoggedOutRouter } from './routers/logged-out-router';

const IS_LOGGED_IN = gql`
  query isLoggedIn {
    isLoggedIn @client 
  }
`; 
   // @client 를 붙여주지 않으면 GraphQL 이 서버한테 가서 이걸 요구한다.
   // 우리의 목표는 client cache 에 요구해야 한다. 그래서 @client 를 붙여준다.
   // @client 앞의 isLoggedIn 이 있을것이다. 이부분이 중요한데, apollo.ts 의 Field 안의 철자랑 반드시 같아야 한다. 

function App() {
  const { data } = useQuery(IS_LOGGED_IN);
  console.log(data); // 기대값 : {isLoggedIn: false } 
  
  return <LoggedOutRouter />;
}

export default App;

이게 local state 에 접근하는 방법이다.

Router ?

Router 에는 { hashRouter , BrowserRouter } 두가지가 있다.
hashRouter/# 
BrowserRouter/

여기선 멋진 BrowserRouter 을 사용한다.

apollo codezen ? (apollo-tooling)

mutation 을 보호해주는 역할을 한다.
(검증된.) 

apollo tooling 은 back-end 에서 mutations, query responses, input type 을
전부 다 typescript 정의를 자동으로 생성해 준다.
이말은 내가 mutation 을 보낼때 back-end response 의 타입을 알 수 있다는 이야기다.

 $ apollo client:codegen mytypes.d.ts --target=typescript 실행....
 지린다....

 SAGE
  $ apollo client:codegen [OUTPUT]

ARGUMENTS
  OUTPUT
      Directory to which generated files will be written.
      - For TypeScript/Flow generators, this specifies a directory relative to each source file by default.
      - For TypeScript/Flow generators with the "outputFlat" flag is set, and for the Swift generator, this specifies 
      a file or directory (absolute or relative to the current working directory) to which:
        - a file will be written for each query (if "output" is a directory)
        - all generated types will be written
      - For all other types, this defines a file (absolute or relative to the current working directory) to which all 
      generated types are written.

OPTIONS
  -c, --config=config                        Path to your Apollo config file

  -g, --graph=graph                          The ID for the graph in Apollo to operate client commands with. 
                                             Overrides config file if set.

  -v, --variant=variant                      The variant of the graph in Apollo to associate this client to

  --[no-]addTypename                         [default: true] Automatically add __typename to your queries, can be 
                                             unset with --no-addTypename

  --clientName=clientName                    Name of the client that the queries will be attached to

  --clientReferenceId=clientReferenceId      Reference id for the client which will match ids from client traces, 
                                             will use clientName if not provided

  --clientVersion=clientVersion              The version of the client that the queries will be attached to

  --customScalarsPrefix=customScalarsPrefix  Include a prefix when using provided types for custom scalars

  --endpoint=endpoint                        The URL for the CLI use to introspect your service

  --excludes=excludes                        Glob of files to exclude for GraphQL operations. Caveat: this doesn't 
                                             currently work in watch mode

  --globalTypesFile=globalTypesFile          By default, TypeScript will put a file named "globalTypes.ts" inside 
                                             the "output" directory. Set "globalTypesFile" to specify a different 
                                             path. Alternatively, set "tsFileExtension" to modify the extension of 
                                             the file, for example "d.ts" will output "globalTypes.d.ts"

  --header=header                            Additional header to send during introspection. May be used multiple 
                                             times to add multiple headers. NOTE: The `--endpoint` flag is REQUIRED 
                                             if using the `--header` flag.

  --includes=includes                        Glob of files to search for GraphQL operations. This should be used to 
                                             find queries *and* any client schema extensions

  --key=key                                  The API key to use for authentication to Apollo

  --localSchemaFile=localSchemaFile          Path to one or more local GraphQL schema file(s), as introspection 
                                             result or SDL. Supports comma-separated list of paths (ex. 
                                             `--localSchemaFile=schema.graphql,extensions.graphql`)

  --mergeInFieldsFromFragmentSpreads         Merge fragment fields onto its enclosing type

  --namespace=namespace                      The namespace to emit generated code into.

  --omitDeprecatedEnumCases                  Omit deprecated enum cases from generated code [Swift only]

  --only=only                                Parse all input files, but only output generated code for the specified 
                                             file [Swift only]

  --operationIdsPath=operationIdsPath        Path to an operation id JSON map file. If specified, also stores the 
                                             operation ids (hashes) as properties on operation types [currently 
                                             Swift-only]

  --outputFlat                               By default, TypeScript/Flow will put each generated file in a directory 
                                             next to its source file using the value of the "output" as the 
                                             directory name. Set "outputFlat" to put all generated files in the 
                                             directory relative to the current working directory defined by 
                                             "output".

  --passthroughCustomScalars                 Use your own types for custom scalars

  --queries=queries                          Deprecated in favor of the includes flag

  --suppressSwiftMultilineStringLiterals     Prevents operations from being rendered as multiline strings [Swift 
                                             only]

  --tagName=tagName                          Name of the template literal tag used to identify template literals 
                                             containing GraphQL queries in Javascript/Typescript code

  --target=target                            (required) Type of code generator to use (swift | typescript | flow | 
                                             scala | json | json-modern (exposes raw json types))

  --tsFileExtension=tsFileExtension          By default, TypeScript will output "ts" files. Set "tsFileExtension" to 
                                             specify a different file extension, for example "d.ts"

  --useFlowExactObjects                      Use Flow exact objects for generated types [flow only]

  --useFlowReadOnlyTypes                     Use read only types for generated types [flow only]. **Deprecated in 
                                             favor of `useReadOnlyTypes`.**

  --useReadOnlyTypes                         Use read only types for generated types [flow | typescript]

  --watch                                    Watch for file changes and reload codegen


  typescript

 $ apollo client:codegen src/__api_type__ --target=typescript --outputFlat 
 => 각 파일마다 하나씩 src ( 꼭 하위여야 한다. ) 밑의 [자유선언 폴더 ] __api_types__ 같이.

추가적으로 자꾸 __api_type__ 에 기존에 있던 코드들이 남아있어 지우고 깔끔하게 다시 apollo:codegen 을 해줘야 
하는 번거로움이 있는데 , 이를 해소하기 위해 아래와 같은 코드를 심어 줘야 한다.
"apollo:codegen": "rm -rf src/__api_type__ && 
 apollo client:codegen src/__api_type__ --target=typescript --outputFlat",

rm -rf 이 명령어는 리눅스나, mac 에서만 작동한다. 따라서 rimraf 를 설치해줘서 windows 에서도 동일한 명령을 수행 할 수 있게
바꿔주도록 하자.
 * rimraf 는 패키지 인데 많은 사람들이 사용하지 않는다.
 $ npm i rimraf 
 설치를 마치고 아래와 같은 코드를 package.json 에서 기존의 것을 지워주고 다시 작성해 주도록 한다.
 "apollo:codegen" :rimraf src/__api_types__ && 
 apollo client:codegen src/__api_type__ --target=typescript --outputFlat 
 
 codegen 이 일부 추가 되었으니 start 명령어도 손봐 주도록 한다.
"start": "npm run apollo:codegen &  npm run tailwind:build && react-scripts start",
tail wind 시그니처 컬러 적용방법

tailwind.config.js 에서 제일 상단에 아래와 같은 코드를 삽입.
const colors = require('tailwindcss/colors')

하위에 theme 아래 주석으로 중요하다는 부분을 눈여겨 보자.
module.exports = {
  purge: [],
  darkMode: false,
  theme: {
    extend: { // 중요. extend 객체 내부에 내가 원하는 것을 넣어준다.
      colors: { 
        // 이부분에 객체 확장을 해줘야 한다.
        lime: colors.lime
      }
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

what is react-helmet

npm i react-helmet

title 을 바꿔주는 라이브러리다. 기본 title 은 reactApp 이다.

localstorage

  토큰을 애플리케이션 단에서 저장하고 싶을때, 혹은 애플리케이션 내에 저장 하고싶은 상태는
  apollo.ts 가 관장한다.
  

Switch ?

Switch 가 있으면 한번에 route 하나만 render 하라고 알려준다.

rem ?

em ? rem ? 
em 은 예를 들면 document 에 있는 font 의 사이즈다.
document 에 있는 font 가 16px 이라고 가정하고 ,
margin top 을 1em 으로 가정해주면 margin-top 은 16px 이 된다.

f-t: 16px 
m-t: 1em === 16px

f-t: 50px 이면 
m-t: 2em 이다.

이게 em 이다. em 은 element 의 사이즈에 좌지우지 된다.

tailwind css 가 padding 과 size 를 이용하는 방법은
rem 이다.
rem 은 root em 이란 뜻이다.

rem 은 body 의 font 사이즈를 가져와서 number 만큼 곱하고,
em 은 가장 가까이 정의되어있는 font 사이즈를 가져와서 number 만큼 곱한다.

< body : f-t :5px 

 < f-t : 50px 
  rem => 2rem => 10px;
  em => 2em => 100px;

react font-awesome ?

아이콘 무료 버전이 있고 pro (유료) 버전이 있으니 잘 알아보고 적용하자.
  npm i  @fortawesome/fontawesome-svg-core
    @fortawesome/free-solid-svg-icons
    @fortawesome/react-fontawesome

일반적인 js 에서 특정 url 값 뜯어오기

 window.location.href.split("code=")
솔직히 위 만한게 없다.
그러나 우리는 react-router 를 쓰기 때문에
useLocation 을 사용 하면 더 간단하게 뜯어올 수 있다.

cache 에 write 하는 방법

      client.writeFragment({
          id: `User${useData.me.id}`, // cache 엔 object Id 가 있다.
          fragment: gql`  // 그러면 fragment 를 write 할 수 있다. fragment 는 큰 type 의 일부분이다.
          fragment VerifiedUser on User { // 예를들어 여기서는 type 이 user 다. 나는 이 fragment 를 cache 로 write 한다.
              verified // 이것만 바꾼다. 그리고 여기서 선언한 data 는 
          }
          `,
          data: {
              verified: true, // 여기서도 넣어줘야 한다.
          }
      })

      cache 

apollo client ?

    const client = useApolloClient(); 
    우리가 원하는 클라이언트를 줄 수 있다.

application 에서 일어나는 모든 update 를 최적화 하는방법 back-end api 에 의존 X

1. 직접 cache 를 업데이트 하는 방법.
2. 직접 query 를 refresh 하는 방법. 

결국 이게 무슨 말이냐면, mutation 이 일어나면 내가 직접 cache 를 update 할꺼란 이야기.
=> object 의 id를 갖고, fragment를 write하고 , data 를 send 한다.

    const onCompleted = async (data: editProfile) => {
        const { editProfile: { ok } } = data;
        if (ok && userData) {
            const { me: { email: prevEmail, id } } = userData;
            const { email: newEmail } = getValues();
            if (prevEmail !== newEmail) {
            client.writeFragment({
                id: `User:${id}`,
                fragment: gql`
                    fragment EditedUser on User {
                        verified
                        email
                    }
                    `,
                data: {
                    verified: false,
                    email: newEmail,
                }
            });
            }
            // 혹은 => await refreshUser(); refetch 를 커스텀 훅에서 선언해 refetch 를 선언해줘도 같은기능을 한다.
        }
    };

refetch

  refetch 는 function 인데 이걸 call 하면 query 를 다시 fetch 해준다.
  그렇게 하면 cache 가 자동적으로 update 될것이고, apollo 는 자동으로 cache 를 업데이트 해준다.

tailwind css

아무것도 붙어 있지 않은경우 엔 모바일에 적용될것이고,
md: 같은게 붙어있다면 medium 사이즈나 그보다 큰 화면에 적용될것이다.

React form 을 작성하는 이유?

이뉴 는 form 을 빠르게 작성할 수 있기 떄문이다. 
일반적인 form 은 보통 form submit 에 validation 에 여러것들이 붙어있다보니 
작성이 불가피하게 코딩을 많이 해야한다.
그러나 react-form 을 쓰면 이러한 것들을 최소화 시킬 수 있다.

다른 페이지로 redirect 하기 위해선 history 가 필요하다.

react-router 에서 useHistory() 를 제공한다.
history push 는 object 를 제공한다.

useLocation vs useHistory

  useLocation 은 우리가 어딘지를 알 수 있고,
  useHistory 는 우리가 어디로 갈 수 있다.

history push ?

  history.push 에 1번 , 2 번의 차이점
        history.push({
            pathname: "/search",
            search: `?term=${searchTerm}`, // [1] 
            state: { // [2]
                searchTerm
            }
        });
        1번의 경우엔 url 에 페이지가 표현 하고 자 하는 내용이 담겨있다.
        2번의 경우엔 url 에 표현하고자 하는 내용이 숨김처리가 된다 + 브라우저가 Route 의 state 를 기억해준다.
        Route 의 state 가 브라우저 메모리에 저장되기 때문이다.

        적절히 숨겨야 할 페이지엔 숨기고 그렇지 않을경우엔 그대로 노출시켜주면 공유가 가능하니
        적절히 사용하면 좋을것같다.

orm + gql 에서 Search 그리고 Lazy Query

  조건에 따라 달라지는 query 를 실행해야 한다.
  우리는 /search? 라는 url 에 query 를 보내야 한다.
  search 관련 gql 에는 input 으로 query 와 page 가 필요하다.
  그에 따라 시간이 다소 걸리고, (string 으로 검색하기 때문) + 사용자가 url의 일부를 지울수도 있기 때문에
  query 가 실패하고 gql 에러를 터트리게 되는데 lazy Query 를 알아야 한다.
  lazy Query 는 바로 실행되지 않는것이다. 내가 불러야 한다.
  직접 call 하기 위해 "저기요 실행하세요" 라고 불러줘야 하기 때문이다. 그게 lazy Query 다.
  

react-router-dom 은 우리에게 3가지를 준다.

1. useHistory: 우리가 가고싶은곳을 가게해주고, change, replace, push 를 할 수 있게끔 만들어줘.
2. useLocation: 우리가 어디에 있는지 url 을 알게해주고.
3. useParams: 우리에게 파라미터를 준다.

Apollo cache 를 사용하는 방법

/* /stores/store.ts 를 만든후. */ 
import { makeVar } from '@apollo/client'; 

 Apollo Client에서 제공하는 makeVar를 통해 반응 변수라는 것을 만들 수 있는데,
 이 반응 변수는 Apollo Client 캐시 외부에 로컬 상태를 저장하기 위해 사용된다.

export const currentPage = makeVar(1);

커스텀 페이지 네이션을 만들기 위해 위와 같은 코드를 사용하였는데.
export 해준다음 모든 컴포넌트에서 사용이 가능하다.
그러나, 남용하면 좋지 않기때문에 커스텀 하게 쓸수 있게 페이지네이션을 분리했다.

const page = useReactiveVar(currentPage);
useReactiveVar를 통해 직접 반응 변수에서 상태를 가져올 수 있다. 
(이 경우에는 cache에 정의를 하지 않아도 되고 쿼리를 작성하지 않아도 된다).
useReactivevar() 에 currentPage 를 넣어준뒤, 커스텀 Component 에 프로퍼티로 내려주도록 해본다.

    <PageNation data={data} pages={page} />
    data 는 쿼리에서 받아온 객체, pages 에 위에서 정의한 page 를 내려준다.

    // PageNation.tsx
    커스텀 페이지네이션 도입부에 다음과 같이 프로퍼티를 정의해준다.
    interface PageProps {
      data: any;
      pages: number;
    }

export const PageNation = ({ data, pages }: PageProps) => {
  
  data 는 안에 포함된 내부 객체가 달라질수 있기떄문에 타입을 정의하기 모호하다.
  pages 같은경우는 오로지 number 로만 받아오기때문에 number 로 타이핑을 해준다.


    const onPageClick = (val: number) => { // button 이벤트 속성에 다음페이지 시 +1 을 이전페이지엔 -1 을 먹여주면 된다.
        changePage(pages, val); // 직접 변경하면 react 가 뭐라뭐라 하니 store.ts 에 함수를 만들어준다. 
    };

    // store.ts
    export const changePage = (page: number, value: number) => { // page 와 value 를 내려준다음
      let result = page + value; // 더해주고 
      currentPage(result); // 적용시켜주면 끝.
    };

jest 초기사용시.

$npm test -- --coverage --watchAll=false // 이렇게 하면 제약없이 모든 test 파일을 볼 수 있다.

그러나 나는 특정파일만 test 하고 싶다.

예를들어 components , pages, routers 만 이라던가.

그럴경우 package.json 에 다음과 같이 작성.
  ...
  "jest": {
    "collectCoverageFrom": [
      "./src/components/**/*.tsx",
      "./src/pages/**/*.tsx",
      "./src/routers/**/*.tsx"
    ]
  }
components,pages,routers 를 collectCoverageFrom 안으로 넣어줬는데
collectCoverageFrom 는 커버리지 정보를 수집해야하는 파일 집합을 나타내는 glob 패턴 배열이다 .
파일이 지정된 glob 패턴과 일치하면이 파일에 대한 테스트가없고 테스트 스위트에서 필요하지 않은 경우에도 
해당 파일에 대한 커버리지 정보가 수집된다.
  

front-jest

 import { render } from '@testing-library/react'; 
 
 render 에는 정말 많은 것을 사용하게 도와준다.
 특히 debug 가 인상적이다.

 describe('<App />', () => {
    it("renders OK", () => {
        const { debug } = render(<App />);
        debug(); 
    })
})

debug(); 호출시 

앱의 구조가 훤히 보인다.

   <body>
      <div>
        <span>
          logged-out
        </span>
      </div>
    </body>

container ?

 컴포넌트를 debug() 로 찍어보면 알겠지만 child component 가 있을경우에 기대하는 값 까지도 
 test 의 일환이다. 사람은 알 수 있지만 컴퓨터는 모르기 때문 . 

 다음 예제를 보자 . 
 /* eslint-disable jest/expect-expect */
import { render } from '@testing-library/react';
import React from 'react';
import { Restaurant } from '../restaurant'
import { BrowserRouter as Router } from 'react-router-dom';

describe('<Restaurant>', () => {
    it("renders OK with props", () => {
        const restaurantProps = { // 한꺼번에 던저주기 위해 선언. 
            index: 1,
            id: 1,
            coverImg: "x",
            name: "nameTest",
            categoryName: "catTest"
        }
        const { /*debug,*/ getByText, container } = render(<Router>
            {/* <Restaurant index={1} id={1} coverImg="x" name="nameTest" categoryName="catTest" /> */}
            { /* 그리고 아래에서 스프레드 연산자로 풀어버리고. */ }
            <Restaurant {...restaurantProps} /> 
        </Router>)
        // debug();
        getByText(restaurantProps.name)
        getByText(restaurantProps.categoryName) 
        // 기대하는 firstChild 가 속성으로 href 를 가지고 있다면 href 고 , 값은 `/restaurant/${restaurantProps.id}` 로 기대하겠다 라는 의미다.
        expect(container.firstChild).toHaveAttribute("href", `/restaurant/${restaurantProps.id}`)
    })
})

apollo test ?

  아폴로 테스트 ? 는 무었일까.
  지금껏. gql 을 테스트해본 적은 없었다.
  매번 내가 누구인지 인증하는 쿼리를 날릴빠엔, 커스텀 훅으로 만들어서 
  모든 컴포넌트에 심어주는게 좋다. 
  그런데 그 훅을 test 하는 과정이 문제였다.
  hook 자체를 mock 하면 안되고 , hook 에 결과를 주는걸 mock 해야한다는 것이다.
  hook 말고 서버에 보내는 `graphql` 의  request 를 mock 하면 된다.
  따라서 apollo 측에서 제공 하는 두가지 방식을 소개하겠다.

  1. 첫번째는 MockedProvider 로 풀어내는 방법
  MockProvider 는 mocks 라는 prop 를 받는다.
  mocks 는 query, mutation, result 를 mock 할 수 있게 해준다.
  말 그대로 결과를 만들어서 테스트 할 수 있다는것이다.

  예를 들어 , (주석에서 상세히 설명)
  const mocks = [
  {
    request: {
      query: GET_DOG_QUERY, // 쿼리를 만들고  ...[2] 번째 해석
      variables: { // 이 variable 을 가지고 ...[1] 번째 해석
        name: 'Buck',
      },
    },
    result: { // result 를 테스트 하는것이다. ...[3] 번째 해석
      data: { // 이 output 을 만들어서 ...[4] 번째 해석
        dog: { id: '1', name: 'Buck', breed: 'bulldog' },
      },
    },
  },
];


자 그럼 위에서 배운것처럼 실제 useMe 쿼리에 작성해보자.

완성하면 다음과 같은 코드가 MockProvider 에 mocks 프로퍼티에 심어지게 된다. 
            <MockedProvider mocks={[
                {
                    request: {
                        query: ME_QUERY // 이 쿼리를통해서 ... [1]
                    },
                    result: { // 결과값으로 쓰겠다. ... [4]
                        data: { // mocking 할려는 녀석들은 아래와 같고, ...[2]
                            me: { // 타입을 맞춰준다음  ...[3]
                                id: 1,
                                email: "",
                                role: "",
                                verified: true,
                            }
                        }
                    }
                }
            ]}>
    그런다음 해야할게 남아있다.
    원래 query 는 즉시 일어나는게 아니다.
    query 가 끝나기 까지 시간이 좀 걸린다.
    그래서 apollo 에서는 response 를 기다려서 해결하라고 알려준다.
    // 아래코드는 timeout 그리고 0 초에 result 를 받는거니까 아무것도 아닌것 같지만.
    // result 를 기다리긴 한다. 따라서 result 를 기다려서 테스트 하게 된다.
    await new Promise(resolve => setTimeout(resolve, 0));
   
   it 실행 도입부에 async 를 붙여주고, 나서 저장을 하고, npm run test 를 해보면 
    `
    console.error
    Warning: An update to Header inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
   `
   위와 같은 에러가 나타나게 되는데 이 에러는 우리가 state 를 update 해서 나타나는것이다.
   우리는 state update 도 기다려 줘야한다.
   WaitFor 를 사용해 함수 내부에 기존 로직을 담으면 다음과 같다.
       it("renders OK", async () => {
        await waitFor(async () => {
            const { debug } = render(
                <MockedProvider mocks={[
                    {
                        request: {
                            query: ME_QUERY
                        },
                        result: {
                            data: {
                                me: {
                                    id: 1,
                                    email: "",
                                    role: "",
                                    verified: true,
                                }
                            }
                        }
                    }
                ]}>
                    <Router>
                        <Header />
                    </Router>
                </MockedProvider>
            );
            await new Promise(resolve => setTimeout(resolve, 0));
        })
    이렇게 되면 우리가 website(react) 의 state 를 바꿔서 rerender 를 trigger 하니까,
    rerender 를 기다려야하고 , Promise 도 기다려 줘야한다는 것이다.
    })

    이제 완성된 코드를 보자 

    /* eslint-disable jest/expect-expect */
import { render, waitFor } from '@testing-library/react';
import React from 'react';
import { Header } from '../header';
import { MockedProvider } from '@apollo/client/testing';
import { BrowserRouter as Router } from 'react-router-dom'
import { ME_QUERY } from '../../hooks/useMe'
describe('<Headr />', () => {
    it("renders Verify Banner", async () => {
        await waitFor(async () => {
            const { getByText } = render(
                <MockedProvider mocks={[
                    {
                        request: {
                            query: ME_QUERY
                        },
                        result: {
                            data: {
                                me: {
                                    id: 1,
                                    email: "",
                                    role: "",
                                    verified: false,
                                }
                            }
                        }
                    }
                ]}>
                    <Router>
                        <Header />
                    </Router>
                </MockedProvider>
            );
            await new Promise(resolve => setTimeout(resolve, 0));
            getByText("Please Verified your email.")
        });
    });
    it("renders without Verify Banner", async () => {
        await waitFor(async () => {
            const { queryByText } = render(
                <MockedProvider mocks={[
                    {
                        request: {
                            query: ME_QUERY
                        },
                        result: {
                            data: {
                                me: {
                                    id: 1,
                                    email: "",
                                    role: "",
                                    verified: true,
                                }
                            }
                        }
                    }
                ]}>
                    <Router>
                        <Header />
                    </Router>
                </MockedProvider>
            );
            await new Promise(resolve => setTimeout(resolve, 0));
            expect(queryByText("Please Verified your email.")).toBeNull();
        });
    });
})

ㅋㅋ 좀 길다.
getBy 는 element 를 찾지못하면 테스트에 실패한다.
qeuryBy 는 element 가 존재하지 않으면 테스트에 성공한다.

getBy 의 반대가 queryBy 다.

  
   참고 Doc => https://www.apollographql.com/docs/react/development-testing/testing/

apollo testing 2


첫번째가 apollo graphql 이 제공하는 테스트방법으로 테스트했다면
두번째는 더좋은 테스팅 라이브러리를 도입하는것이다.

아폴로 graphql 이 제공하는 testing 방법으로는.
mutation 을 function 으로 테스트 할 수없다.
결과값만 테스트하는 과정에서
실수하고싶지 않아도 실수할 수 있다. + input variable 는 테스팅 할 수없다.

따라서 아래와 같은 라이브러리를 사용해보자.


$npm i mock-apollo-client 

const mockedClient = createMockClient();
전에는 provider 전체를 mocking 했다면 지금은 client 만 mocking 해도 된다.

waitFor 는 state 가 바뀌는걸 await 하게 해준다.
waitFor() 에 user testing moudle 의 userEvent를 import 해서 사용 할 수 있다.

node

Callback Function 이 뭘까?
자바스크립트에서는, 함수(function)는 일급 객체입니다. 즉, 함수는 Object 타입이며 다른 일급객체와 똑같이 사용 될 수 있습니다. (String, Array, Number, 등등..) function 자체가 객체이므로 변수안에 담을 수 도 있고 인수로서 다른 함수에 전달 해 줄수도있고, 함수에서 만들어질수도있고 반환 될수도있습니다.

https://velopert.com/255

history 같은건 어떻게 jest 로 테스트 해야하나.

useHistory 같은 얘들은 엄~청 많은 함수들을 담고 있다,
그런데 어떻게 mocking 해야할까 ? 
답은 간단하다.
해당 라이브러리를 mocking 한 후 스프레드 operator 를 사용하면 된다.

example ) 

const mockPushImplementation = jest.fn();

jest.mock('react-router-dom', () => {
    const realModule = jest.requireActual('react-router-dom');

    return {
        ...realModule,
        useHistory: () => {
            return {
                push: mockPushImplementation,
            }
        }
    }
})

마지막에 
afterAll(() => {
  jest.clearAllMocks() 
}) 
해주는것 잊지말구. 
다른데서 useHitory mocking 을 쓸 수도 있으니깐.
des......

 

what is Cypress ?

Cypress 는 windows , mac, linux 에서 멋진 end to end test 를
할 수 있게 도와주는 softWare 다.

step 1 ) 
npm install cypress

download done => npx cypress open

cypress 는 어떻게 움직일까 ? 
npm run start 로 local server 를 띄워준뒤, 
npx cypress open 을 해주면 (기본적으로 setting 을 안했을시)
example 들이 나온다. 
root 디렉토링서 cypress 폴더를 찾아준뒤 , integration 폴더를 열어서 
모두 지워준다. ( 괜찮다. cypress 홈페이지에 예제 차고 넘친다.)
e2e 테스트 대상 ( login 부터 ) 페이지 테스트 파일을 만들어주는데 , 
.spec 이 굳이 안붙어줘도 될뿐더러 , cypress 소프트 웨어가 관장하는 웹페이지 가 아주 경이롭기 짝이없다.

step 2 )
test 하는 방법은 jest 와 비슷하다.
just describe , it 의 향연.
describe('Log in', () => {
  it('should see Login Page', () => {
    cy.visit('/').title().should('eq', 'Login | Nuber Eats');
  });

다른점이 있다면 .
cy.visit 을 써줘야 한다는점 . 그 뒤론 비슷비슷하고 , 
form 요소 의 class name 을 가져오는 과정에서 dom 을 testing 하기 쉽지 않기 때문에 . 
다음 과 같은 lib 를 다운받아준다.
npx install @testing-library/cypress --save -dev

그러면 jest 의 getByText 와 같은 친구를 데려올 수 있다.
class name 을 가져오는게 아니라 , text 로 테스트를 할 수 있다.

그런다음 cypress > tsconfig 에 

{
  "compilerOptions": {
    "allowJs": true,
    "baseUrl": "../node_mudules",
    "types": [
      "cypress",
      "@testing-library/cypress" // 이부분을 추가 해 주자.
    ],
    "outDir": "#"
  },
  "include": ["./**/*.*"]
}

profile
시니어의 길로 가는 개발자

0개의 댓글