UMC 8th Web 워크북

🍎 로그인 구현해보기 (umc 4주차)

minnote29 2025. 4. 13. 18:07

🍎

스웨거를 일단 켜놓는다. 로그인 같은 경우는 email, password를 입력을 해줘야 한다.아마도 아래 사진처럼 작성을 했을 것이다. 하지만, 로그인 사이트가 대부분 이렇게 허접하지는 않을 것이다. -> 백엔드와의 유효성 검사를 맞춰서 유효성 검증 부분도 해주는 것이 좋다. 

훅을 사용해서 로그인 관련 유효성 검증을 할 것이다. 

 

 

🍎

일단은 구조를 먼저 잡는다. 

그 다음, input 태그를 쓸 건데 타입은 email, 스타일로는 border를 준다. width도 강제로 픽셀을 주고, 패딩도 10픽셀 정도 준다. 포커스가 됐을 때, border를 다른 색이 되도록 하고, 약간 둥글게 처리한다. 비밀번호도 똑같이.

그리고 버튼도 만들어주는데 handleSubmit 함수를 API 요청 시 작동되도록 처리를 할 것이다. 세부적인 스타일 설명을 생략하도록 한다. 

const Loginpage = () => {
    const handleSubmit = () => {

    };

    return (
        <div className="flex flex-col items-center justify-center h-full gap-4">
            <div className="flex flex-col gap-3">
                <input className={'border border-[#333} w-[300px] p-[10px] focus:border-[#807bff] rounded-sm'}
                    type={'email'}
                    placeholder={"이메일"}  
                />
                <input className={'border border-[#333} w-[300px] p-[10px] focus:border-[#807bff] rounded-sm'}
                    type={'password'}
                    placeholder={"비밀번호"}  
                />
                <button type="button"
                    onClick={handleSubmit} 
                    disabled={false} 
                    className="w-full bg-blue-600 text-white py-3 rounded-md text-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer disabled:bg-gray-300"
                >
                    로그인
                </button>
            </div>
        </div>
    )
}

export default Loginpage;

그 다음 할게 뭐냐면 이메일의 유효성 검증 관련 해서 이메일 규격이 안 맞을 경우 에러를 보여 주고 싶을 텐데, 이를 위해서 useForm이라는 훅을 만들 것이다. hooks 폴더 안에 useForm이라는 훅을 만들어서 관리할 건데, useForm은 input 값들처럼 여러 데이터가 입력되는 것을 한번에 관리하는 것을 보통 폼이라고 하는데 이 폼 데이터를 관리하는 훅을 useForm.ts로 만든다. 

 

 

🍎

일단, 먼저 함수의 인터페이스(함수의 규격)를 정한다.

interface UseFormProps<T> {
    //
    initailValue: T; // { email: '', password: '' }
    // 올바른지 체크
    validate: (values: T) => Record<keyof T, string>; // T에 대한 키 값을 받고, 그것에 대한 value가 string이다라고 이해하면 된다. 
}

그 다음 function useForm을 만든다. 그리고 이 함수는 제네릭을 받을 것이다. props로 제네릭을 넘겨준다고 규격을 정했긴 때문이다. 

그리고 함수를 입력해주면 되는데, 아무 값도 입력을 안 했는데 페이지를 들어가자마자 값이 없다고 바로 에러 메시지가 띄워지면 이상할 것이다. 그래서 내가 이 요소들을 눌렀는지 안 눌렀는지를 관리하는 touched라는 것을 관리를 할 것이다.

const [touched, setTouched] = useState<Record<string, boolean>>()

이렇게 한 이유는 false 일때 touched가 안 되고, true일 때는 touchde가 되도록 처리를 한 것이다. 

const [error, setErrors] = useState<Record<string, string>>();

 이 같은 경우는 만약에 해당하는 것이 아니면 string 메시지를 띄운다고 생각하면 되겠다. 

 

그 다음, 객체를 업데이트 해주는 것을 처리해줘야 한다.

아래 같은 경우는 로그인이면 로그인과 관련된 key, 회원가입이면 회원가입과 관련된 key를 처리하기 위해서 지정해줬다고 보면 된다. 

name: keyof T

기존 입력값을 유지시켜서 값을 바꿨다고 다른 것이 바뀌면 안 되서 그 처리를 해주고(이메일을 바꿨는데 패스워드가 바뀌면 안 되니까 불변성 유지를 해줘야 함.)

// 사용자가 입력값을 바꿀 때 실행되는 함수다.
    const handleChange = (name: keyof T, text: string) => {
        setValues({
            ...values, // 불변성 유지(기존 값 유지) // setValues는 기존 값 유지하고~~
            [name]: text, // name을 찾아서 텍스트를 넣어주면 된다 라고 생각하면 된다.
        });
    };

 

이 값들이 에러에 걸릴 때, 계속 변경이 돼야 한다. 그래서 구현한 코드를 리턴을 해줄 것이다. 값들을 쓰기 위해서. 이 ㄱ밧들을 어떻게 쓰냐면 return { value, onChange, onBlur }; // 이렇게 써주면 다른 곳에서 굳이 불러서 사용할 필요가 없다. 

// 이메일, 인풋, 패스워드 인풋, 속성들을 좀 가져오는 것
    const getInputProps = (name: keyof T) => {
        const value = values[name]; // 값을 받기
        const onChange = ( // 이 함수를 통해서 우리가 e.target.value를 가져와준다고 생각하면 된다.
            e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
        ) => handleChange(name, e.target.value);  

        // 바뀔 때, 블러 처리
        const onBlur = () => handleBlur(name);

        return { value, onChange, onBlur };
    };

