React-Hook-Form Yup refetch 문제점 useApolloClient Apollo-Cache-State

hyeseon han·2021년 10월 13일
0

React-Hook-Form

폼을 대신 만들어주는 도구이다. react-form, redux-form, react-hook-form, formik 등 다양한 종류가 있다. 폼 라이브러리를 사용하면 하나하나 직접 만들지 않고, 밸리데이션 체크 및 폼 관리 등을 좀 더 깔끔하게 할 수 있다.

react-hook-form은 함수형 컴포넌트와 hook을 사용하는 경우, 가장 사용하기 쉽고 성능적으로 좋은 폼이다. 비제어 컴포넌트 방식을 사용한다.

제어 컴포넌트

  • state가 제어한다.
  • 입력을 할 때 마다 state 저장한다.
  • 장문일 경우 좀 느리다.
  • 사용자의 입력을 받는 컴포넌트에 event 객체를 이용해 setState()로 값을 저장하는 방식을 제어 컴포넌트 방식
  • onChange 방식이 제어 컴포넌트에 속한다.
export default function App() {
  const [input, setInput] = useState("");
  const onChange = (e) => {
    setInput(e.target.value);
  };

  return (
    <div className="App">
      <input onChange={onChange} />
    </div>
  );
}

비제어 컴포넌트

  • 다 입력해놓고 입력해 놓은 결과를 ref로 가지고 오는 방식.
  • state로 매번 저장하지 않는다.
export default function App() {
  const inputRef = useRef(); // ref 사용
  const onClick = () => {
    console.log(inputRef.current.value);
  };

  return (
    <div className="App">
      <input ref={inputRef} />
      <button type="submit" onClick={onClick}>
        전송
      </button>
    </div>
  );
}

차이점

  • 제어:
    • 속도는 느리지만 정확도가 높다. 중요한 text는 제어가 사용이 더 좋다.
    • 사용자가 입력하는 모든 데이터가 동기화되어 이를 막기 위해서 스로틀링이나 디바운싱 사용하면 된다.

  • 비제어:
    • 속도는 빠르지만 data가 커지면 실제 입력값이랑 submit 눌렀을 때 값이랑 다른 경우도 있을 수 있다.


Yup

검증 라이브러리
react-hook-form에서만 사용할 수 있는 것이 아니며, 다른 폼 라이브러리들과도 함께 사용가능하다.

실습

// container

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { schema } from "./Myform.validations";
import MyformUI from "./Myform.presenter";

export default function Myfrom() {
  const { handleSubmit, register, formState } = useForm({
    mode: "onChange",
    resolver: yupResolver(schema),
  });

  function onClickLogin(data) {
    console.log(data);
  }

  return (
    <MyformUI
      handleSubmit={handleSubmit}
      onClickLogin={onClickLogin}
      register={register}
      formState={formState}
    />
  );
}
  • handleSubmit: Submit을 관리하기 위해 만든 함수이다. 함수를 인자로 받으며 그 함수에 data라는 인자를 넘겨준다
  • register: 여러기능이 있다. ~를 등록한다.
    규칙: 스프레드 시킨다. …register(사용하고 싶은 이름 =데이터)
  • resolver: 이 함수는 Yup, Joi, Superstruct 등의 외부 유효성 검사 방법들을 실행한다. yup과 연결시켜주는 도구 불러와서(yupResolver)
  • yupResolver(schema): schema 얻은 후에는 이를 React-Hook- Form useForm후크 로 전달해야 합니다. 이를 위해 yupResolver 함수로 래핑하여 리졸버로 전달한다.
  • mode: 검증을 언제 할것인가?
    onChange 를 적어야지 onChange를 했을 때 검증을 한다.
  • formState: errors 내장되어있다.
// presenter

import Button01 from "../../commons/buttons/01/Button01";
import Input01 from "../../commons/inputs/01/Input01";

export default function MyformUI(props) {
  return (
    <form onSubmit={props.handleSubmit(props.onClickLogin)}>
      <div>리액트 훅 폼 연습!!</div>
      이메일: <Input01 type="text" register={props.register("myEmail")} />
      <div>{props.formState.errors.myEmail?.message}</div>
      <br />
      <Input01 type="password" register={props.register("myPassword")} />
      <div>{props.formState.errors.myPassword?.message}</div>
      <br />
      <Button01
        name="로그인하기"
        type="submit"
        isValid={props.formState.isValid}
      />
    </form>
  );
}
  • 로그인하기 버튼을 클릭하면 onClickLogin이 실행되는데 handleSubmit으로 감싼다.
