티스토리 뷰

주소값을 다양한 곳에서 받기때문에 주소 Form hook 만들기를 시도해보았다...! 

신세계.. 

 

훅으로 만드는 이유는 여러곳에서 사용되어야 하는 경우가 주된 이유일 것으로 생각이 든다.

이때 useFormContext 훅을 사용하면 컨텍스트에서 register와 error 직접 접근할 수 있도록 해준다.

 

이때 사용할 수 있는 라이브러리로 react-hook-form 이다. 

🔧설치 명령어
// react-hook-form
yarn add react-hook-form​

// DaumPostcodeEmbed
yarn add react-daum-postcode

 

내가 작성한 코드를 기준으로 설명을 풀어가보도록 하겠다...!!

 

셋팅..?

처음 사용하기 위해서는 다음과 같은 셋팅이 필요하다.

useFormContext에서 제공해주는 메서드를 사용하기 위해서 인데, 

나의 경우 상위 컴포넌트에서 제출할 예정으로 handleSubmit 메서드는 제외!

  const {
    register,
    setValue,
    formState: { errors },
  } = useFormContext<AddressFormType>();

 

 

setValue

setValue에는 저장이 필요한 값을 담아주어야 한다.

나의 경우 DaumPostcodeEmbed에서 주소를 검색하고 검색된 주소 값을 input값에 담도록 했다.

  const handleComplete = (data: any) => {
    let fullAddress = data.address;
    let extraAddress = "";

    if (data.addressType === "R") {
      if (data.bname !== "") {
        extraAddress += data.bname;
      }
      if (data.buildingName !== "") {
        extraAddress +=
          extraAddress !== "" ? `, ${data.buildingName}` : data.buildingName;
      }
      fullAddress += extraAddress !== "" ? ` (${extraAddress})` : "";
    }

    setValue("address", fullAddress, { shouldValidate: true });
    setIsOpen(false);
  };

 

 

 

register

register는 setValue에 담긴 값을 사용할 수 있다. 

나의 경우는 input을 readOnly를 주어 읽기만 가능하도록 하고,

setValue에 담긴 값을 input에 보여질 수 있도록 해주었다.

required에는 에러로 보여줄 값을 작성해두었다.

          <input
            readOnly
            placeholder="주소를 입력해주세요"
            {...register("address", { required: "값이 입력되지 않았습니다." })}
            type="text"
            name="address"
            id="address"
            className="border rounded-lg h-10 px-2 w-full"
          />

 

 

errors

errors는 required에 담긴 값을 사용할 수 있다. 

        {errors.address && errors.address.type === "required" && (
          <span className="text-warning-color text-xs">
            {errors.address.message}
          </span>
        )}

 

그리고 이것을 어떻게 상위컴포넌트에서 사용하느냐 

 

methods 정의하기

methos는 FormProvider 컨텍스르를 통해 폼 상태를 하위컴포넌트에 제공하기 때문에,

useForm에서 반환된 모든 메서드와 상태를 FormProvider에 전달하는것이 중요하다고 한다.

  const methods = useForm<SignInFormType>();

 

<FormProvider/>로 감싸주기

<FormProvider>컴포넌트를 사용하면 useForm의 컨텍스트를 사용할 수 있다!

나의 경우 상위 컴포넌트에서 주소값 이외에도 사용자에게 닉네임을 추가로 받아야한다.

그래서 useForm에 추가로 값을 저장하기 위해 <form/> 자체를 <FormProvider/>로 감싸주었다.

 

이후 위에서 말했던 정의해둔 methods를 풀어서 전달..!!

<FormProvider {...methods}> 

 

      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <label htmlFor="nickname" className="flex flex-col gap-2 mt-6">
            닉네임
            <input
              type="text"
              id="nickname"
              placeholder="2~12글자"
              {...methods.register("nickname", {
                required: "값이 입력되지 않았습니다.",
                minLength: {
                  value: 2,
                  message: "닉네임은 최소 2글자 이상이어야 합니다.",
                },
                maxLength: {
                  value: 12,
                  message: "닉네임은 최대 12글자 이하이어야 합니다.",
                },
              })}
              className="border rounded-lg h-10 px-2"
            />
            <div>
              {methods.formState.errors.nickname && (
                <span className="text-warning-color text-xs">
                  {methods.formState.errors.nickname.message}
                </span>
              )}
            </div>
          </label>
          <AddressSearch getAddress={getAddress} />
          <button
            type="submit"
            className="flex flex-col gap-5 mt-12 text-center hover:shadow-lg h-10 justify-center items-center border rounded-lg w-full"
          >
            회원가입하기
          </button>
        </form>
      </FormProvider>

 

 

상위 컴포넌트에서 매서드 사용하기

상위 컴포넌트에서 매서드를 사용할 때에는 아래와 같이 사용해주어야 한다.

          <label htmlFor="nickname" className="flex flex-col gap-2 mt-6">
            닉네임
            <input
              type="text"
              id="nickname"
              placeholder="2~12글자"
              {...methods.register("nickname", {
                required: "값이 입력되지 않았습니다.",
                minLength: {
                  value: 2,
                  message: "닉네임은 최소 2글자 이상이어야 합니다.",
                },
                maxLength: {
                  value: 12,
                  message: "닉네임은 최대 12글자 이하이어야 합니다.",
                },
              })}
              className="border rounded-lg h-10 px-2"
            />
            <div>
              {methods.formState.errors.nickname && (
                <span className="text-warning-color text-xs">
                  {methods.formState.errors.nickname.message}
                </span>
              )}
            </div>
          </label>

register와 errors가 다른 방식으로 접근하는 이유

register는 폼의 입력필드를 등록하고 해당 필드의 속성을 반환하는 함수이다. 

