[ React ] Hooks 사용해서 섹시한 useForm 만들기

junghyeonsu·2021년 5월 19일
6
post-thumbnail

모든 웹사이트를 만들 때 들어가지 않는 곳이 없는
회원가입 Form을 React로 만들 때 다들 어떤 식으로 만드시나요?

이번에 저도 똑같이 회원가입 Form을 만들었는데 정말 세련되지 못한 것 같아서
이번에 React hooks를 사용한 useForm 만들기를 하면서 했던 내용들을 공유하려고 합니다.

모든 코드는 Typescript를 기준으로 작성되었고,
UI 라이브러리는 Chakra UI를 사용했습니다.

📌 섹시하지 못한 Form

우선 Form을 만들 때 고려해야 하는 것들이 있습니다.

  • Input handler (onChange 함수)
  • Error text (validation 함수)

위 사진처럼 이름만 받고, 버튼을 클릭하면 회원가입이 된다고 합시다.
그러면 작성해야하는 코드는 아래와 같습니다.

☑️ State

  const [name, setName] = useState<string>("");
  const [nameErrorText, setNameErrorText] = useState<string>("");
  const [isLoading, setIsLoading] = useState<boolean>(false);

우선 Input의 값을 담아줄 state가 필요합니다. 그리고 Error Text를 띄울 state와 제출 버튼을 클릭했을 때 로딩중인지 나타낼 state 이렇게 생성했습니다.

☑️ onChange Handler

  const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setName(event.target.value);
  };

Input 값이 변경될 때마다 값을 갱신시켜 줄 핸들러를 생성합니다.

☑️ validation function

 const validateName = () => {
    if (!name) {
      setNameErrorText('이름을 입력해주세요.');
    } else if (name.length < 2) {
      setNameErrorText('이름은 최소 1자 이상입니다.');
    } else {
      setNameErrorText('');
    }
  };

제출 버튼을 클릭했을 때 Input 값을 판별할 validation function 또한 선언해주었습니다.

☑️ Submit function

const handleSubmit = async (event: React.SyntheticEvent) => {
  event.preventDefault(); // 새로고침 방지
  validateName(); // Input값 검증
  setIsLoading(true);
};

// isLoading 값이 바뀌면 실행
useEffect(() => {
    (async function() {
      if (isLoading) {
        if (!nameErrorText) {
          await new Promise((r) => setTimeout(r, 2000));
          toast({
            title: `회원가입 되었습니다!`,
            description: `${name}님 환영합니다!`,
            status: 'success',
            duration: 9000,
            isClosable: true,
          });
        }
        setIsLoading(false);
      }
    })();
  }, [isLoading]);

그리고 제출 함수를 작성해줍니다.
useEffect 안에는 async - await 구문을 사용하기 위해서 IIFE(즉시실행함수) 구문을 사용했습니다.
isLoading 값이 변경되면 제출된 값에 대해서 Alert(toast)를 띄우는 간단한 함수입니다.

이렇게 작성하면 문제점이 뭘까요?

우선 Form에 필요한 데이터는 name만 있는게 아닙니다.
보통 회원가입 페이지를 만들면 id, password, email 등등 많은 값들이 필요하게 됩니다.

그럼 위에서 작성한 것처럼 만들면
onChange 핸들러를 그 수에 맞게 만들고,
validation 함수도 그에 맞게 만들어야 합니다.
그럼 자연스럽게 해당 값들을 관리할 state들이 늘어나게 되죠.

코드도 길어질 뿐더러 정말 섹시하지 못합니다.

이렇게 만드는 사람이 있냐구요?

그럼 섹시하게 Form을 만드는 방법은 무엇일까요?

📌 섹시한 Form

섹시한 Form 을 작성하기 위한 아이디어로는 중복된 코드를 줄이는 것입니다.

우선 섹시하지 못한 Form 의 중복되는 코드들은

  1. 각종 state 들 (input value, error text)
  2. onChange Handler 함수들
  3. validation 함수들

