폼을 대신 만들어주는 도구이다. 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 눌렀을 때 값이랑 다른 경우도 있을 수 있다.
검증 라이브러리
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
: 여러기능이 있다. ~를 등록한다.resolver
: 이 함수는 Yup, Joi, Superstruct 등의 외부 유효성 검사 방법들을 실행한다. yup
과 연결시켜주는 도구 불러와서(yupResolver
)yupResolver(schema)
: 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("비밀번호는 반드시 입력해주세요!"),
});
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
로 쉽게 화면을 업데이트하였다. 이는 불필요한 네트워크 요청을 백엔드에 한 번 더 보내게 되므로 좋은 방법이 아니다.
추가적인 네트워크 요청 없이 프론트엔드의 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같은 쿼리 언어
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]
},
});
}
},
});
불러온 게시물 10개 중 3번째 게시물으 삭제하는데, refetch
없이 3번째 게시물만 지우기
기존:
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이 실행된다.
{} 부부늘 갈아끼어줘야한다.