[포스코x코딩온] KDT-Web-8 13주차 회고2 react-hook-form

Yunes·2023년 9월 28일
0

[포스코x코딩온]

목록 보기
34/47
post-thumbnail

서론

추석기간동안 과제로 BE, FE 를 합친 todo 앱 만들기가 주어졌다.

때마침 리액트 스터디에서 redux 로 todo 앱을 만들었던 게 있어서 해당 프로젝트에 BE 와 react-hook-form 을 추가하는 식으로 코드를 짜봤다.

이번 과제를 진행하며 아래의 사항에 대해 새로이 알 수 있었다.

  1. redux 에서 비동기 로직 ( HTTP API 호출 ) 을 어떻게 처리하는지
  2. react-hook-form 을 어떻게 사용하는지

결과 미리보기

할일 추가, 삭제, 수정, 전체 조회 기능의 REST API 를 붙였고 그 외에 필터 기능을 추가하여 모든 할일 체크하기와 완료한 일 삭제하기, 필터기능 적용하기 등을 만들었다.

redux 에서 HTTP API 호출하기

그러기 위해 먼저 BE 코드가 준비되어야 했다.

BE 의 요구사항은 다음과 같다.

  • Node.js express 프레임워크와 MySQL 을 사용하여 BE 앱 개발
  • 이때 db 는 todo 라는 테이블에 id, title, done 이 필요
  • REST API 는 Get /todos, POST /todo, PATCH /todo/:todoId, DELETE /todo/todoId 가 필요햐다.

BE 개발하기

sequelize 도 사용해도 좋으나 구조가 todo 테이블 하나밖에 없어서 그냥 mvc 패턴도 쓰지 않고 Node.js express 에 MySQL 만 사용하는 식으로 코드를 구성했다.

├── server
│   ├── controller
│   │   └── Cmain.js
│   ├── index.js
│   ├── model
│   │   └── Model.js
│   ├── package-lock.json
│   ├── package.json
│   ├── routes
│   │   └── main.js

컨트롤러, 모델을 각각 하나씩 사용했고 view 에 ejs 를 사용할 필요 없이 React 에서 화면을 띄울 것이라 controller 에서는 데이터만 전달하려고 res.json 을 사용했다.

// server/controller/Cmain.js

const model = require("../model/Model");

const getTodos = (req, res) => {
  model.db_getTodos(undefined, (result) => {
    res.json(result);
  });
};

const postTodo = (req, res) => {
  model.db_postTodo(req.body, () => {
    res.json({ result: true });
  });
};

const patchTodo = (req, res) => {
  const id = req.params.todoId;
  const { title, done } = req.body;

  model.db_update({ id, title, done }, () => {
    res.json({ result: true });
  });
};

const deleteTodo = (req, res) => {
  const id = req.params.todoId;

  model.db_delete({ id }, () => {
    res.json({ result: true });
  });
};

module.exports = {
  getTodos,
  postTodo,
  patchTodo,
  deleteTodo,
};
// server/model/Model.js

const mysql = require("mysql");
require('dotenv').config()

//mysql연결
const conn = mysql.createConnection({
  host: process.env.MYSQL_HOST,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
  database: process.env.MYSQL_DB,
  port: process.env.MYSQL_PORT,
});

conn.connect(function (err) {
  if (err) throw err;
  console.log("connected!");
});

const db_getTodos = (data, cb) => {
  const query = "select * from todo";
  conn.query(query, (err, result) => {
    if (err) {
      console.error(err);
      return;
    }
    cb(result);
  });
};

const db_postTodo = (data, cb) => {
  const query = "insert into todo (title, done) values (?, ?)";
  conn.query(query, [data.title, data.done], (err, result) => {
    if (err) {
      console.error(err);
      return;
    }
    cb();
  });
};

const db_update = (data, cb) => {
  const query = "update todo set title = ?, done= ? where id = ?";

  conn.query(query, [data.title, data.done, data.id], (err, result) => {
    if (err) {
      console.error(err);
      return;
    }
    cb();
  });
};

const db_delete = (data, cb) => {
  const query = "delete from todo where id = ?";

  conn.query(query, [data.id], (err, result) => {
    if (err) {
      console.error(err);
      return;
    }
    cb();
  });
};