// 공통 컴포넌트 Input01
export default function Input01(props) {
  return <input type={props.type} {...props.register} />;
}
  • …register(사용하고 싶은 이름=데이터) : [데이터]를 등록한다.
    로그인하기 버튼을 클릭하면 onClickLogin 실행 → handelSubit으로 감싸줬기 때문에register로 등록된 데이터들이 onClickLogin에 들어간다.
  • formState.errors 를 다 적야줘야한다.
// styles

import styled from "@emotion/styled";
import { IProps } from "./Myform.types";

export const MyButton = styled.button`
  background-color: ${(props: IProps) => (props.isValid ? "yellow" : "green")};
`;
//validations

import * as yup from "yup";

export const schema = yup.object().shape({
  myEmail: yup
    .string()
    .email("이메일 형식이 적합하지 않습니다.")
    .required("반드시 입력해야하는 필수 사항입니다."),
  myPassword: yup
    .string()
    .min(4, "비밀번호는 최소 4자리 이상입니다.")
    .max(15, "비밀번호는 최대 15자리까지 입니다")
    .required("비밀번호는 반드시 입력해주세요!"),
});
  • schema: 데이터 구조, 제약 조건 명세 기술

여러가지 쿼리 방식

  • useQuery: 컴포넌트가 열리면 자동으로 useQuery가 실행된다.(자동 호출)
    ➤ 요청으로 받은 결과가 cache 에 들어간다.

  • useLazyQuery: 어떤 버튼을 클릭하면 Query 실행된다.(원하는 상황 )
    ➤ 요청으로 받은 결과가 apollo cache 에 들어간다.

  • useApolloClient: 어떤 버튼을 클릭하면 Query 실행된다.(원하는 상황, 추가로 쿼리를 요청할 경우)
    ➤ axios. 변수에 넣어서 사용한다.

    const result = await axios.get ("koreajson.com")
// client를 axios처럼 사용

export default function 연습UseApolloClient() {
  const { setAccessToken, setUserInfo, userInfo } = useContext(GlobalContext);

  const { handleSubmit, register } = useForm();
  const [loginUser] = useMutation(LOGIN_USER);
  const client = useApolloClient();

  async function onClickLogin(data) {
    const result = await loginUser({
      variables: {
        email: data.myEmail,
        password: data.myPassword,
      },
    });
    const accessToken = result.data.loginUser.accessToken;
    const resultUserInfo = await client.query({
      query: FETCH_USER_LOGGED_IN,
      context: {
        headers: {
          authorization: `Bearer ${accessToken}`,
        },
      },
    });
    //  headers authorization에 추가로 요청한 쿼리 accessToken을 넣어준다.
    const userInfo = resultUserInfo.data.fetchUserLoggedIn;

    setAccessToken(accessToken);
    setUserInfo(userInfo);
  }

  return (
    <>
      {userInfo.email ? (
        `${userInfo.name}님 환영합니다.`
      ) : (
        <form onSubmit={handleSubmit(onClickLogin)}>
          이메일: <input type="text" {...register("myemail")} />
          비밀번호: <input type="text" {...register("myPassword")} />
          <button>로그인하기</button>
        </form>
      )}
    </>
  );
}

refetch의 문제점과 개선 방법

refetch로 쉽게 화면을 업데이트하였다. 이는 불필요한 네트워크 요청을 백엔드에 한 번 더 보내게 되므로 좋은 방법이 아니다.

추가적인 네트워크 요청 없이 프론트엔드의 apollo 저장소에 직접 자바스크립트로 CRUD를 하게 되면 대규모 환경에서 더 효율적으로 서비스를 제공할 수 있따.

// refetch

  const { data, refetch } = useQuery(FETCH_BOARDS, {
    variables: { page: 1 },
  });

  function onClickPage(event) {
    refetch({ page: Number(event.target.id) });
  }

Apollo

  • GraphQL을 편하게 사용할 수 있도록 도와주는 라이브러리
  • client와 server 어디에든 사용 가능

GraphQL: SQL같은 쿼리 언어

ApolloState 직접 수정

  • readQuery: 데이터를 validate하기 위해 쿼리를 넘겨줘야한다.
  • writeQuery: 데이터로부터 자동으로 쿼리를 생성해서 쓴다.
    modify

cache 오브젝트의 readQuery, writeQuery 함수를 제공하여 caching 데이터에 쿼리를 할 수 있도록 해준다.

const { data: dataBoards } = useQuery(FETCH_BOARDS);

const [createBoard] = useMutation(CREATE_BOARD, {
  update(cache, { data }) {
    const prevBoards = cache.readQuery({ query: FETCH_BOARDS });
		const nextBoards = data.createBoard.createBoard

    if (newBoard && existingBoards) {
      cache.writeQuery({
        query: FETCH_BOARDS,
        data: {
          fetchBoards: [...prevBoards.fetchBoards, nextBoards]
        },
      });
    }
  },
});

