프로젝트 작업일지 : 회원가입 (폼 만들기_React Hook Form)

작업일지에 들어가며

 

이전에 ERD를 어느정도는 구성했지만 테이블간 관계를 더 고려해봐야해서 데이터베이스가 없다.

 

그래서 DB와 프론트쪽으로 우선 업무를 간단히 분담하여 프로젝트를 진행하기로하였다.

 

나는 프론트 업무를 담당하게되어 이번 글에서는 리액트로 화면을 구성해보도록 하겠다.

 

우리는 이미 구현이 되어있는 템플릿 사용과 아니면 차크라UI를 사용하여 필요한 것들을 직접 만들어 사용할 지 논의했고,

 

템플릿을 사용하면 기존 코드를 해석하는 시간도 오래 걸리고 우리가 원하는 방향으로 수정하기 까다롭다고 판단하여

 

차크라 UI를 사용하여 화면을 만들기로 하였다.


Chakra UI 사용 방법

하기 차크라 UI 홈페이지에 들어가서 기본 세팅을 해주면 된다.

1. 라이브러리 설치

npm i @chakra-ui/react @emotion/react

 

2. 스니펫 추가

npx @chakra-ui/cli snippet add

 

3. 프로바이더 셋업

import { Provider } from "@/components/ui/provider"

function App({ Component, pageProps }) {
  return (
    <Provider>
      <Component {...pageProps} />
    </Provider>
  )
}

 

4. jsconfig.json

package.json과 같은 위치에 생성한다.

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "skipLibCheck": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
🚨 에러 : Module not found: Error: Can't resolve '@/components/ui/provider' 'C:\yyummmmmmmm\react\project\src'

모듈을 찾을 수 없다고 에러가 나왔다. jsconfig.json에 경로를 설정해줬는데도 해당 모듈을 찾지 못한다.
구글링과 GPT에 물어봐도 도저히 해결이 안되어서 components/ui 내부에 있는 모듈들은 그냥 상대 경로 설정을 해줬다.

import { Provider } from "@/components/ui/provider"
🔽
import { Provider } from "../components/ui/provider"

 

5. Components 사용

각 컴포넌트 별로 설명이 상세히 되어 있어 사용하기에는 편할 것 같다.

import { Button } from "@/components/ui/button"
import { HStack } from "@chakra-ui/react"

const Demo = () => {
  return (
    <HStack>
      <Button>Click me</Button>
      <Button>Click me</Button>
    </HStack>
  )
}

 


회원가입 폼 구현

1. 차크라 UI | Components | Input | Hook Form

import { Button, Input, Stack } from "@chakra-ui/react"
import { Field } from "@/components/ui/field"
import { useForm } from "react-hook-form"

interface FormValues {
  firstName: string
  lastName: string
}

const Demo = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>()

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

  return (
    <form onSubmit={onSubmit}>
      <Stack gap="4" align="flex-start" maxW="sm">
        <Field
          label="First name"
          invalid={!!errors.firstName}
          errorText={errors.firstName?.message}
        >
          <Input
            {...register("firstName", { required: "First name is required" })}
          />
        </Field>
        <Field
          label="Last name"
          invalid={!!errors.lastName}
          errorText={errors.lastName?.message}
        >
          <Input
            {...register("lastName", { required: "Last name is required" })}
          />
        </Field>
        <Button type="submit">Submit</Button>
      </Stack>
    </form>
  )
}
<Field
	label="First name"
	invalid={!!errors.firstName}
	errorText={errors.firstName?.message}
>
	<Input
		{...register("firstName", { required: "First name is required" })}
	/>
</Field>

해당 코드에 원하는 입력값들을 받으면 될 것 같다.

 

2. React Hook Form 라이브러리 설치

입력 폼을 만들기 전 라이브러리를 설치해준다.

npm install react-hook-form

 

3. 기본 제공 코드 수정