module.exports = {
  db_getTodos,
  db_postTodo,
  db_update,
  db_delete,
};
// server/routes/main.js

const express = require("express");
const router = express.Router();
const controller = require("../controller/Cmain");

router.get("/todos", controller.getTodos);

router.post("/todo", controller.postTodo);

router.patch("/todo/:todoId", controller.patchTodo);

router.delete("/todo/:todoId", controller.deleteTodo);

module.exports = router;

서버의 index.js 엔트리보인트에 혹시몰라 cors 설정을 위한 코드를 추가해두었다.

// server/index.js

const express = require("express");
const app = express();
const PORT = 8000;
const cors = require("cors");

app.use(cors());

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

//router 분리
const router = require("./routes/main");
app.use("/", router);

//오류처리
app.use("*", (req, res) => {
  res.status(404).render("404");
});

app.listen(PORT, () => {
  console.log(`http://localhost:${PORT}`);
});

각 코드를 짜는동안 postman 으로 동작 여부를 체크했다.

db 와 테이블 자체는 workbench 에서 직접 생성했다.

create database mini;

use mini;

create table todo (
	id int not null primary key auto_increment,
    title varchar(100) not null,
    done tinyint(1) not null default 0
);

select * from todo;

FE 개발하기

기왕 redux 를 사용하려고 한거 react-hook-form 을 활용하고자 했다.

기존에 redux 로 todo 앱을 만들었을 때는 DB 를 연결해두지 않아서 slice 에서 reducer 를 만들어 사용했는데 db 를 연동하고자 하니 axios 로 api 를 호출해야 하기도 하고 비동기 처리를 위해 createAsyncThunk 라는 메서드를 사용해야 했다.

먼저 redux 를 어떻게 활용했는지 정리해보자.

프론트엔드 클라이언트 구조는 다음과 같다.

├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── app
│   │   └── store.js
│   ├── features
│   │   ├── filters
│   │   │   ├── Filter.jsx
│   │   │   └── filtersSlice.js
│   │   └── todos
│   │       ├── Todo.jsx
│   │       └── todosSlice.js

비동기가 아니라면 reducer, action 등을 redux/toolkit 의 createSlice 를 통해 한번에 생성이 가능했는데 API 를 연결해야 하다보니 비동기 처리가 필요하여 createAsyncThunk 를 사용해야 했다.

createAsyncThunk

redux/tookit 의 메서드로 redux action type 문자열promise 를 반환하는 콜백 함수 를 갖는 함수를 말한다.

이 함수는 전달한 액션 타입에 기반한 promise 생명주기의 액션을 생성하고 promise callback 을 실행할 thunk action 생성자를 반환한다. 그리고 반환된 promise 에 기반한 생명주기 action 을 dispatch 한다.

즉, createAsyncThunk 는 async 요청을 다루는 과정을 표준의 권장된 방식으로 추상화한다.

이 함수는 action.payload 처럼 어떤 데이터를 fetching 할 것인지, 어떻게 loading stating 를 추적하길 원하는지 혹은 반환될 데이터가 어떻게 진행될지 알 수 없기에 어떤 리듀서 함수도 생성하지 않는다.

따라서 createAsyncThunk 는 비동기 로직을 수행하거나 그 결과 특정 데이터를 생성해주기만 하고 그 이후 해당 데이터 및 로직 수행의 결과를 어디서 가져다 사용하고 store 의 어떤 state 에 저장하고 활용할지는 직접 처리해줘야 한다.

다음은 Redux Toolkit docs 에서 소개하는 예시코드다.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// First, create the thunk
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId: number, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

interface UsersState {
  entities: []
  loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}

const initialState = {
  entities: [],
  loading: 'idle',
} as UsersState

// Then, handle actions in your reducers:
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    // standard reducer logic, with auto-generated action types per reducer
  },
  extraReducers: (builder) => {
    // Add reducers for additional action types here, and handle loading state as needed
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      // Add user to the state array
      state.entities.push(action.payload)
    })
  },
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))

createAsyncTHunk 함수의 결과를 활용하는 리듀서 동작을 추가하기 위해 extraReducer 에 addCase 를 통해 state 관리를 하고 있음을 알 수 있다.


parameter