Apollo Cache State

불러온 게시물 10개 중 3번째 게시물으 삭제하는데, refetch 없이 3번째 게시물만 지우기

기존:

  • 삭제 Api 요청 → refetchQuery
  • fetchBoard을 10개를 refetch를 한 것 비효율적이다.
  • 추가적인 query 안날리고 즉시 cache state 바로 바꾸면 된다.
import { gql, useMutation, useQuery } from "@apollo/client";

const FETCH_BOARDS = gql`
	...
`;

const DELETE_BOARD = gql`
	...
`;

const CREATE_BOARD = gql`
	...
`;

export default function ApolloCacheStatePage() {
  const { data } = useQuery(FETCH_BOARDS);
  const [deleteBoard] = useMutation(DELETE_BOARD);
  const [createBoard] = useMutation(CREATE_BOARD);

  // boardId = 3번째 게시물 Id
  const onClickDelete = (boardId) => async () => {
    // 가장 가까운 함수에 async 붙여줘야함
    
    // onClickDelete를 했을 때 boardId를 넘겨준다.
    await deleteBoard({
      variables: {
        boardId: boardId,
      },
      update(cache, { data }) {
        // variables 요청을 하고 끝나면 update 실행된다.
        const deletedId = data.deleteBoard;
        cache.modify({
          fields: {
            fetchBoards: (prev, { readField }) => {
              // 1. 기존의 fetchBoards 10개에서, 지금 삭제된 ID를 제외한 9개를 만들고
              // 2. 그렇게 만들어진 9개의 새로운 fetchBoards 를 return 하여, 덮어씌우기
              // fields: fetchBoard, createBoard 이런 API들 캐시의 어떤 필드를 수정할 것인가?
              // 기존 fetchBoard에 10개가 있었는데 [ ] 로  (기존의 10개)fetchBoards를 덮어씌운다.
              
              //prev: 기존의 fetchBoard 10개를 가지고 와야한다. 
              // prev.filter : 10개에서 deletedId가 들어있는거 빼고 9개로 필터링 한다.
	      // newFetchBoards 변수에 넣어준다. 9개가 필터링 된 변수
              // readField: id 뽑아오는 방법

              const newFetchBoards = prev.filter(
                (el) => readField("_id", el) !== deletedId
                // id를 el에서 뽑는다. 
                // 이렇게 뽑힌 id가 deletedId 삭제된 id가 아닌 것들만 return 한다.
              );
              return [...newFetchBoards];
              // neFetchBoards: 9개 데이터가 들어있다. 9개가 기존의 10개 데이터를 덮어씌운다. refetch없이 삭제 가능
            },
          },
        });
      },
    });
  };

// creBoard를 날려주면 createBoardInput 내용이 db안에는 하나의 row가 생성된다. data가 update에 들어간다.
  const onClickCreate = () => {
    createBoard({
      variables: {
        createBoardInput: {
          writer: "테스트",
          password: "123",
          title: "테스트제목",
          contents: "테스트내용",
        },
      },
      update(cache, { data }) {
        cache.modify({
          fields: {
            fetchBoards: (prev) => {
              // 추가된 createBoard 결과물과 이전의 10개를 합쳐서 11개를 돌려주기
              return [data.createBoard, ...prev]; // data.createBoard 를 뒤에 넣으다면 뒤에 새로 생기게된다.
            },
          },
        });
      },
    });
  };
// UpdateBoard를 한다면 수정한 id만 바꿔주면 된다. 기존 10개에서  수정한 id가 있는 것을 찾는다. 그 부분만  업데이트로 들어온데이터로 바꿔주면 된다.  

  return (
    <>
      {data?.fetchBoards.map((el) => (
        <div key={el._id}>
          <span>{el.writer}</span>
          <span>{el.title}</span>
          <span>{el.contents}</span>
          // hof id={el._id}
          <button onClick={onClickDelete(el._id)}>삭제하기</button>
        </div>
      ))}
      <button onClick={onClickCreate}>등록하기</button>
    </>
  );
}

data: api를 요청을 하면 결과(응답)이 온다. 응답 = Id ▶︎ 이 결과가 데이터로 들어간다.


1 의 요청이 끝나면 2 실행. data = 1의 result data에 result 가 들어오면서 요청이 끝나면 2가 실행. cache.modify({}) 캐시를 수정한다.


1 요청이 나가고 2결과가 3에 들어간다. 요청이 끝나면 결과와 함께 3이 실행된다.


{} 부부늘 갈아끼어줘야한다.

0개의 댓글