여기서 추가로 처리해 줘야 될 것이 values 변경될 때마다 에러 검증 로직이 실행되게 해야 한다. -> 뭔가 변경될 때마다 로직이 실행된다면 useEffect. useEffect를 통해서 에러처리를 해줄 것이다.

// values가 변경될 때마다 에러 검증 로직이 실행됨.
    // { email: ""}
    useEffect(() => {
        const newErrors = validate(values);
        setErrors(newErrors); // 오류 메시지 업뎃
    }, [validate, values]); // validate, values가 바뀔 때마다 useEffect를 계속 실행되도록 할 것이다. 

    return { values, errors, touched, getInputProps };

 이를 외부에서 쓸 수 있게 export 해준다.

import { ChangeEvent, useEffect, useState } from "react";

interface UseFormProps<T> {
    //
    initailValue: T; // { email: '', password: '' }
    // 올바른지 체크
    validate: (values: T) => Record<keyof T, string>; // T에 대한 키 값을 받고, 그것에 대한 value가 string이다라고 이해하면 된다. 
}

function useForm<T>({initailValue, validate}: UseFormProps<T>) {
    const [values, setValues] = useState(initailValue);
    const [touched, setTouched] = useState<Record<string, boolean>>();
    const [errors, setErrors] = useState<Record<string, string>>();

    // 사용자가 입력값을 바꿀 때 실행되는 함수다.
    const handleChange = (name: keyof T, text: string) => {
        setValues({
            ...values, // 불변성 유지(기존 값 유지) // setValues는 기존 값 유지하고~~
            [name]: text, // name을 찾아서 텍스트를 넣어주면 된다 라고 생각하면 된다.
        });
    };

    // boolean에 따라서 블러 or not 블러
    const handleBlur = (name: keyof T) => {
        setTouched({
            ...touched,
            [name]: true,
        })
    };

    // 이메일, 인풋, 패스워드 인풋, 속성들을 좀 가져오는 것
    const getInputProps = (name: keyof T) => {
        const value = values[name]; // 값을 받기
        const onChange = ( // 이 함수를 통해서 우리가 e.target.value를 가져와준다고 생각하면 된다.
            e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
        ) => handleChange(name, e.target.value);  

        // 바뀔 때, 블러 처리
        const onBlur = () => handleBlur(name);

        return { value, onChange, onBlur };
    };

    // values가 변경될 때마다 에러 검증 로직이 실행됨.
    // { email: ""}
    useEffect(() => {
        const newErrors = validate(values);
        setErrors(newErrors); // 오류 메시지 업뎃
    }, [validate, values]); // validate, values가 바뀔 때마다 useEffect를 계속 실행되도록 할 것이다. 

    return { values, errors, touched, getInputProps };
}

export default useForm;

 

 

 

🍎

이제, 이 useForm을 어떻게 활용을 할 거냐?

먼저, utils 폴더를 하나 만들어서 검증 관련 로직을 짤 것이다. (validate.ts)

이메일 유효성 검사 정규표현식은 검색해서 따온다. 그리고 그것을 코드에 넣어서 처리하도록 한다. 

/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i

비밀번호도 마찬가지다.

if ( // 이 로그인 유효성 검사에 테스트했을 때, values.email 값이 일치하지 않는다면 errors.email이 띄워지도록 한다는 것이다. 
        !/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i.test(
            valuse.email,
        )
    ) {
        errors.email = "올바른 이메일 형식이 아닙니다!";
    }

    // 비밀번호 8자 20자 사이
    if (!(valuse.password.length >= 8 && valuse.password.length < 20)) {
        errors.password = "비밀번호는 8~20자 사이로 입력해주세요.";
    }

    return errors;

그 다음 로그인 유효성 검사 함수를 만들어준다.

 

이제, LoginPage에 useForm을 넣어서 대입하면 된다. 

여기서 error 처리 같은 경우

{errors?.email && (
                    <div className="text-red-500 text-sm">{errors.email}</div>
                )}

이런 식으로 적어주면 올바른 이메일로 입력할 경우 에러 메시지가 없어지는 걸 볼 수 있는데, 새로고침을 하면 에러 메시지가 떠있는 것을 알 수 있다. ui/ux 적으로 별로 안 좋기 때문에 추가적으로 조건을 추가해준다.

{errors?.email && touched?.email && (
                    <div className="text-red-500 text-sm">{errors.email}</div>
                )}

 이렇게 하면 새로고침 해도 에러가 안 뜨고, 터치해야지 에러가 뜬다는 것을 알 수 있다. input 같은 경우도 스타일 변경을 해준다.

<input
                    {...getInputProps("email")}
                    name="email"
                    className={`border border-[#333} w-[300px] p-[10px] focus:border-[#807bff] rounded-sm
                        ${errors?.email && touched?.email ? "border-red-500 bg-red-200" : "border-gray-300"}`}
                    type={'email'}
                    placeholder={"이메일"}  
                />

useForm 처리가 잘 되는 것을 볼 수 있는데 이제 버튼 즉, 모든 에러가 없을 때 버튼이 활성화되는 효과를 주면 좋을 것이다. 

그 부분을 처리하기 위해서 아래처럼 코드를 작성해준다. 

// 오류가 하나라도 있거나, 입력값이 비어있으면 버튼을 비활성화
    const isDisabled = 
        Object.values(errors || {}).some((error) => error.length > 0) || // 오류가 있으면 true
        Object.values(values).some((value) => value === ""); // 입력값이 비어있으면 true

이것을 이제 버튼에 달아보면 정상적으로 작동하는 걸 알 수 있다.