createAsyncThunk 는 3가지 파라미터를 가질 수 있다.

  1. action type value
  • action type value 에 users/requestStatus 를 전달했다면 이는 다음 3가지 액션 타입을 생성해준다.
    • pending : users/requestStatus/pending
    • fulfilled : users/requestStatus/fulfilled
    • rejected : users/requestStatus/rejected
  1. preloadCreator callback
  • 비동기 로직의 결과를 포함하는 promise 를 반환해야 하는 콜백함수이다. 이 함수는 값을 동기적으로 반환할수도 있다.
  • 만약 콜백함수를 실행하는데 오류가 발생한다면 Error 인스턴스를 포함하는 rejected 프로미스를 반환할 것이다.
  • preloadCreator 는 2개의 인자를 갖고 호출할 수 있다.
    • arg : 하나의 값을 전달해야 하는데 여러 값을 전달하기 원할시 객체에 담아 전달한다.
    • thunkAPI : Redux thunk function 으로 전달되는 파라미터들을 포함하는 객체
      • dispatch : store 의 dispatch 메서드
      • getState : store 의 getState 메서드
        ...
  1. options : condition, dispatchConditionRejection, idGenerator(arg) 등의 선택 필드들을 갖는 객체인데 실제로 사용해보지는 않았다.

실제로 적용해본 코드

export const editTodo = createAsyncThunk(
  "todos/editTodo",
  async ({ id, newText, completed }, { dispatch, getState }) => {
    const currentState = getState();
    const isDone = currentState.todos.todos.find(
      (todo) => todo.id === id
    ).completed;

    try {
      const response = await axios.patch(
        `http://localhost:8000/todo/${id + 1}`,
        {
          title: newText,
          done: isDone ? 1 : 0,
        }
      );

      if (response.data.result) {
        dispatch(
          todosSlice.actions.editTodoState({
            id,
            text: newText,
            completed,
          })
        );
      }
    } catch (error) {
      console.error("Error editing todo:", error);
      throw error;
    }
  }
);

createAsyncTHunkRedux thunk action creator 를 반환하기 때문에

dispatch(editTodo)
dispatch(editTodo.pending)
dispatch(editTodo.fulfilled)
dispatch(editTodo.rejected)

같이 코드를 사용할 수 있다.

import { createAsyncThunk } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';

// Define an asynchronous thunk action
export const fetchUserById = createAsyncThunk(
  'user/fetchById',
  async (userId, thunkAPI) => {
    try {
      // Simulate an API call or any asynchronous operation
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) {
        throw new Error('Failed to fetch user data');
      }
      const userData = await response.json();
      return userData;
    } catch (error) {
      throw error;
    }
  }
);

// Example usage in a React component
const MyComponent = () => {
  const dispatch = useDispatch();

  const userId = 123; // Replace with the desired user ID

  const fetchData = async () => {
    try {
      // Dispatch the pending action
      dispatch(fetchUserById.pending());

      // Dispatch the asynchronous operation
      const resultAction = await dispatch(fetchUserById(userId));

      // Check if the operation was fulfilled
      if (fetchUserById.fulfilled.match(resultAction)) {
        // Dispatch the fulfilled action
        dispatch(fetchUserById.fulfilled(resultAction.payload));
      } else if (fetchUserById.rejected.match(resultAction)) {
        // Dispatch the rejected action
        dispatch(fetchUserById.rejected(resultAction.error));
      }
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return (
    <div>
      <button onClick={fetchData}>Fetch User</button>
    </div>
  );
};

export default MyComponent;

Promsie Lifecycle Actions

createAsyncThunkpending, fulfilled, rejected 같은createAction 를 사용하는 3가지 Redux action createor 를 생성한다.

이런 각각의 생명주기 액션 생성자는 dispatche 될때 reducer 로직이 액션 타입을 참조하고 액션에 응답할 수 있도록 반환된 액션 생성자에 첨부될 것이다.

각 액션 객체는 action.meta 아래 현재의 유일한 requestIdarg 값을 포함할 것이다.

실제 코드에서 사용해본 예시

extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.todos = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        console.error("Error fetching todos:", action.error);
      });
  },

각강의 생명주기 액션 생성자 인터페이스 구조

interface PendingAction<ThunkArg> {
  type: string
  payload: undefined
  meta: {
    requestId: string
    arg: ThunkArg
  }
}

