NodeJS
풀스택 틱톡 클론 따라하기 - 4. AuthModal - Register
liufeier
2023. 11. 20. 01:45
반응형
1:28:07 ~ 1:49:13
AuthModal - Register
// src/components/Login.tsx
import React from "react";
function Login() {
return <div>Login</div>;
}
export default Login;
// // src/components/Register.tsx
import React from "react";
function Register() {
return <div>Register</div>;
}
export default Register;
npm i react-icons
// src/components/AuthModal.tsx
import { useState } from "react";
import { useGeneralStore } from "../stores/generalStore";
import { ImCross } from "react-icons/im";
import Login from "./Login";
import Register from "./Register";
function AuthModal() {
// 사용자가 등록 또는 로그인 양식 중 어느 것을 보고 있는지를 관리하는 지역 상태
const [isRegistered, setIsRegistered] = useState(false);
// 전역 상태에서 상태 및 상태 수정 함수에 액세스
const setIsLoginOpen = useGeneralStore((state) => state.setLoginIsOpen);
const isLoginOpen = useGeneralStore((state) => state.isLoginOpen);
return (
// 화면 전체를 덮는 모달 오버레이
<div
id="AuthModal"
className="fixed flex items-center justify-center z-50 top-0 left-0 w-full h-full bg-black bg-opacity-50"
>
{/* 모달 내용 */}
<div className="relative bg-white w-full max-w-[470px] h-[70%] p-4 rounded-lg">
{/* 우측 상단에 위치한 닫기 버튼 */}
<div className="w-full flex justify-end">
<button
onClick={() => setIsLoginOpen(!isLoginOpen)}
className="p-1.5 rounded-full bg-gray-100"
>
<ImCross color="#000000" size="26" />
</button>
</div>
{/* 로그인 또는 등록 양식을 지역 상태에 따라 렌더링 */}
{isRegistered ? <Login /> : <Register />}
{/* 로그인 및 등록 양식 간 전환 */}
<div className="absolute flex items-center justify-center py-5 left-0 bottom-0 border-t w-full">
<span className="text-[14px] text-gray-600">
Don't have an account?
</span>
<button
onClick={() => setIsRegistered(!isRegistered)}
className="text-[14px] text-[#F02C56] font-semibold pl-1"
>
{isRegistered ? <span>Sign up</span> : <span>Log in </span>}
</button>
</div>
</div>
</div>
);
}
export default AuthModal;
isRegistered: 사용자가 등록 또는 로그인 양식 중 어느 것을 보고 있는지를 나타내는 지역 상태이다.
setIsLoginOpen: 로그인 모달의 가시성을 전환하는 전역 상태의 함수이다.
isLoginOpen: 로그인 모달이 열려 있는지 닫혀 있는지를 나타내는 전역 상태이다.
컴포넌트는 화면 전체를 덮는 모달 오버레이를 렌더링하며, 지역 상태에 따라 로그인 또는 등록을 위한 양식이 포함된 내용을 표시한다.
모달에는 닫기 버튼과 로그인 및 등록 양식 간 전환을 위한 요소도 포함되어 있다.
// src/components/Input.tsx
import React from "react";
import { useEffect } from "react";
/**
* Input 컴포넌트는 입력 필드를 렌더링하고, 포커스 및 에러 처리를 담당합니다.
* @param {string} placeHolder - 입력 필드에 나타날 플레이스홀더 텍스트.
* @param {string} inputType - 입력 필드의 타입 (예: "text", "password" 등).
* @param {number} max - 입력 값의 최대 길이.
* @param {string} error - 입력 필드와 연관된 오류 메시지.
* @param {boolean} autoFocus - 페이지 로드 시 자동으로 입력 필드에 포커스 여부.
* @param {function} onChange - 입력 필드 값이 변경될 때 호출되는 함수.
* @param {string} value - 입력 필드의 현재 값.
*/
function Input({
placeHolder,
inputType,
max,
error,
autoFocus,
onChange,
value,
}: {
placeHolder: string;
inputType: string;
max: number;
error: string;
autoFocus: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value?: string;
}) {
// autoFocus가 true인 경우, 페이지 로드 시 입력 필드에 포커스를 줌
useEffect(() => {
if (autoFocus) {
const input = document.getElementById(`input-${placeHolder}`);
input?.focus();
}
}, [autoFocus, placeHolder]);
return (
<div>
{/* 입력 필드 */}
<input
value={value}
id={`input-${placeHolder}`}
placeholder={placeHolder}
type={inputType}
autoComplete="off"
maxLength={max}
onChange={onChange}
className="block w-full bg-[#F1F1F2] text-gray-800 border border-gray-300 rounded-md py-2.5 px-3 focus:outline-none"
/>
{/* 오류 메시지 표시 */}
{error && (
<span className="text-red-500 text-[14px] font-semibold">{error}</span>
)}
</div>
);
}
export default Input;
// // src/components/Register.tsx
import { useMutation } from "@apollo/client";
import { RegisterUserMutation } from "../gql/graphql";
import { REGISTER_USER } from "../graphql/mutations/register";
import { useUserStore } from "../stores/userStore";
import { useGeneralStore } from "../stores/generalStore";
import { useState } from "react";
import { GraphQLErrorExtensions } from "graphql";
import Input from "./Input";
function Register() {
// useMutation 훅으로 REGISTER_USER 뮤테이션을 호출하고 결과를 받아온다.
const [registerUser, { data, loading, error }] =
useMutation<RegisterUserMutation>(REGISTER_USER);
// useUserStore와 useGeneralStore 훅을 사용하여 전역 상태 및 액션을 가져온다.
const setUser = useUserStore((state) => state.setUser);
const setIsLoginOpen = useGeneralStore((state) => state.setLoginIsOpen);
// 서버에서 반환된 GraphQL 에러를 저장할 상태와 초기값을 설정한다.
const [errors, setErrors] = useState<GraphQLErrorExtensions>({});
// 회원가입 폼 데이터를 담을 상태와 초기값을 설정한다.
const [registerData, setRegisterData] = useState({
email: "",
password: "",
fullName: "",
confirmPassword: "",
});
// 회원가입 버튼 클릭 시 실행되는 함수
const handleRegister = async () => {
// 기존 에러를 초기화한다.
setErrors({});
// useMutation 훅으로 REGISTER_USER 뮤테이션을 호출한다.
// 호출 시, 회원가입 폼 데이터를 변수로 전달한다.
await registerUser({
variables: {
email: registerData.email,
password: registerData.password,
fullname: registerData.fullName,
confirmPassword: registerData.confirmPassword,
},
}).catch((err) => {
console.trace(err.graphQLErrors);
// GraphQL 에러가 발생하면 해당 에러를 상태에 저장한다.
setErrors(err.graphQLErrors[0].extensions);
});
// 회원가입이 성공하면 반환된 사용자 정보로 전역 상태를 업데이트한다.
if (data?.register.user) {
console.trace(data.register.user);
setUser({
id: String(data?.register.user.id),
email: data?.register.user.email,
fullname: data?.register.user.fullname,
});
}
// 로그인 모달을 닫는다.
setIsLoginOpen(false);
};
return (
<>
{/* 회원가입 폼 UI */}
<div className="text-center text-[28px] mb-4 font-bold">Sign up</div>
{/* Full name, Email, Password, Confirm Password에 대한 Input 컴포넌트들을 생성한다. */}
<div className="px-6 pb-2">
<Input
max={64}
placeHolder="Full name"
onChange={(e) =>
setRegisterData({ ...registerData, fullName: e.target.value })
}
inputType="text"
autoFocus={true}
error={errors?.fullname as string}
></Input>
</div>
<div className="px-6 pb-2">
<Input
max={64}
placeHolder="Email"
onChange={(e) =>
setRegisterData({ ...registerData, email: e.target.value })
}
inputType="email"
autoFocus={true}
error={errors?.email as string}
></Input>
</div>
<div className="px-6 pb-2">
<Input
max={64}
placeHolder="Password"
onChange={(e) =>
setRegisterData({ ...registerData, password: e.target.value })
}
inputType="password"
autoFocus={true}
error={errors?.password as string}
></Input>
</div>
<div className="px-6 pb-2">
<Input
max={64}
placeHolder="Confirm password"
onChange={(e) =>
setRegisterData({
...registerData,
confirmPassword: e.target.value,
})
}
inputType="password"
autoFocus={true}
error={errors?.confirmPassword as string}
></Input>
</div>
<div className="px-6 mt-6">
{/* 회원가입 버튼 */}
<button
onClick={handleRegister}
disabled={
!registerData.email ||
!registerData.password ||
!registerData.fullName ||
!registerData.confirmPassword
}
className={[
"w-full text-[17px] font-semibold text-white py-3 rounded-sm",
!registerData.email ||
!registerData.password ||
!registerData.fullName ||
!registerData.confirmPassword
? "bg-gray-200"
: "bg-[#F02C56]",
].join(" ")}
>
Register
</button>
</div>
</>
);
}
export default Register;
여기까지 하고 회원가입이 잘되는지 테스트해보았는데, Internal Server Error가 발생하였다.
prisma와 데이터베이스를 마이그레이트하지 않아 발생하는 에러였다.
# 백엔드에서
npx prisma migrate dev
npx prisma generate
가입 잘되고, user 테이블에 데이터 잘 생성되는걸 확인하였지만, 이메일 유효성 검사와 패스워드 8자 미만 입력 시 에러 메세지를 클라이언트의 ui에 표시해주는게 되지 않아 이를 작업해주어야한다.
반응형