정도로 나눌 수 있습니다.

그래서 우리는 React Hooks 를 이용해서 useForm 을 제작할 겁니다!

✅ Hooks? useForm?

그럼 Hooks 는 무엇일까요? 잠깐 짚고 넘어가면

React Hooks은 리액트의 새로운 기능으로 React 16.8버전에 새로 추가된 기능으로 state, component에 대한 것들을 바꿔놓았습니다.
React Hooks

예를 들면 function component에서 state을 가질 수 있게 된 것이죠
만일 앱을 react hook을 사용하여 만든다면 class component, render 등을 안해도 된다는 뜻입니다.
모든 것은 하나의 function이 되는 것 함수형 프로그래밍이 가능해지는 것입니다.

보통 Hooks 들의 이름 앞에는 use 를 붙입니다.
그래서 요약을 하자면 우리는 form 을 만들건데,
render 함수 또는 생명주기 함수들을 사용하지 않는 hook 을 사용하니, 이름은 useForm 짓자.

이렇게 됩니다.

✅ useForm 제작

우선 useForm의 전체 코드를 살펴보고 하나하나씩 뜯어보겠습니다.

// useForm.ts
import { useEffect, useState } from "react";

function useForm({ initialValues, onSubmit, validate }: useFormProps) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setValues({ ...values, [name]: value });
  };

  const handleSubmit = async (event: React.SyntheticEvent) => {
    setIsLoading(true);
    event.preventDefault();
    await new Promise((r) => setTimeout(r, 1000));
    setErrors(validate(values));
  };

  useEffect(() => {
    if (isLoading) {
      if (Object.keys(errors).length === 0) {
        onSubmit(values);
      }
      setIsLoading(false);
    }
  }, [errors]);

  return {
    values,
    errors,
    isLoading,
    handleChange,
    handleSubmit,
  };
}

export default useForm;

useForm의 매개변수들

function useForm({ initialValues, onSubmit, validate }: useFormProps) {

우선 useForm 을 사용하는 곳에서 받아올 매개변수들입니다.

initialValues 는 말 그대로 초기값 입니다.
이름, 아이디, 패스워드, 이메일등등의 값들을 하나씩 정의하는 것이 아닌
객체로 생성해서 하나의 state로 관리를 합니다.

useForm({
  initialValues: {
    id: '',
    password: '',
  },
});

이런식으로 useForm 을 사용하는 곳에서 state 들을 정의할 수 있습니다.

onSubmitvalidate 도 같은 맥락으로 제출 함수와 검증 함수를 useForm 을 사용하는 곳에서 선언에서 사용할 수 있도록 하는 것입니다.

state, onChange handler

// useForm.ts
const [values, setValues] = useState(initialValues);

위에서 받은 initialValuesuseForm 안에서 values 라는 state로 저장이 됩니다.
그러면 각각의 state 들은 어떻게 할까요? useForm 안에서의 onChange 는 어떻게 작성될까요?

// SignUpForm.tsx
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setValues({ ...values, [name]: value });
  };

섹시하지 못한 Form 에서 각각의 state 들마다 handler를 하나씩 생성해주었는데, useForm 에서는 하나로 통일했습니다.

들어오는 event.target 객체의 namevalue 를 받습니다.
그래서 Input 컴포넌트의 name 속성으로 state와 같은 이름을 지정해줘야 합니다.

<Input name="id" />

이런식으로 말이죠.

이렇게 코딩을하면 Input 컴포넌트가 많아진다해도, name 만 변경해주고, 그에 맞는 initialValues 만 선언해주면 handleChange 함수를 또 만들지 않아도 됩니다.

Validation

그럼 입력 검증은 어떤식으로 진행될까요?

우선 저는 validation을 코드를 적을 파일을 새로 작성해주었습니다.

// SignUpValidation.ts
type SignUpValidationProps = {
  name?: string,
  email?: string,
  id?: string,
  password?: string,
}