interface FulfilledAction<ThunkArg, PromiseResult> {
  type: string
  payload: PromiseResult
  meta: {
    requestId: string
    arg: ThunkArg
  }
}

interface RejectedAction<ThunkArg> {
  type: string
  payload: undefined
  error: SerializedError | any
  meta: {
    requestId: string
    arg: ThunkArg
    aborted: boolean
    condition: boolean
  }
}

이런 액션들을 리듀서에서 다루기 위해 키 노테이션 객체나 builder callback 노테이션을 사용하여 createReducer 혹은 createSlice 에서 액션 생성자를 참조하자.

const reducer1 = createReducer(initialState, {
  [fetchUserById.fulfilled]: (state, action) => {},
})

const reducer2 = createReducer(initialState, (builder) => {
  builder.addCase(fetchUserById.fulfilled, (state, action) => {})
})

const reducer3 = createSlice({
  name: 'users',
  initialState,
  reducers: {},
  extraReducers: {
    [fetchUserById.fulfilled]: (state, action) => {},
  },
})

const reducer4 = createSlice({
  name: 'users',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchUserById.fulfilled, (state, action) => {})
  },
})

react-hook-form

input 이 들어간 부분에서 보통 react 는 다음과 같은 구조로 코드를 작성한다.

export defualt function Todo() {
	const [ inputText, setInputText ] = useState("");
  
	return (<>
        	<input type="text" value={inputText} onChange={(e) => setInputText(e.target.value)} />
        	<button type="button" onClick={() => {dispatch(todoAdd(inputText))}}>
        </>)
}

대강 작성하긴 해쓴데 input 에 들어갈 값을 매 순간 useState 로 만든 state 와 setState 를 통해 업데이트하기에 input 박스에 글자 하나를 입력할 때마다 상태가 변경되어 매번 재렌더링이 된다.

그런데 react-hook-form 을 사용하면 submit 이 될때까지 재렌더링이 되지 않는다. 심지어 그와 관련된 커스텀 메서드들을 제공하고 있어 form 을 작성하는 경우 매우 유용해 보였다.

예시 코드

import { useForm } from "react-hook-form";

export default function Form2() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
  } = useForm();

  // handleSubmit : 두개의 함수를 받는데 하나는 유효할때 실행되는 함수, 하나는 오류가 발생할때 실행되는 함수이다.
  const onValid = (data) => {
    console.log("data : ", data);
  };

  return (
    <>
      <form onSubmit={handleSubmit(onValid)}>
        <input
          placeholder="이름"
          type="text"
          {...register("name", {
            required: "이름은 필수 항목입니다.",
          })}
        />
        {errors.name?.message}
        <input
          placeholder="나이"
          type="text"
          {...register("age", {
            required: "나이를 입력하세요",
            min: { message: "0 이상의 숫자만 입력 가능합니다.", value: 0 },
          })}
        />
        {errors.age?.message}
        <button type="submit">제출</button>
      </form>
    </>
  );
}

todo 앱에서는 input 을 두군데 사용했다.

할일 수정의 경우 체크 여부, 할일 텍스트 수정, 삭제 버튼 등이 엮여있어서 할일 추가에만 react-hook-form 을 적용했다.

react-hook-form 적용해본 코드

import { useForm } from "react-hook-form";
import { todoAdded } from "./todosSlice";

export default function TodoForm({ dispatch }) {
  const { register, handleSubmit, reset } = useForm();

  const onSubmit = (data) => {
    dispatch(todoAdded(data.textInput));
    reset();
  };

  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      style={{
        display: "flex",
        justifyContent: "space-between",
        margin: "10px",
        width: "100%",
      }}
    >
      <input
        type="text"
        style={{ width: "90vw" }}
        placeholder="할일을 추가하세요"
        {...register("textInput", { required: true })}
      />
      <button type="submit">add</button>
    </form>
  );
}

react hook form 에 대해 알아보자.

react hook form

설치하기

npm install react-hook-form

예시코드

import { useForm } from "react-hook-form"


