Web/프론트엔드(Front-end)

(React) 회원가입 페이지에서 input값 검증하기

심플블루 2024. 7. 21. 18:55
반응형

 

실제로는 더 많은 입력값과 검증이 필요하겠지만, 검증함수의 로직을 파악할 용도로 간단하게 작성하였다.
전체코드는 아래와 같다.
import { useState } from "react";
import { useDispatch } from "react-redux";
import { loginPostAsync } from "../../slices/loginSlice";
import { useNavigate } from "react-router-dom";
import useCustomRegister from "../../hooks/useCustomRegister";
import { Link } from "react-router-dom";
import AddressInput from '../common/AddressInput';

const RegisterComponent = () => {

    const initState = {
        email: '',
        pw: '',
        confirmPw: '', // 비밀번호 확인 필드 추가
        username: '',
        zonecode: '', // 우편번호
        address: '', // 주소
        detailedAddress: '' // 상세주소
    }
    
    const [registerParam, setRegisterParam] = useState({...initState})

    const {doRegister, moveToPath} = useCustomRegister()

    const [isOpen, setIsOpen] = useState(false) // 주소찾기 모달 열기/닫기 상태
    const [errors, setErrors] = useState({
        email: '',
        pw: '',
        confirmPw: '',
        username: '',
        address: '',
        detailedAddress: ''
    })

    const validateField = (name, value, pwValue) => {
        let error = ''
        switch (name) {
            case 'email':
                if (!value) {
                    error = '이메일은 필수 입력항목입니다.'
                } else if (!/\S+@\S+\.\S+/.test(value)) {
                    error = '유효한 이메일 주소를 입력해주세요.'
                }
                break
            case 'pw':
                if (!value) {
                    error = '비밀번호는 필수 입력항목입니다.'
                } else if (value.length < 6) {
                    error = '비밀번호는 최소 6자 이상이어야 합니다.'
                }
                break
            case 'confirmPw':
                if (value && value !== pwValue) {
                    error = '비밀번호가 일치하지 않습니다.'
                } 
                break
            case 'address':
                if (!value) {
                    error = '주소는 필수 입력항목입니다.'
                }
                break
            case 'detailedAddress':
                if(!value) {
                    error = '상세 주소는 필수 입력항목입니다.'
                }
                break
            default:
                break                 
        }
        return error
    }    
        // const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        

    const handleChange = (e) => {
        const {name, value} = e.target

        setRegisterParam({
            ...registerParam,
            [name]: value
        });

        let error
        if (name === 'confirmPw') { // confirmPw 필드에 입력할 때마다 현재 pw 필드의 값과 비교하여 에러 체크
            error = validateField(name, value, registerParam.pw)
        } else { // confirm 필드가 비어있을 때는 에러 메시지를 표시하지 않음
            error = validateField(name, value)
        }
        setErrors({
            ...errors,
            [name]: error
        })

        // pw가 변경되었을 때도 confirmPw 필드의 값을 재검증하여 에러 상태 업데이트
        if(name === 'pw' && registerParam.confirmPw) {
            const confirmPwError = validateField('confirmPw', registerParam.confirmPw, value)
            setErrors(prev => ({
                ...prev,
                confirmPw: confirmPwError
            }))
        }
    }

    const handleAddressComplete = (data) => {
        console.log("주소 선택 완료:", data);
        setRegisterParam({
            ...registerParam,
            zonecode: data.zonecode,
            address: data.address
        });
        setIsOpen(false); // 주소찾기 모달 닫기
    }

    const toggleModal = () => {
        console.log("토글 모달 호출됨, 현재 상태:", isOpen);
        setIsOpen(!isOpen); // 주소찾기 모달 열기/닫기 토글
    }

    const handleClickRegister = () => {
        const newErrors ={}
        Object.keys(registerParam).forEach(key => {
            const error = validateField(key, registerParam[key])
            if(error) {
                newErrors[key] = error
            }
        })
        if(Object.keys(newErrors).length > 0) {
            setErrors(newErrors)
            return
        }

        // 회원가입 처리 로직
        doRegister(registerParam)
    }

    return (
        <div className="border-2 border-sky-200 mt-10 m-2 p-4 overflow-auto" style={{maxHeight: '100vh'}}>
            <div className="flex justify-center">
                <div className="text-4xl m-4 p-4 font-extrabold text-blue-500">
                    회원가입
                </div>
            </div>
            <div className="flex justify-center">
                <div className="relative mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 py-3 pr-3 text-left font-bold">Email</div>
                    <input className="w-4/5 p-3 rounded-r border 
                            border-solid border-neutral-500 shadow-md"
                    name="email"
                    type={'text'}
                    placeholder="이메일주소"
                    value={registerParam.email}
                    onChange={handleChange}>
                    </input>
                    <div className="w-1/5 pr-3 text-left"></div>
                    {errors.email && <div className="w-4/5 text-red-600 mt-1">{errors.email}</div>}
                </div>
            </div>
            <div className="flex justify-center">
                <div className="relative mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 py-3 pr-3 text-left font-bold">
                        Password
                    </div>
                    <input className="w-4/5 p-3 rounded-r border border-solid
                             border-neutral-500 shadow-md"
                    name="pw"
                    type={'password'}
                    placeholder="비밀번호는 최소 6자리이상 입력"
                    value={registerParam.pw}
                    onChange={handleChange}>                    
                    </input>
                    <div className="w-1/5 pr-3 text-left"></div>
                    {errors.pw && <div className="w-4/5 text-red-600 mt-1">{errors.pw}</div>}
                </div>
            </div>
            <div className="flex justify-center">
                <div className="relative mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 py-0 pr-3 text-left font-bold">
                        Confirm Password
                    </div>
                    <input
                        className="w-4/5 p-3 rounded-r border border-solid border-neutral-500 shadow-md"
                        name="confirmPw"
                        type="password"
                        placeholder="비밀번호를 한번더 입력해 주세요"
                        value={registerParam.confirmPw}
                        onChange={handleChange}>
                    </input>
                    <div className="w-1/5 pr-3 text-left"></div>
                    {errors.confirmPw && <div className="w-4/5 text-red-600 mt-1">{errors.confirmPw}</div>}
                </div>
            </div>
            <div className="flex justify-center">
                <div className="relateve mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 py-3 pr-3 text-left font-bold">
                        Username
                    </div>
                    <input className="w-4/5 p-3 rounded-r border border-solid
                             border-neutral-500 shadow-md"
                    name="username"
                    type={'text'}
                    placeholder="닉네임"
                    value={registerParam.username}
                    onChange={handleChange}>    
                    </input>
                    <div className="w-1/5 pr-3 text-left"></div>
                    {errors.username && <div className="w-4/5 text-red-600 mt-1">{errors.username}</div>}
                </div>
            </div>
            <div className="flex justify-center">
                <div className="relateve mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 py-3 pr-3 text-left font-bold">
                        Address
                    </div>
                    <div className="w-4/5 flex">
                        <input className="mr-3 p-3 rounded-r border border-solid 
                                        border-neutral-500 shadow-md"
                               name="zonecode"
                               type={'text'}
                               placeholder="우편번호"
                               value={registerParam.zonecode}
                               readOnly>
                        </input>                 
                        <button
                            className="w-36 rounded bg-green-600 text-xl text-white p-2"
                            type="button"
                            onClick={toggleModal}
                        >
                            주소찾기
                        </button>
                        <AddressInput
                            isOpen={isOpen}
                            onComplete={handleAddressComplete}
                            toggleHandler={toggleModal}
                        />
                    </div>
                </div>
            </div>
            <div className="flex justify-center">
                <div className="relateve mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 p-3 text-left font-bold">
                    </div>
                    <input className="w-4/5 p-3 rounded-r border border-solid 
                                        border-neutral-500 shadow-md"
                            name="address"
                            placeholder="주소"
                            type={'text'}
                            value={registerParam.address}
                            readOnly>
                    </input>
                    <div className="w-1/5 pr-3 text-left"></div>
                    {errors.address && <div className="w-4/5 text-red-600 mt-1">{errors.address}</div>}
                </div>     
            </div>    
            <div className="flex justify-center">
                <div className="relateve mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 p-3 text-left font-blod">
                    </div>
                    <input className="w-4/5 p-3 rounded-r border border-solid
                                     border-neutral-500 shadow-md"
                            name="detailedAddress"
                            type={'text'}
                            placeholder="상세주소"
                            value={registerParam.detailedAddress}
                            onChange={handleChange}>
                    </input>
                    <div className="w-1/5 pr-3 text-left"></div>
                    {errors.detailedAddress && <div className="w-4/5 text-red-600 mt-1">{errors.detailedAddress}</div>}               
                </div>
            </div>  

            <div className="flex justify-center mt-8">
                <div className="relative mb-1 flex w-full justify-center">
                    <div className="w-1/5 p-3 text-left font-blod">
                    </div>
                    <div className="w-4/5 flex justify-center font-bold">
                        <button className="rounded p-4 w-full bg-blue-500 text-xl text-white"
                        onClick={handleClickRegister}>
                            가입하기
                        </button>
                    </div>
                </div>
            </div>

            <div className="flex justify-center">
                <div className="w-4/5 p-6 flex justify-center font-bold">
                    <Link to="/member/login" className="text-blue-500">
                        로그인 페이지로 이동
                    </Link>
                </div>
            </div>

        </div>      
    )
}