import { Button, Input, Stack } from "@chakra-ui/react"
import { Field } from "../components/ui/field"
import { useForm } from "react-hook-form"

const AuthRegister = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()

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

  return (
    <form onSubmit={onSubmit}>
      <Stack gap="4" align="flex-start" maxW="sm">
        <Field
          label="First name"
          invalid={!!errors.firstName}
          errorText={errors.firstName?.message}
        >
          <Input
            {...register("firstName", { required: "First name is required" })}
          />
        </Field>
        <Field
          label="Last name"
          invalid={!!errors.lastName}
          errorText={errors.lastName?.message}
        >
          <Input
            {...register("lastName", { required: "Last name is required" })}
          />
        </Field>
        <Button type="submit">Submit</Button>
      </Stack>
    </form>
  )
}

export default AuthRegister;
import { Field } from "../components/ui/field" : 경로를 찾지 못하기 때문에 상대 경로로 수정

interface FormValues { firstName: string lastName: string } : 타입스크립트 인터페이스 삭제

const { register, handleSubmit, formState: { errors }, } = useForm() : 타입스크립트 <FormValues> 삭제

 

4. 이메일 필드 구현

<Box>
	<Field
		label="이메일"
		invalid={!!errors.email}
		errorText={errors.email?.message}
>
	<Input
		placeholder="이메일 형식에 맞춰 입력해주세요."
		{...register("email", {
			required: "이메일은 필수 입력입니다.",
			pattern: {
				value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
				message: "올바른 이메일 형식으로 입력해주세요.",
			},
		})}
	/>
	</Field>
</Box>
<Box>
	<Field label="&nbsp;" invalid={!!errors.email} errorText="&nbsp;">
		<DuplicatedEmail />
	</Field>
</Box>

다른 입력 필드들도 위 처럼 작성해주면 된다. 정규표현식을 사용해서 유효성 검증도 가능하다.

이메일 중복 검사 버튼을 따로 구현할 것이기에 <DuplicatedEmail /> 컴포넌트도 하나 생성해주었다.

 

5. 비밀번호 필드 구현 | PasswordInput

<Field
	label="비밀번호"
	invalid={!!errors.password}
	errorText={errors.password?.message}
>
 	<PasswordInput
		placeholder="8자 이상 20자 이하로 입력해주세요."
		{...register("password", {
			required: "비밀번호는 필수 입력입니다.",
			pattern: {
				value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\d!@#$%^&*(),.?":{}|<>]{8,20}$/,
				message: "비밀번호는 최소 8자 이상 최대 20자 이하, 숫자, 특수문자, 영문자가 포함되어야 합니다.",
			},
		})}
	/>
</Field>
<Field
	label="비밀번호 확인"
	invalid={!!errors.passwordConfirm}
	errorText={errors.passwordConfirm?.message}
>
	<PasswordInput
		placeholder="비밀번호 확인"
			{...register("passwordConfirm", {
                required: "비밀번호 확인은 필수 입력입니다.",
			})}
	/>
</Field>

차크라 UI에 비밀번호 보이는 기능도 포함되어 있어 상당히 좋아보이는 비밀번호 전용 컴포넌트가 보이길래 비밀번호는 해당 컴포넌트로 교체했다. 비밀번호 확인 기능은 추후에 추가해야겠다.

 

6. 주소 필드 구현

<Box>
	<Field
		label="주소"
		invalid={!!errors.postcode}
		errorText={errors.postcode?.message}
	>
	<Input
		placeholder="우편번호"
		readOnly
		{...register("postcode", {
			required: "우편번호는 필수 입력입니다.",
		})}
	/>
	</Field>
</Box>
<Box>
	<Field
		label="&nbsp;"
		invalid={!!errors.postcode}
		errorText="&nbsp;"
	>
	<DaumPostAPI />
	</Field>