export default function App() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm()


  const onSubmit = (data) => console.log(data)


  console.log(watch("example")) // watch input value by passing the name of it


  return (
    /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* register your input into the hook by invoking the "register" function */}
      <input defaultValue="test" {...register("example")} />


      {/* include validation with required or other standard HTML validation rules */}
      <input {...register("exampleRequired", { required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}


      <input type="submit" />
    </form>
  )
}

react-hook-form 의 useForm() 메서드를 통해 여러가지 메서드를 얻을 수 있는데 그중 register 를 통해 각 input 에 대해 얻을 data 가 객체 내에 어떤 key 로 들어오고, validation 중 무엇을 확인할지 등을 정할 수 있다.

import { useForm } from "react-hook-form";
import { todoAdded } from "./todosSlice";

export default function TodoForm({ dispatch }) {
  const { register, handleSubmit, reset } = useForm();

  const onSubmit = (data) => {
    console.log("data : ", data);
    dispatch(todoAdded(data.textInput));
    reset();
  };

  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      style={{
        display: "flex",
        justifyContent: "space-between",
        margin: "10px",
        width: "100%",
      }}
    >
      <input
        type="text"
        style={{ width: "90vw" }}
        placeholder="할일을 추가하세요"
        {...register("textInput", { required: true })}
      />
      <button type="submit">add</button>
    </form>
  );
}

예를 들어 앞에서 소개했던 코드를 다시 가져와보면 register 에 첫번째 인자로 전달한 문자열이 handleSubmit 의 인자로 전달한 onSubmit 메서드의 인자가 갖는 객체의 key 가 된다.

정확하게는 onSubmit 의 인자를 data 라고 했는데 해당 data 는 브라우저 콘솔기록을 보면 알 수 있듯이 { textInput: "" } 이런 형태를 갖는 객체이다.

그래서 form 에서 input 에 값을 이름지어서 붙여주고 유효성를 판단할때 register 와 handleSubmit 등을 사용할 수 있다.

register

register 가 유효성을 평가할 수 있는 사항은 다음과 같다.

  • required
  • min
  • max
  • minLength
  • maxLength
  • pattern
  • validate
import { useForm } from "react-hook-form"


export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = (data) => console.log(data)


  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName", { required: true, maxLength: 20 })} />
      <input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
      <input type="number" {...register("age", { min: 18, max: 99 })} />
      <input type="submit" />
    </form>
  )
}

Controller

만약 Material UI 나 AntD 같이 외부 UI 라이브러리와 결합하려면 react-hook-form 의 Controller 로 감싸서 사용할 수 있다.

import Select from "react-select"
import { useForm, Controller } from "react-hook-form"
import Input from "@material-ui/core/Input"


const App = () => {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      firstName: "",
      select: {},
    },
  })
  const onSubmit = (data) => console.log(data)


  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="firstName"
        control={control}
        render={({ field }) => <Input {...field} />}
      />
      <Controller
        name="select"
        control={control}
        render={({ field }) => (
          <Select
            {...field}
            options={[
              { value: "chocolate", label: "Chocolate" },
              { value: "strawberry", label: "Strawberry" },
              { value: "vanilla", label: "Vanilla" },
            ]}
          />
        )}
      />
      <input type="submit" />
    </form>
  )
}

만약 전역 상태관리와 통합해서 사용하고 싶다면?

react-redux 의 경우 connect 를 통해 react-hook-form 과 같이 사용할 수 있다.

import { useForm } from "react-hook-form"
import { connect } from "react-redux"
import updateAction from "./actions"


export default function App(props) {
  const { register, handleSubmit, setValue } = useForm({
    defaultValues: {
      firstName: "",
      lastName: "",
    },
  })
  // Submit your data into Redux store
  const onSubmit = (data) => props.updateAction(data)


  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input type="submit" />
    </form>
  )
}


// Connect your component with redux
connect(
  ({ firstName, lastName }) => ({ firstName, lastName }),
  updateAction
)(YourForm)

그러나 redux 공식문서에 가보면 connect 는 좀 이전 버전의 메서드인 것 같다. 여전히 8.x 버전에서 지원은 하지만 useDispatch(), useSelector() 등의 hook API 를 사용하는 것을 권장하고 있었다.

음... 위의 코드는 react-hook-form 에서 get start 에 소개되고 있는 코드인데 업데이트를 안한지 좀 된건가? react redux 에서는 하지 말라는 형태의 코드 모양을 보여주고 있다.

