폼을 대신 만들어주는 도구이다. 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이 실행된다.
{} 부부늘 갈아끼어줘야한다.