</Box>
	<Field
		invalid={!!errors.roadAddress}
		errorText={errors.roadAddress?.message}
	>
	<Input
		placeholder="도로명 주소"
		readOnly
		{...register("roadAddress", {
			required: "도로명 주소는 필수 입력입니다.",
		})}
	/>
	</Field>
	<Field
		invalid={!!errors.detailAddress}
		errorText={errors.detailAddress?.message}
	>
	<Input
		placeholder="상세 주소"
		{...register("detailAddress", {
			required: "상세 주소는 필수 입력입니다.",
		})}
	/>
</Field>

<DaumPostAPI /> : 사용자 주소는 다음 주소 API를 사용할 예정이므로 미리 컴포넌트를 만들어 넣어두었다.

 

7. 약관 동의 체크박스 구현

<Controller
	name="terms"
	control={control}
	rules={{ required: "약관에 동의해야 합니다." }}
	render={({ field }) => (
<Checkbox {...field}>
회원가입 시&nbsp;
</Checkbox>
	)}
/>
<Box display="flex" alignItems="center" gap="1" mr="2">
	<Terms1 />
	<Text fontSize="sm">과</Text>
	<Terms2 />
	<Text fontSize="sm">에 동의하게 됩니다.</Text>
</Box>
{errors.terms && (
	<Box color="red.500" fontSize="sm" mt="2">
	{errors.terms.message}
</Box>)}

회원 약관 동의를 받을 체크박스를 생성하였고 약관 내용인 <Terms1>, <Terms2>는 각각 모달로 만들어 구현 예정

 

8. 회원가입 핸들러 작성

const handleRegister = async (data) => {
  
    try {
      const response = await axios.post(
        "http://localhost:8080/auth/register",
        data
      );

      if (response.status === 200) {
        alert("회원가입 완료"); // 작동 확인용
      }
    } catch (error) {
      if (error.response) {
        const { code } = error.response.data;
        if (code === "DP") {
          setError("phone", {
            type: "manual",
            message: "이미 사용 중인 번호 입니다.",
          });
        } else if (code === "DM") {
          setError("email", {
            type: "manual",
            message: "이미 사용 중인 이메일 입니다.",
          });
        } else if (error.response.status === 500) {
          setError("submit", {
            type: "manual",
            message: "각 항목의 형식에 맞게 입력해주세요.",
          });
        }
      } else {
        setError("submit", {
          type: "manual",
          message: "서버와 연결할 수 없습니다. 잠시 후 다시 시도해주세요.",
        });
      }
    }
  };

 

이전 프로젝트에서 사용했던 서버단 코드를 그대로 사용할 예정이라 setError의 경우 각 응답 코드 별로 메시지를 다르게 설정해주었다. 

const onSubmit = handleSubmit((data) => {
	handleRegister(data);
});

 

React Hook Form은 자체적으로 폼 제출 핸들러인 handleSubmit 함수가 있기 때문에 submit시 작동되도록 해당 코드에 handleRegister 함수를 넣어주면 된다.

 

9. 완성

이런 식으로 작성하였고 세부 디자인의 경우 GPT의 도움을 받아 무리없이 진행할 수 있었다.