react-redux 의 connect() 함수는 React 컴포넌트를 Redux 스토어로 연결시켜준다.

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
  • mapStateToProps : redux store 의 state 를 다루는 함수
const mapStateToProps = (state) => ({ todos: state.todos })

///

const mapStateToProps = (state, ownProps) => ({
  todo: state.todos[ownProps.id],
})
  • mapDispatchToProps : redux store 의 dispatch 를 다루는 함수 혹은 객체
const mapDispatchToProps = (dispatch) => {
  return {
    // dispatching plain actions
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
    reset: () => dispatch({ type: 'RESET' }),
  }
}

/// 

// binds on component re-rendering
<button onClick={() => this.props.toggleTodo(this.props.todoId)} />

// binds on `props` change
const mapDispatchToProps = (dispatch, ownProps) => ({
  toggleTodo: () => dispatch(toggleTodo(ownProps.todoId)),
})

Handle errors

useForm() 에 formState: { errors } 를 통해 에러를 관리할 수 있다.

import { useForm } from "react-hook-form"


export default function App() {
  const {
    register,
    formState: { errors },
    handleSubmit,
  } = useForm()
  const onSubmit = (data) => console.log(data)


  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("firstName", { required: true })}
        aria-invalid={errors.firstName ? "true" : "false"}
      />
      {errors.firstName?.type === "required" && (
        <p role="alert">First name is required</p>
      )}


      <input
        {...register("mail", { required: "Email Address is required" })}
        aria-invalid={errors.mail ? "true" : "false"}
      />
      {errors.mail && <p role="alert">{errors.mail.message}</p>}


      <input type="submit" />
    </form>
  )
}

React Hook Form API

6개의 대표적인 hook 이 있음을 알 수 있다. 아직 useForm 만 사용해봤는데 한번 알아보자.

useForm()

이 페이지에서는 useForm 이 최소한의 재렌더링으로 form 의 유효성을 확인하는 강력한 커스텀 훅이라고 소개하고 있다.

useForm 함수가 반환하는 객체가 위와 같은 메서드들을 포함하고 있다.

register

register 메서드는 input 혹은 select 요소노드를 등록할 수 있게 해주고 React Hook Form 의 규칙에 따라 유효성 검사를 실시한다.

  • 유효성 검사는 HTML 표준에 따른 것이고 커스텀 유효성 검사도 실행할 수 있다.

register 가 기본적으로 onCHange, onBlur, ref, name 을 props 로 갖고 있기에 여러 props 를 명시할 필요 없이 ...regiser 로 이름만 전달해주면 된다.

const { onChange, onBlur, name, ref } = register('firstName'); 
// include type check against field path with the name you have supplied.
        
<input 
  onChange={onChange} // assign onChange event 
  onBlur={onBlur} // assign onBlur event
  name={name} // assign name prop
  ref={ref} // assign ref prop
/>
// same as above
<input {...register('firstName')} />

구체적인 예시코드는 register 를 활용할때 docs 를 참고하자. 예시코드가 정말 세세하게 잘 제공되어 있다.

그중 두가지정도 예시를 기록해두자면 에러가 발생시 표시할 메세지를 지정할 수 있고 onChange, onBlur 등이 어떻게 동작할지도 설정할 수 있다.

	
<input
  type="number"
  {...register("test", {
    min: {
      value: 3,
      message: 'error message' // JS only: <p>error message</p> TS only support string
    }
  })}
/>

///

	
register('firstName', {
  onChange: (e) => console.log(e)
})

주의!

  • 첫번째 인자로 전달하는 name 은 필수적으로 전달해야하고 unique 해야 한다.

formState

이 객체는 전체 form 상태의 정보를 갖고 있다. 이는 form 어플리케이션에서 유저와의 상호작용을 유지하고 추적할 수 있도록 도와준다.

isDirty

  • 사용자가 입력값을 수정하면 true, 기본값이면 false
const {
  formState: { isDirty, dirtyFields },
  setValue,
} = useForm({ defaultValues: { test: "" } });


// isDirty: true
setValue('test', 'change')
 
// isDirty: false because there getValues() === defaultValues
setValue('test', '')

touchedFields

  • 사용자가 상호작용한 모든 input 을 갖는 객체