export default RegisterComponent;

 

 

1. 상태 추가: 먼저, 각 필드의 에러 메시지를 저장할 상태를 추가합니다.

const [errors, setErrors] = useState({
    email: '',
    pw: '',
    confirmPw: '',
    username: '',
    detailedAddress: ''
});

 

2. 검증 함수 생성: 입력값을 검증하는 함수를 만듭니다.

const validateField = (name, value, pwValue) => {
    let error = '';
    switch (name) {
        case 'email':
            if (!value) {
                error = '이메일은 필수 입력항목입니다.';
            } else if (!/\S+@\S+\.\S+/.test(value)) {
                error = '유효한 이메일 주소를 입력해주세요.';
            }
            break;
        case 'pw':
            if (!value) {
                error = '비밀번호는 필수 입력항목입니다.';
            } else if (value.length < 6) {
                error = '비밀번호는 최소 6자 이상이어야 합니다.';
            }
            break;
        case 'confirmPw':
            if (value && value !== pwValue) {
                error = '비밀번호가 일치하지 않습니다.';
            }
            break;
        case 'username':
            if (!value) {
                error = '사용자 이름은 필수 입력항목입니다.';
            }
            break;
        case 'detailedAddress':
            if (!value) {
                error = '상세 주소는 필수 입력항목입니다.';
            }
            break;
        default:
            break;
    }
    return error;
};

 

