React로 로그인 form 만들기

이번에는 React를 이용해 로그인 form을 만들어보자.

컴포넌트 구조 설계

state 관리 설계

Context 초기 설정

const initialFormData = {
    id: '',
    pw: '',
    confirmPw: '',
}

export const FormContext = createContext({
    formData: initialFormData,
    setFormData: () => {},
})

function App() {
    const [formData, setFormData] = useState(initialFormData)

    return (
        <FormContext.Provider value= >
            <section className="form-wrapper">
                <Form />
            </section>
            <FontControlBox />
            <Modal />
        </FormContext.Provider>
    )
}

Form 컴포넌트에서의 errorData state 관리

const initialErrorData = {
    id: '', // true or invalidId
    pw: '', // true or invalidPw
    confirmPw: '', // true or invalidConfirmPw
}

const Form = ({ modalRef }) => {
     const [errorData, setErrorData] = useState(initialErrorData)
     const handleSubmit = (e) => {
        e.preventDefault()
        const isValid = Object.values(errorData).every((val) => val === true)
        if (isValid === true) modalRef.current.showModal()
    }

     return (
        <FormInput
                id={'id'}
                label={'아이디'}
                inputProps=
                errorData={errorData}
                setErrorData={setErrorData}
        />
     )
}

FormInput 컴포넌트에서의 유효성 검사와 에러 메세지 렌더

const ERROR_MSG = {
    required: '필수 정보입니다.',
    invalidId:
        '5~20자의 영문 소문자, 숫자와 특수기호(_),(-)만 사용 가능합니다.',
    invalidPw: '8~16자 영문 대 소문자, 숫자를 사용하세요.',
    invalidConfirmPw: '비밀번호가 일치하지 않습니다.',
}
const checkRegex = (inputId) => {
        let result
        const value = formData[inputId]
        if (value.length === 0) {
            result = 'required'
        } else {
            switch (inputId) {
                case 'id':
                    result = ID_REGEX.test(value) ? true : 'invalidId'
                    break
                case 'pw':
                    result = PW_REGEX.test(value) ? true : 'invalidPw'
                    checkRegex('confirmPw')
                    break
                case 'confirmPw':
                    result =
                        value === formData['pw'] ? true : 'invalidConfirmPw'
                    break
                default:
                    return
            }
        }
        // 정리!
        setErrorData((prev) => ({ ...prev, [inputId]: result }))
}

<input
    id={id}
    {...inputProps}
    value={formData[id]}
    onChange={(e) =>
        setFormData({ ...formData, [id]: e.target.value })
    }
    onBlur={() => checkRegex(id)}
/>
<div id="id-msg" className="mt-1 mb-3 text-xs text-red-500">
    {errorData[id] !== true ? ERROR_MSG[errorData[id]] : ''}
</div>

FontControlBox 컴포넌트

const $html = document.documentElement
const MAX_FONT_SIZE = 20
const MIN_FONT_SIZE = 12

const getHtmlFontSize = () => {
    return parseFloat(window.getComputedStyle($html).fontSize)
}

const FontControlBox = () => {
    const [fontSize, setFontSize] = useState(getHtmlFontSize())

    const onClickFontsizeControl = (flag) => {
        if (flag === 'increase') {
            setFontSize((prev) => prev + 1)
        } else {
            setFontSize((prev) => prev - 1)
        }
    }

    useLayoutEffect(() => {
        $html.style.fontSize = fontSize + 'px'
    }, [fontSize])

    return (
        <aside id="font-control-box" className="flex fixed bottom-0 right-0">
            <button
                id="increase-font-btn"
                onClick={() => onClickFontsizeControl('increase')}
                disabled={fontSize >= MAX_FONT_SIZE}
                className="bg-white text-gray-500 border border-gray-300 hover:bg-red-50 focus:outline-none focus:shadow-outline disabled:bg-gray-500 disabled:text-white rounded-full"
            >
                +
            </button>
            <button
                id="decrease-font-btn"
                onClick={() => onClickFontsizeControl('decrease')}
                disabled={fontSize <= MIN_FONT_SIZE}
                className="bg-white text-gray-500 border border-gray-300 hover:bg-blue-50 focus:outline-none focus:shadow-outline disabled:bg-gray-500 disabled:text-white rounded-full"
            >
                -
            </button>
        </aside>
    )
}

완성본

완성본 보러가기

끝!