defaultValues

  • useForm 의 defaultValue 혹은 reset API 를 통해 업데이트된 defaultValue 를 통해 설정된 값 객체

isSubmitted

  • form 이 제출되었으면 true, reset 메서드가 실행되기까지 true 로 유지된다.

isLoading

  • form 이 현재 비동기 기본값을 로딩중이면 true
  • 오직 async defaultValue 에만 사용할 수 있다.
const { 
  formState: { isLoading } 
} = useForm({ 
  defaultValues: async () => await fetch('/api') 
});

submitCount

  • form 이 submitted 한 횟수

isValid

  • 만약 form 이 아무 에러도 갖지 않는다면 true

errors

  • 에러 필드들을 갖는 객체

실습때 이 errors 를 활용하여 validation 을 위반할 때의 message 를 통해 form 에 어떤 점에서 유효성에 위배되는지 나타내줄 수 있었다.

import { useForm } from "react-hook-form";

export default function Form2() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
  } = useForm();

  return (
    <>
      <form onSubmit={handleSubmit()}>
        <input
          placeholder="나이"
          type="text"
          {...register("age", {
            required: "나이를 입력하세요",
            min: { message: "0 이상의 숫자만 입력 가능합니다.", value: 0 },
          })}
        />
        {errors.age?.message}
        <button type="submit">제출</button>
      </form>
    </>
  );
}

watch

이 메서드는 구체적인 입력을 계속 지켜보고 그들의 값을 반환한다. 이 메서드는 입력 값을 렌더링하고 조건에 따라 무엇을 렌더링할지 결정할 때 사용된다.

인자로 아무것도 전달하지 않으면 모든 입력을 watch 하고 ( watch 를 뭐라고 표현해야 할지 모르겠다.. ) 특정 문자열이나 배열을 전달시 해당 이름을 갖는 입력들만 watch 할 수 있다. 또한 콜백도 따로 지정이 가능한 것 같다.

react hook form docs 는 예시코드가 상세히 나와있어서 너무 좋다..

import React from "react"
import { useForm } from "react-hook-form"


function App() {
  const {
    register,
    watch,
    formState: { errors },
    handleSubmit,
  } = useForm()
  const watchShowAge = watch("showAge", false) // you can supply default value as second argument
  const watchAllFields = watch() // when pass nothing as argument, you are watching everything
  const watchFields = watch(["showAge", "number"]) // you can also target specific fields by their names


  // Callback version of watch.  It's your responsibility to unsubscribe when done.
  React.useEffect(() => {
    const subscription = watch((value, { name, type }) =>
      console.log(value, name, type)
    )
    return () => subscription.unsubscribe()
  }, [watch])


  const onSubmit = (data) => console.log(data)


  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input type="checkbox" {...register("showAge")} />


        {/* based on yes selection to display Age Input*/}
        {watchShowAge && (
          <input type="number" {...register("age", { min: 50 })} />
        )}


        <input type="submit" />
      </form>
    </>
  )
}

handleSubmit

이 함수는 만약 form 의 유효석 검사가 성공적이었다면 form 데이터를 받는다.

handleSubmit(onSubmit)()


// You can pass an async function for asynchronous validation.
handleSubmit(async (data) => await fetchAPI(data))

props 를 보니 form 이 성공적으로 제출되면 data 를, 아니라면 errors 를 받는 것 같다.

sync

import { useForm } from "react-hook-form"


export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = (data, e) => console.log(data, e)
  const onError = (errors, e) => console.log(errors, e)


  return (
    <form onSubmit={handleSubmit(onSubmit, onError)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <button type="submit">Submit</button>
    </form>
  )
}

async

import React from "react";
import { useForm } from "react-hook-form";


const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));


function App() {
  const { register, handleSubmit, formState: { errors }, formState } = useForm();
  const onSubmit = async data => {
    await sleep(2000);
    if (data.username === "bill") {
      alert(JSON.stringify(data));
    } else {
      alert("There is an error");
    }
  };


  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="username">User Name</label>
      <input placeholder="Bill" {...register("username"} />


      <input type="submit" />
    </form>
  );
}

reset

reset 은 form 상태, field reference, subscription 등을 전부 초기화한다. 선택적인 인자를 전달해서 form state 일부만 리셋할수도 있다.