따라서 입력필드에 직접 전달이 되어야한다.

 

formState.errors는 폼 전체의 에러상태를 담고 있는 객체이다

따라서 이 객체는 폼의 상태를 확인하기 위해서 필요한 곳에서 접근할 수 있다.

 

< AddressSearch 전체코드 >

"use client";

import { useState } from "react";
import DaumPostcodeEmbed from "react-daum-postcode";
import { useFormContext } from "react-hook-form";

interface AddressFormType {
  address?: string;
}

interface AddressSearchProps {
  getAddress: (address: string) => void;
}

export default function AddressSearch({ getAddress }: AddressSearchProps) {
  const {
    register,
    setValue,
    formState: { errors },
  } = useFormContext<AddressFormType>();

  const [isOpen, setIsOpen] = useState<boolean>(false);

  //react-daum-postcode에서 제공해주는 코드 *이대로 복사해서 어디든 사용가능..!*
  const handleComplete = (data: any) => {
    let fullAddress = data.address;
    let extraAddress = "";

    if (data.addressType === "R") {
      if (data.bname !== "") {
        extraAddress += data.bname;
      }
      if (data.buildingName !== "") {
        extraAddress +=
          extraAddress !== "" ? `, ${data.buildingName}` : data.buildingName;
      }
      fullAddress += extraAddress !== "" ? ` (${extraAddress})` : "";
    }

    setValue("address", fullAddress, { shouldValidate: true });
    getAddress(fullAddress);
    setIsOpen(false);
  };

  const handleToggle = () => {
    setIsOpen(!isOpen);
  };

  return (
    <>
      <div>
        <label htmlFor="address" className="flex flex-col gap-2 mt-6">
          주소
        </label>
        <div className="flex flex-row gap-2 mt-2">
          <input
            readOnly
            placeholder="주소를 입력해주세요"
            {...register("address", { required: "값이 입력되지 않았습니다." })}
            type="text"
            name="address"
            id="address"
            className="border rounded-lg h-10 px-2 w-full"
          />
          <button
            type="button"
            onClick={() => setIsOpen((value) => !value)}
            className="bg-main-color hover-color py-1 w-28 px-2 rounded-lg text-white"
          >
            주소 검색
          </button>
        </div>
        {errors.address && errors.address.type === "required" && (
          <span className="text-warning-color text-xs">
            {errors.address.message}
          </span>
        )}
      </div>
      {isOpen && (
        <div className="postcode-embed-fullscreen">
          <div className="postcode-embed-container">
            <DaumPostcodeEmbed
              onComplete={handleComplete}
              style={{ height: "91%" }}
            />
            <button
              type="button"
              onClick={handleToggle}
              className="bg-main-color text-center flex justify-center items-center text-white px-4 py-2 rounded-lg h-8 sm:h-10 mt-2 w-full"
            >
              닫기
            </button>
          </div>
        </div>
      )}
    </>
  );
}

 

< SignIn 전체코드 >

"use client";

import { signinUserApi } from "@/app/api/users";
import AddressSearch from "@/components/AddressSearch";
import { failToSiginIn, successToSiginIn } from "@/components/Common/Toastify";
import { useLoginStore } from "@/store/users/userStore";
import { SignInFormType } from "@/types/users/users";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";

export default function SignInPage() {
  const router = useRouter();
  const { socialId, socialType } = useLoginStore();
  const [address, setAddress] = useState<string>("");

  const getAddress = (selectedAddress: string) => {
    setAddress(selectedAddress);
    methods.setValue("address", selectedAddress, { shouldValidate: true });
  };

  const methods = useForm<SignInFormType>();

  const onSubmit = async (data: SignInFormType) => {
    const requestData = {
      nickName: data.nickname,
      address: data.address,
      socialId: String(socialId),
      socialType,
    };

    try {
      const response = await signinUserApi(requestData);
      if (response?.status === 201) {
        successToSiginIn();
        router.push("/users/login");
      } else {
        failToSiginIn();
      }
    } catch (error) {
      failToSiginIn();
      console.error("fail to siginin:", error);
    }
  };

  //nickName, address
  return (
    <div className="max-w-xl mx-auto pt-10 pb-10">
      <div className="flex flex-col gap-6">
        <h1 className="text-lg font-semibold text-center">회원가입</h1>
        <hr className="border-b-border-color" />
        <div className="text-xl md:text-2xl font-semibold">
          ZooMingle에 처음 오셨나요?
        </div>
      </div>
      <div className="text-xs md:text-sm text-gray-500 mt-2">
        간단한 정보를 입력하고 ZooMingle을 이용해보세요.
      </div>
      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <label htmlFor="nickname" className="flex flex-col gap-2 mt-6">
            닉네임
            <input
              type="text"
              id="nickname"
              placeholder="2~12글자"
              {...methods.register("nickname", {
                required: "값이 입력되지 않았습니다.",
                minLength: {
                  value: 2,
                  message: "닉네임은 최소 2글자 이상이어야 합니다.",
                },
                maxLength: {
                  value: 12,
                  message: "닉네임은 최대 12글자 이하이어야 합니다.",
                },
              })}
              className="border rounded-lg h-10 px-2"
            />
            <div>
              {methods.formState.errors.nickname && (
                <span className="text-warning-color text-xs">
                  {methods.formState.errors.nickname.message}
                </span>
              )}
            </div>
          </label>
          <AddressSearch getAddress={getAddress} />
          <button
            type="submit"
            className="flex flex-col gap-5 mt-12 text-center hover:shadow-lg h-10 justify-center items-center border rounded-lg w-full"
          >
            회원가입하기
          </button>
        </form>
      </FormProvider>
    </div>
  );
}