const AuthRegister = () => {

  const {
    register,
    handleSubmit,
    setError,
    formState: { errors },
    control,
  } = useForm();

  const handleRegister = async (data) => {
  
    try {
      const response = await axios.post(
        "http://localhost:8080/auth/register",
        data
      );

      if (response.status === 200) {
        alert("회원가입 완료");
      }
    } catch (error) {
      if (error.response) {
        const { code } = error.response.data;
        if (code === "DP") {
          setError("phone", {
            type: "manual",
            message: "이미 사용 중인 번호 입니다.",
          });
        } else if (code === "DM") {
          setError("email", {
            type: "manual",
            message: "이미 사용 중인 이메일 입니다.",
          });
        } else if (error.response.status === 500) {
          setError("submit", {
            type: "manual",
            message: "각 항목의 형식에 맞게 입력해주세요.",
          });
        }
      } else {
        setError("submit", {
          type: "manual",
          message: "서버와 연결할 수 없습니다. 잠시 후 다시 시도해주세요.",
        });
      }
    }
  };

  const onSubmit = handleSubmit((data) => {
    handleRegister(data);
  });

  return (
    <form onSubmit={onSubmit}>
      <Box
        display="flex"
        alignItems="center"
        justifyContent="center"
        height="100vh"
        width="full"
      >
        <Stack gap="1" align="flex-start" maxW="lg" width="full">
          <Box
            display="flex"
            alignItems="center"
            justifyContent="space-between"
            width="full"
          >
            <Text fontSize="2xl" fontWeight="bold">
              회원가입
            </Text>
            <Link href="#" color="orange.500" fontWeight="bold">
              기존 계정으로 로그인
            </Link>
          </Box>
          <Box
            display="flex"
            alignItems="center"
            justifyContent="space-between"
            width="full"
          >
            <Box flex="7" mr="4">
              <Field
                label="이메일"
                invalid={!!errors.email}
                errorText={errors.email?.message}
              >
                <Input
                  size="md"
                  placeholder="이메일 형식에 맞춰 입력해주세요."
                  width="100%"
                  {...register("email", {
                    required: "이메일은 필수 입력입니다.",
                    pattern: {
                      value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
                      message: "올바른 이메일 형식으로 입력해주세요.",
                    },
                  })}
                />
              </Field>
            </Box>
            <Box flex="3">
              <Field label="&nbsp;" invalid={!!errors.email} errorText="&nbsp;">
                <DuplicatedEmail />
              </Field>
            </Box>
          </Box>
          <Field
            label="비밀번호"
            invalid={!!errors.password}
            errorText={errors.password?.message}
          >
            <PasswordInput
              size="md"
              placeholder="8자 이상 20자 이하로 입력해주세요."
              width="100%"
              {...register("password", {
                required: "비밀번호는 필수 입력입니다.",
                pattern: {
                  value:
                    /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\d!@#$%^&*(),.?":{}|<>]{8,20}$/,
                  message:
                    "비밀번호는 최소 8자 이상 최대 20자 이하, 숫자, 특수문자, 영문자가 포함되어야 합니다.",
                },
              })}
            />
          </Field>
          <Field
            label="비밀번호 확인"
            invalid={!!errors.passwordConfirm}
            errorText={errors.passwordConfirm?.message}
          >
            <PasswordInput
              size="md"
              placeholder="비밀번호 확인"
              width="100%"
              {...register("passwordConfirm", {
                required: "비밀번호 확인은 필수 입력입니다.",
              })}
            />
          </Field>
          <Field
            label="이름"
            invalid={!!errors.name}
            errorText={errors.name?.message}
          >
            <Input
              size="md"
              placeholder="한글과 영어만 입력해주세요."
              width="100%"
              {...register("name", {
                required: "이름은 필수 입력입니다.",
                pattern: {
                  value: /^[a-zA-Z가-힣]{2,20}$/,
                  message: "이름은 한글과 영어만 입력해야 합니다.",
                },
              })}
            />
          </Field>
          <Field
            label="휴대전화"
            invalid={!!errors.phone}
            errorText={errors.phone?.message}
          >
            <Input
              size="md"
              placeholder="'-'를 제외한 숫자 11자리를 입력해주세요."
              width="100%"
              {...register("phone", {
                required: "휴대전화는 필수 입력입니다.",
                pattern: {
                  value: /^[0-9]{11}$/,
                  message: "'-'를 제외한 숫자 11자리를 입력해주세요.",
                },
              })}
            />
          </Field>
          <Field
            label="생년월일"
            invalid={!!errors.birthDate}
            errorText={errors.birthDate?.message}
          >
            <Input
              size="md"
              placeholder="YYYYMMDD 형식으로 입력해주세요."
              width="100%"
              {...register("birthDate", {
                required: "생년월일은 필수 입력입니다.",
                pattern: {
                  value: /^\d{8}$/,
                  message: "생년월일은 YYYYMMDD 형식이어야 합니다.",
                },
              })}
            />
          </Field>
          <Box
            display="flex"
            alignItems="center"
            justifyContent="space-between"
            width="full"
          >
            <Box flex="7" mr="4">
              <Field
                label="주소"
                invalid={!!errors.postcode}
                errorText={errors.postcode?.message}
              >
                <Input
                  size="md"
                  placeholder="우편번호"
                  readOnly
                  width="100%"
                  {...register("postcode", {
                    required: "우편번호는 필수 입력입니다.",
                  })}
                />
              </Field>
            </Box>
            <Box flex="3">
              <Field
                label="&nbsp;"
                invalid={!!errors.postcode}
                errorText="&nbsp;"
              >
                <DaumPostAPI />
              </Field>
            </Box>
          </Box>
          <Field
            invalid={!!errors.roadAddress}
            errorText={errors.roadAddress?.message}
          >
            <Input
              size="md"
              placeholder="도로명 주소"
              readOnly
              width="100%"
              {...register("roadAddress", {
                required: "도로명 주소는 필수 입력입니다.",
              })}
            />
          </Field>
          <Field
            invalid={!!errors.detailAddress}
            errorText={errors.detailAddress?.message}
          >
            <Input
              id="detailAddress"
              size="md"
              placeholder="상세 주소"
              width="100%"
              {...register("detailAddress", {
                required: "상세 주소는 필수 입력입니다.",
              })}
            />
          </Field>
          <Box
            display="flex"
            alignItems="center"
            width="full"
            flexWrap="nowrap"
            whiteSpace="nowrap"
            fontSize="sm"
          >
            <Controller
              name="terms"
              control={control}
              rules={{ required: "약관에 동의해야 합니다." }}
              render={({ field }) => (
                <Checkbox
                  {...field}
                  colorPalette="orange"
                  fontSize="xs"
                  size="sm"
                >
                  회원가입 시&nbsp;
                </Checkbox>
              )}
            />
            <Box display="flex" alignItems="center" gap="1" mr="2">
              <Terms1 />
              <Text fontSize="sm">과</Text>
              <Terms2 />
              <Text fontSize="sm">에 동의하게 됩니다.</Text>
            </Box>
          </Box>
          {errors.terms && (
            <Box color="red.500" fontSize="sm" mt="2">
              {errors.terms.message}
            </Box>
          )}
          {errors.submit && (
            <Box color="red.500" fontSize="sm" mt="2">
              {errors.submit.message}
            </Box>
          )}
          <Stack spacing="4" width="full" mt="4">
            <Button type="submit" colorPalette="orange" width="full" size="lg">
              회원가입
            </Button>
          </Stack>
        </Stack>
      </Box>
    </form>
  );
};

export default AuthRegister;


작업일지를 마치며

✨ 나의 생각

차크라 UI를 사용해서 만들어서 페이지를 만드는 데 아주 어려움은 없었지만 
이번 작업을 하면서, 아! 프론트 디자인 적으로는 재능이 없구나를 느꼈다. GPT의 도움이 절실 했다.
화면을 더 이쁘게 만들고 싶었지만 현재의 능력으로는 이 정도 만드는 게 최선인 것 같다.
백엔드 개발자를 희망하고는 있지만 개발자는 백, 프론트 구분 없이 모두 다 잘해야한다고 생각하기 때문에
앞으로 공부를 더 해야겠다고 느꼈다. 다음 글은 아마 다음 주소 API 구현이 되지 않을까 싶다.

Reference


🙏 Chakra UI

🙏 Installation | Chakra UI

🙏 Components | Chakra UI