제출 이후 useEffect 내부에서 reset 하는 것을 권장하고 있다.

useEffect(() => {
  reset({
    data: "test",
  })
}, [isSubmitSuccessful])
import React, { useCallback } from "react"
import { useForm } from "react-hook-form"


export default function App() {
  const { register, handleSubmit, reset } = useForm()
  const resetAsyncForm = useCallback(async () => {
    const result = await fetch("./api/formValues.json") // result: { firstName: 'test', lastName: 'test2' }
    reset(result) // asynchronously reset your form values
  }, [reset])


  useEffect(() => {
    resetAsyncForm()
  }, [resetAsyncForm])


  return (
    <form onSubmit={handleSubmit((data) => {})}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />


      <input
        type="button"
        onClick={() => {
          reset(
            {
              firstName: "bill",
            },
            {
              keepErrors: true,
              keepDirty: true,
            }
          )
        }}
      />


      <button
        onClick={() => {
          reset((formValues) => ({
            ...formValues,
            lastName: "test",
          }))
        }}
      >
        Reset partial
      </button>
    </form>
  )
}

reset 은 사용할 경우가 자주 있을것 같아서 docs 주소를 남긴다. 추후 코드 작성시 예시코드를 참고하자.

setError

이 함수는 하나 혹은 그 이상의 오류를 수동으로 설정할 수 있게 해준다.

import { useForm } from "react-hook-form"


const App = () => {
  const {
    register,
    setError,
    formState: { errors },
  } = useForm()


  return (
    <form>
      <input {...register("test")} />
      {errors.test && <p>{errors.test.message}</p>}


      <button
        type="button"
        onClick={() => {
          setError("test", { type: "focus" }, { shouldFocus: true })
        }}
      >
        Set Error Focus
      </button>
      <input type="submit" />
    </form>
  )
}

getValues

form 값을 읽는데 최적화된 헬퍼이다. watch 와 getValue 의 차이점으로 getValue 는 재렌더링을 촉발하거나 입력의 변화에 subscribe 를 하고 있지 않다는 점이 있다.

그래서 예시코드에서도 getValues 가 지속적으로 변화사항을 추적하고 있지 않으니 onClick 에서 실행하도록 하고 있는 것 같다.

import { useForm } from "react-hook-form"


export default function App() {
  const { register, getValues } = useForm()


  return (
    <form>
      <input {...register("test")} />
      <input {...register("test1")} />


      <button
        type="button"
        onClick={() => {
          const values = getValues() // { test: "test-input", test1: "test1-input" }
          const singleValue = getValues("test") // "test-input"
          const multipleValues = getValues(["test", "test1"])
          // ["test-input", "test1-input"]
        }}
      >
        Get Values
      </button>
    </form>
  )
}

trigger

수동으로 form 혹은 입력에 대해 유효성 검사를 실행하도록 할 수 있다. 이 메서드는 임력 유효성 검사가 다른 입력값에 의존적인 dependant validation 을 해야할때 유용하다.

import React from "react"
import { useForm } from "react-hook-form"


export default function App() {
  const {
    register,
    trigger,
    formState: { errors },
  } = useForm()


  return (
    <form>
      <input {...register("firstName", { required: true })} />
      <input {...register("lastName", { required: true })} />
      <button
        type="button"
        onClick={async () => {
          const result = await trigger("lastName")
          // const result = await trigger("lastName", { shouldFocus: true }); allowed to focus input
        }}
      >
        Trigger
      </button>
      <button
        type="button"
        onClick={async () => {
          const result = await trigger(["firstName", "lastName"])
        }}
      >
        Trigger Multiple
      </button>
      <button
        type="button"
        onClick={() => {
          trigger()
        }}
      >
        Trigger All
      </button>
    </form>
  )
}

control

이 객체는 React Hook Form 으로 컴포넌트를 등록하기 위한 메서드를 포함하고 있다.

그러나 이 객체에 직접적으로 접근해서는 안되며 control은 내부적으로 사용하기 위한 객체이다.

import { useForm, Controller } from "react-hook-form"


function App() {
  const { control } = useForm()


  return (
    <Controller
      render={({ field }) => <input {...field} />}
      name="firstName"
      control={control}
      defaultValue=""
    />
  )
}
profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글