3. handleChange 함수 수정: 입력값이 변경될 때마다 검증을 수행하도록 수정합니다.

 

  • const handleChange = (e) => { ... }:
    • handleChange는 화살표 함수로 정의된 이벤트 핸들러입니다.
    • e는 이벤트 객체(event object)로, 이 객체는 폼 입력 필드의 변경 이벤트에 대한 정보를 담고 있습니다.
  • const {name, value} = e.target:
    • 구조 분해 할당(destructuring assignment)을 사용하여 e.target 객체로부터 name과 value 속성을 추출합니다.
    • e.target은 이벤트가 발생한 요소(이 경우 입력 필드)를 가리킵니다.
    • name은 입력 필드의 이름 속성값이고, value는 입력된 값입니다.
  • setRegisterParam({ ...registerParam, [name]: value }):
    • setRegisterParam은 상태 업데이트 함수로, useState 훅에 의해 생성되었습니다.
    • 새로운 상태 객체를 생성하여 registerParam 상태를 업데이트합니다.
    • ...registerParam은 기존 상태 객체를 펼쳐서 복사합니다. 즉, 현재 상태의 모든 속성을 새로운 객체에 복사합니다.
    • [name]: value는 계산된 속성 이름(computed property name)을 사용하여 동적으로 속성명을 설정합니다. 여기서 name은 입력 필드의 이름이고, value는 해당 필드의 입력값입니다.
    • 이를 통해 입력 필드의 이름에 해당하는 상태 속성을 입력값으로 업데이트합니다.

 

    const handleChange = (e) => {
        const {name, value} = e.target

        setRegisterParam({
            ...registerParam,
            [name]: value
        });

        let error
        if (name === 'confirmPw') { // confirmPw 필드에 입력할 때마다 현재 pw 필드의 값과 비교하여 에러 체크
            error = validateField(name, value, registerParam.pw)
        } else { // confirm 필드가 비어있을 때는 에러 메시지를 표시하지 않음
            error = validateField(name, value)
        }
        setErrors({
            ...errors,
            [name]: error
        })

        // pw가 변경되었을 때도 confirmPw 필드의 값을 재검증하여 에러 상태 업데이트
        if(name === 'pw' && registerParam.confirmPw) {
            const confirmPwError = validateField('confirmPw', registerParam.confirmPw, value)
            setErrors(prev => ({
                ...prev,
                confirmPw: confirmPwError
            }))
        }
    }

 

4. 폼 제출 시 전체 검증: 폼 제출 시 모든 필드를 한 번 더 검증합니다.

const handleClickRegister = () => {
    const newErrors = {};
    Object.keys(registerParam).forEach(key => {
        const error = validateField(key, registerParam[key]);
        if (error) {
            newErrors[key] = error;
        }
    });

    if (Object.keys(newErrors).length > 0) {
        setErrors(newErrors);
        return;
    }

    // 회원가입 처리 로직
    doRegister(registerParam);
};

 

5. 에러 메시지 표시: 각 입력 필드 아래에 에러 메시지를 표시합니다. 예를 들어

            <div className="flex justify-center">
                <div className="relative mb-4 flex w-full flex-wrap items-stretch">
                    <div className="w-1/5 py-3 pr-3 text-left font-bold">
                        Password
                    </div>
                    <input className="w-4/5 p-3 rounded-r border border-solid
                             border-neutral-500 shadow-md"
                    name="pw"
                    type={'password'}
                    placeholder="비밀번호는 최소 6자리이상 입력"
                    value={registerParam.pw}
                    onChange={handleChange}>                    
                    </input>
                    <div className="w-1/5 pr-3 text-left"></div>
                    {errors.pw && <div className="w-4/5 text-red-600 mt-1">{errors.pw}</div>}
                </div>
            </div>
반응형