export default function SignUpValidation({ name, email, id, password }: SignUpValidationProps) {
  const errors: SignUpValidationProps = {};

  if (!name) {
    errors.name = '이름이 입력되지 않았습니다.'
  } 
  /*
  else if (Some regex validation) {
    errors.name = 'Some error text';
  } 
  */

  if (!email) {
    errors.email = '이메일이 입력되지 않았습니다.';
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email)) {
    errors.email = '입력된 이메일이 유효하지 않습니다.';
  }

  if (!id) {
    errors.id = '아이디가 입력되지 않았습니다.';
  }
  /*
  else if (Some regex validation) {
    errors.id = 'Some error text';
  } 
  */

  if (!password) {
    errors.password = '비밀번호가 입력되지 않았습니다.';
  } else if (password.length < 8) {
    errors.password = '8자 이상의 패스워드를 사용해야 합니다.';
  }

  return errors;
}

저는 4개의 값을 받고, 해당 값에 대한 검증을 진행했습니다.
그리고 각 validation에 걸린다면, errors 객체에 error text를 달아서 반환해주는 식입니다.

그리고 useForm 을 사용하고자 하는 컴포넌트에서

// SignUpForm.tsx
import SignUpValidation from './SignUpValidation';

 const {
   values, errors, isLoading, handleChange, handleSubmit
 } = useForm({
    initialValues: ...,
    onSubmit: ...,
    validate: SignUpValidation,
  });

아까 useForm.ts 파일에서 validation 을 받도록 되어있기 때문에,
validate: SignUpValidation 이런식으로 validation 함수를 넘겨줍니다.

그러면 useForm 에서는

// useForm.ts
  const handleSubmit = (event: React.SyntheticEvent) => {
    setIsLoading(true);
    event.preventDefault();
    setErrors(validate(values));
  };

  useEffect(() => {
    (async () => {
      if (isLoading) {
        if (Object.keys(errors).length === 0) {
          await onSubmit(values);
        }
        setIsLoading(false);
      }
    })();
  }, [errors, isLoading]);

제출 함수인 handleSubmit 이 실행될 때, errors state의 setter인 setErrors 를 통해서 저희가 넘겨준 validate 함수 SignUpValidation 함수를 실행해서 errors 객체를 state로 변경시켜줍니다.

그럼 errors state가 변경되었으니, 아래 useEffect 가 실행이 되겠죠
그럼 errorskey 의 개수를 파악하는 식으로 에러가 났는지 안났는지 확인할 수 있습니다.

if (Object.keys(errors).length === 0) {
   // Submit API 호출!
}

다음과 같이 말이죠.

그럼 validation 진행은 알았고, error text 는 어떻게 나타낼까요?

useForm 을 사용하는 컴포넌트에서 errors state를 사용하면 됩니다.

const FormComponent = () => {
    const {
    values,
    errors,
    isLoading,
    handleChange,
    handleSubmit, 
  } = useForm({
    initialValues:..., 
    onSubmit:...,
    validate:...,
  });
 return (
   <form>
     <input type="email" name="email" />
     <p>{errors.email}</p>
   </form>
 );
}

이런식으로요

📌 마무리

useForm 을 사용해서 form 을 작성하는게 정답인 것은 아닙니다.
이것보다 더 좋은 코드가, 효율적인 코드가 있을 수도 있습니다!
이렇게 작성하는 방법도 있구나, 라고 생각해주시면 감사하겠습니다.
맨날 그냥 코드를 작성하다가, 어떻게 하면 더 효율적으로 더 깔끔하게 짤 수 있을까를 생각하면 점점 더 실력이 느는 것 같습니다.

저기서 성능 최적화까지 진행된다면 더욱더 섹시한 코드가 되겠죠.
저도 이번에 form 을 작성하면서 이렇게도 짤 수 있구나를 알게되어서 되게 좋았습니다.

참고

[React Hook] 양식(form)에 적용하기
React Hooks 공식문서

profile
끊임없는 성장을 추구

0개의 댓글