페이지가 로드 된 시점에 ID 입력 창에 Focus가 되어 있어야 합니다.
ID, 비밀번호, 비밀번호 확인 필드에 대한 유효성 검사를 수행해야 합니다.
모든 필드의 값은 빠짐 없이 입력해야 합니다.
ID, 비밀번호, 비밀번호 확인 필드는 유효성 조건을 충족해야 합니다.
- ID: 5~20자. 영문 소문자, 숫자. 특수기호(_),(-)만 사용 가능
- 비밀번호: 8~16자. 영문 대/소문자, 숫자 사용 가능
- 비밀번호 확인: 비밀번호와 일치
유효하지 않은 값일 경우, 각 경우에 맞는 에러 메시지를 보여주어야 합니다.
유효성 조건과 에러 메시지는 아래를 참고해주세요.
가입하기 버튼 클릭 시, 모든 input의 값이 유효한 상태일 경우 입력한 아이디와 비밀번호를 확인할 수 있는 모달 창을 보여주어야 합니다.
회원가입 폼에 사용된 기본 폰트 사이즈는 16px입니다.
기본 폰트 사이즈를 기준으로 1px씩 폰트 사이즈를 조절할 수 있는 기능을 구현해주세요.
(최소: 12px, 최대: 20px)
//대상 : ID입력 input
//이벤트 : 페이지(window)가 로드되었을 때
//핸들러 : focus()
const $id = document.getElementById('id')
const $idMsg = document.getElementById('id-msg')
window.addEventListener('load', () => $id.focus())
//input 태그에 바로 autofocus 속성 달아줘도 됨
const $id = document.getElementById('id')
const $idMsg = document.getElementById('id-msg')
const $pw = document.getElementById('pw')
const $pwMsg = document.getElementById('pw-msg')
const $pwCheck = document.getElementById('pw-check')
const $pwCheckMsg = document.getElementById('pw-check-msg')
const ID_REGEX = new RegExp('^[a-z0-9_-]{5,20}$') //5~20자. 영문 소문자, 숫자. 특수기호(_),(-)만 사용 가능
const PW_REGEX = new RegExp('^[a-zA-Z0-9]{8,16}$') //8~16자. 영문 대/소문자, 숫자 사용 가능
const ID_ERROR_MSG = {
required: '필수 정보입니다.',
invalid: '5~20자의 영문 소문자, 숫자와 특수기호(_),(-)만 사용 가능합니다.',
}
const PW_ERROR_MSG = {
required: '필수 정보입니다.',
invalid: '8~16자 영문 대 소문자, 숫자를 사용하세요.',
}
const PW_CHECK_ERROR_MSG = {
required: '필수 정보입니다.',
invalid: '비밀번호가 일치하지 않습니다.',
}
로직은 id, password, password check 셋 다 동일. 따라서 id 코드 부분만 기재
password check는 정규식 테스트 대신 비밀번호와 일치 여부 확인하면 됨
const checkIdRegex = (value) => {
if (value.length === 0) {
return 'required'
} else {
return ID_REGEX.test(value) ? true : 'invalid'
}
}
const checkIdValidation = (value) => {
const isValidId = checkIdRegex(value)
if (isValidId !== true) {
//isValidId -> invalid, required
$id.classList.add('border-red-600')
$idMsg.innerText = ID_ERROR_MSG[isValidId]
} else {
$id.classList.remove('border-red-600')
$idMsg.innerText = ''
}
return isValidId
}
$id.addEventListener('focusout', () => checkIdValidation($id.value))
// $id.addEventListener('focusout', (e) => checkIdValidation(e.target.value))
const $submit = document.getElementById('submit')
const $modal = document.getElementById('modal')
const $confirmId = document.getElementById('confirm-id')
const $confirmPw = document.getElementById('confirm-pw')
const $cancelBtn = document.getElementById('cancel-btn')
const $approveBtn = document.getElementById('approve-btn')
$submit.addEventListener('click', (e) => {
//form 태그는 눌렸을 때 자동으로 form 내부의 값들을 서버로 전달함
e.preventDefault()
const isValidForm =
checkIdValidation($id.value) === true &&
checkPwValidation($pw.value) === true &&
checkPwCheckValidation($pwCheck.value) === true
if (isValidForm) {
$confirmId.innerText = $id.value
$confirmPw.innerText = $pw.value
$modal.showModal()
}
})
$cancelBtn.addEventListener('click', () => $modal.close())
$approveBtn.addEventListener('click', () => {
window.alert('가입되었습니다.')
$modal.close()
})
const $increaseFontBtn = document.getElementById('increase-font-btn')
const $decreaseFontBtn = document.getElementById('decrease-font-btn')
const $html = document.documentElement
const MAX_FONT_SIZE = 20
const MIN_FONT_SIZE = 12
const getHtmlFontSize = () => {
//html에 적용된 inline style or css sheet의 fontsize에서 숫자만 추출
return parseFloat(window.getComputedStyle($html).fontSize)
}
$increaseFontBtn.addEventListener('click', () => {
//font size + 1px
onClickFontSizeControl('increase')
})
$decreaseFontBtn.addEventListener('click', () => {
//font size - 1px
onClickFontSizeControl('decrease')
})
const onClickFontSizeControl = (flag) => {
const fontSize = getHtmlFontSize()
let newFontSize = flag === 'increase' ? fontSize + 1 : fontSize - 1
$html.style.fontSize = newFontSize
$decreaseFontBtn.disabled = newFontSize <= MIN_FONT_SIZE
$increaseFontBtn.disabled = newFontSize >= MAX_FONT_SIZE
}
유효성 검사와 커스텀 에러 메세지 출력 부분에 겹치는 로직들이 있음
const ERROR_MSG = {
required: '필수 정보입니다.',
invalidId:
'5~20자의 영문 소문자, 숫자와 특수기호(_),(-)만 사용 가능합니다.',
invalidPw: '8~16자 영문 대 소문자, 숫자를 사용하세요.',
invalidPwCheck: '비밀번호가 일치하지 않습니다.',
}
const checkRegex = (target) => {
const { value, id } = target // 구조분해할당
//const value = target.value; const id = target.id
if (value.length === 0) {
return 'required'
} else {
switch (id) {
case 'id':
return ID_REGEX.test(value) ? true : 'invalidId'
case 'pw':
return PW_REGEX.test(value) ? true : 'invalidPw'
case 'pw-check':
return value === $pw.value ? true : 'invalidPwCheck'
}
}
}
const checkValidation = (target, msgTarget) => {
const isValid = checkRegex(target)
if (isValid !== true) {
target.classList.add('border-red-600')
msgTarget.innerText = ERROR_MSG[isValid]
} else {
target.classList.remove('border-red-600')
msgTarget.innerText = ''
}
return isValid
}
$id.addEventListener('focusout', () => checkValidation($id, $idMsg))
$pw.addEventListener('focusout', () => checkValidation($pw, $pwMsg))
$pwCheck.addEventListener('focusout', () =>
checkValidation($pwCheck, $pwCheckMsg)
)
useRef 사용하여 modal의 DOM element인 dialog에 접근. (showModal(), close())
그런데 리액트 함수형 컴포넌트는 ref를 바로 전달받을 수 없고 forwardRef로 감싸줘야지 두번쨰 인자로 ref를 받을 수 있다.
import './App.css'
import Form from './components/Form'
import Footer from './components/Footer'
import FormControlBox from './components/FontControlBox'
import Modal from './components/Modal'
import { createContext, useState, useRef } from 'react'
const initialFormData = {
id: '',
pw: '',
confirmPw: '',
}
export const FormContext = createContext({
formData: initialFormData,
setFormData: () => {},
})
function App() {
const [formData, setFormData] = useState(initialFormData)
const modalRef = useRef(null)
return (
<FormContext.Provider value={{ formData, setFormData }}>
<section className="form-wrapper">
<Form modalRef={modalRef} />
<Footer />
</section>
<FormControlBox />
<Modal ref={modalRef} />
</FormContext.Provider>
)
}
export default App
import { useState } from 'react'
import FormInput from './FormInput'
const initialErrorData = {
id: '',
pw: '',
confirmPw: '',
}
const Form = ({ modalRef }) => {
const [errorData, setErrorData] = useState(initialErrorData)
const handleSubmit = (e) => {
e.preventDefault()
const isValid = Object.values(errorData).every(
(value) => value === true
) //모든 value가 true이면 true
isValid && modalRef.current.showModal()
}
return (
<form
id="form"
className="w-full max-w-md m-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
autoComplete="off"
onSubmit={handleSubmit}
>
<FormInput
id={'id'}
label={'아이디'}
errorData={errorData}
setErrorData={setErrorData}
inputProps={{
type: 'text',
placeholder: '아이디를 입력해주세요.',
// autoFocus: true,
}}
/>
<FormInput
id={'pw'}
label={'비밀번호'}
errorData={errorData}
setErrorData={setErrorData}
inputProps={{
type: 'password',
placeholder: '비밀번호를 입력해주세요.',
autoComplete: 'off',
}}
/>
<FormInput
id={'confirmPw'}
label={'비밀번호 확인'}
errorData={errorData}
setErrorData={setErrorData}
inputProps={{
type: 'password',
placeholder: '비밀번호 확인을 입력해주세요.',
autoComplete: 'off',
}}
/>
<div className="flex items-center justify-center">
<input
id="submit"
type="submit"
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:bg-gray-500"
value="가입하기"
/>
</div>
</form>
)
}
export default Form
import { useEffect, useRef, useContext } from 'react'
import { FormContext } from '../App'
const ID_REGEX = new RegExp('^[a-z0-9_-]{5,20}$') //5~20자. 영문 소문자, 숫자. 특수기호(_),(-)만 사용 가능
const PW_REGEX = new RegExp('^[a-zA-Z0-9]{8,16}$') //8~16자. 영문 대/소문자, 숫자 사용 가능
const ERROR_MSG = {
required: '필수 정보입니다.',
invalidId:
'5~20자의 영문 소문자, 숫자와 특수기호(_),(-)만 사용 가능합니다.',
invalidPw: '8~16자 영문 대 소문자, 숫자를 사용하세요.',
invalidConfirmPw: '비밀번호가 일치하지 않습니다.',
}
const FormInput = ({ id, label, inputProps, errorData, setErrorData }) => {
const inputRef = useRef(null)
const { formData, setFormData } = useContext(FormContext)
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
}
}
//react에서는 setState를 비동기적으로 실행
//직전의 최신 state값 보장하기 위한 방법 = 함수 넘겨주기
setErrorData((prev) => ({ ...prev, [inputId]: result }))
}
useEffect(() => {
if (id === 'id') {
inputRef.current.focus()
}
}, []) //mount될 때만 실행
return (
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor={id}
>
{label}
</label>
<input
id={id}
className="shadow border rounded w-full py-2 px-3 text-gray-700"
ref={inputRef}
value={formData[id]}
onChange={(e) =>
//직전의 최신 state값 보장하기 위한 방법 = 함수 넘겨주기
setFormData((prev) => ({ ...prev, [id]: e.target.value }))
}
onBlur={() => checkRegex(id)}
{...inputProps}
/>
<div className="mt-1 mb-3 text-xs text-red-500">
{errorData[id] !== true ? ERROR_MSG[errorData[id]] : ''}
</div>
</div>
)
}
export default FormInput
import { useEffect, useState } from 'react'
const $html = document.documentElement
const getHtmlFontSize = () => {
//html에 적용된 inline style or css sheet의 fontsize에서 숫자만 추출
return parseFloat(window.getComputedStyle($html).fontSize)
}
const MAX_FONT_SIZE = 20
const MIN_FONT_SIZE = 12
const FormControlBox = () => {
const [fontSize, setFontSize] = useState(getHtmlFontSize())
const handleFontSizeControl = (flag) => {
if (flag === 'increase') {
setFontSize((prev) => prev + 1)
} else {
setFontSize((prev) => prev - 1)
}
}
useEffect(() => {
$html.style.fontSize = fontSize + 'px'
}, [fontSize])
return (
<aside id="font-control-box" className="flex fixed bottom-0 right-0">
<button
id="increase-font-btn"
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"
onClick={() => handleFontSizeControl('increase')}
disabled = {fontSize >= MAX_FONT_SIZE}
>
+
</button>
<button
id="decrease-font-btn"
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"
onClick={() => handleFontSizeControl('decrease')}
disabled = {fontSize <= MIN_FONT_SIZE}
>
-
</button>
</aside>
)
}
export default FormControlBox
useContext (Context API)의 단점