
react-hook-form과 zod 라이브러리를 사용하여 간편하게 폼을 구현할 수 있었다.
zod를 통해 스키마를 정의하면 데이터의 형태와 유효성 검사 규칙을 명확히 정의할 수 있다고 한다.(Schema-based Validation)
먼저 폼 구현에 필요한 모듈들을 import 해준다.
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
그리고 zod 라이브러리를 통해 회원가입 폼의 유효성 검사를 위한 폼 스키마를 선언해준다.
const passwordRegx = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*?()_+~-]).{8,15}$/;
const easyStringRegex =
/^(?!.*(123|abc|admin|password|qwerty|letmein|welcome|monkey|12345|123456|654321|11111|123123|admin123|password123|test|pass|login|letme|welcome1|admin1234|admin@123))/;
export const signUpFormSchema = z
.object({
type: z.string(),
password: z
.string()
.regex(passwordRegx, "영문 대소문자 / 숫자 / 특수문자 중 3가지 이상 조합 (8자~16자)으로 입력해주세요.")
.regex(easyStringRegex, "일련번호, 잘 알려진 단어, 키보드 상 나란히 있는 문자를 제외해주세요."),
confirmPassword: z.string(),
name: z
.string()
.min(2, {
message: "이름을 입력해주세요.",
})
.max(16, {
message: "이름을 16자 이하로 입력해주세요.",
}),
email: z.string().email({
message: "올바른 이메일 형식이 아닙니다.",
}),
})
.superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "비밀번호가 다릅니다.",
path: ["confirmPassword"],
});
}
});
다음으로 useForm 훅을 사용해 폼을 선언하고 초기화한다.
resolver 옵션에 zodResolver를 통해 앞서 선언했던 formSchema를 전달하여 zod 스키마를 사용해 폼의 유효성을 검사할 수 있도록 한다. 이렇게 하면 폼의 입력값이 zod 스키마와 일치하는지를 검사하여 유효성을 확인할 수 있다.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
type: "",
id: "",
password: "",
confirmPassword: "",
name: "",
email: "",
},
});
이제 react-hook-form에서 submit 이벤트가 발생했을 때 호출되는 onSubmit 함수를 작성해준다.
전달받은 값들의 타입은 'z.infer typeof formSchema'로, zod 스키마를 통해 유효성이 검사된 폼 데이터이다.
user 라는 변수를 생성하여 폼으로 입력받은 데이터들을 저장하고, signUp 이라는 함수에 데이터를 넘겨준다.
async function onSubmit(values: z.infer<typeof formSchema>) {
const user: UserSignUpType = {
type: values.type,
id: values.id,
password: values.password,
confirmPassword: values.confirmPassword,
name: values.name,
email: values.email,
};
signUp(user);
}
렌더링 되는 컴포넌트를 보면 값들을 입력받는 input 창들과, '회원가입' 버튼을 클릭하여 onSubmit 함수를 실행시킨다는 것을 알 수 있다.
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem {...field}>
<FormLabel>회원 구분 *</FormLabel>
<FormControl>
<RadioGroup className="flex" defaultValue={"일반 회원"} onValueChange={field.onChange}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="일반 회원" defaultChecked={true} />
<Label htmlFor="일반 회원">일반 회원</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="관리자" />
<Label htmlFor="관리자">관리자</Label>
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem>
<FormLabel>아이디 *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage>영문 소문자 / 숫자 (4자~16자)</FormMessage>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>비밀번호 *</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage>영문 대소문자 / 숫자 / 특수문자 중 3가지 이상 조합 (8자~16자)</FormMessage>
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>비밀번호 확인 *</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>이름 *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>이메일 *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">회원가입</Button>
</form>
</Form>
);
그렇다면 signUp 함수에서는 실제로 Firebase Authentication을 사용해 유저를 회원가입 시키는 로직을 수행해야할 것이다.
처음에 Firebase Authentication만을 이용해서 회원가입 로직을 완성하려 했으나, 이렇게 회원가입을 완료했을 때 받아와지는 값들을 살펴보니 내 프로젝트 구현에 필요했던 유저 타입(관리자/판매자) 등은 따로 저장할 수 있는 곳이 없다는 것을 알았다.

그러면 기본 Firebase Authentication에서 제공하지 않는 유저에 관한 정보들은 어디에 저장해야 할까 생각해봤고 → Firestore Database에 유저 정보에 대한 항목을 따로 만들어서 저장하면 되겠다고 생각했다.
그렇게 해서 완성한 로직은 다음과 같다.
createUserWithEmailAndPassword를 사용해서 유저를 등록한 뒤, 따로'users'라는 컬렉션을 생성하여 setDoc 함수를 사용해 사용자의 정보를 별도로 저장하는 작업을 하도록 했다.
관련된 자세한 내용은 Firebase 공식 문서에서 확인할 수 있다.
export async function signUp(user: UserSignUpType) {
try {
const userCredential = await createUserWithEmailAndPassword(firebaseAuth, user.email, user.password!);
try {
const updated = await updateProfile(userCredential.user, { displayName: user.name });
try {
const uid = userCredential.user.uid;
const userRef = doc(db, "users", uid); // 파이어베이스 자동생성 유저 고유 아이디
await setDoc(userRef, {
type: user.type,
id: user.id,
email: user.email,
name: user.name,
});
} catch (e) {
console.error(e);
}
alert("회원가입 되었습니다.");
} catch (e) {
console.error(e);
}
} catch (e) {
console.error(e);
}
}
이렇게 저장됩니다!
Firebase Authentication에 사용자 등록

Firestore에 사용자 정보 등록

(이후에 캡쳐한 사진이라 다른 컬렉션이 추가되어 있음. 이번 단계에서는 users만 생성되어 있는게 맞아요.)