fix: 모바일 가입 폼에서 한 글자마다 키보드 닫히는 버그 수정

원인: Field 컴포넌트를 SignupPage '안에서' 정의 → 글자 입력 → setForm 재렌더
→ Field가 매번 새 컴포넌트 타입으로 인식 → input 리마운트 → 포커스 상실
→ 모바일 키보드 닫힘. React의 대표 함정.

수정: Field를 컴포넌트 밖으로 이동, value/onChange를 props로 전달.
버그의 원리와 교훈('컴포넌트 안에서 컴포넌트를 정의하지 말 것')을
소스 주석에 학습 포인트로 기록 — 학생들이 같은 함정을 미리 배우게.

검증: 연속 입력(리렌더 3회) 후에도 동일 DOM 노드 유지·포커스 유지 확인.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AWESOMEDEV 2026-07-17 11:03:35 +09:00
parent c0b783613d
commit 03fdde7495

View File

@ -23,6 +23,36 @@ const EMPTY = {
privacyConsent: false, // ·
};
/**
* 입력 부품 (라벨 + input).
*
* 학습 포인트 실제로 났던 버그가 담긴 자리다.
* 처음엔 컴포넌트를 SignupPage "안에" 정의했다. 그랬더니 모바일에서
* 글자를 때마다 키보드가 닫히는 버그가 났다. ?
* 글자 입력 setForm SignupPage 재렌더 함수 안의 Field가 "새로 정의"
* React는 이전 Field와 다른 컴포넌트로 판단 input을 버리고 새로 만듦(리마운트)
* 순간 포커스가 풀림 모바일 키보드가 닫힘.
* 컴포넌트를 밖으로 빼면 Field의 정체(타입) 항상 같아서 input이 유지된다.
* 규칙: "컴포넌트 안에서 컴포넌트를 정의하지 말 것" React의 대표 함정이다.
* (밖으로 나오면서 form/set에 직접 접근할 없으니 value와 onChange를 props로 받는다.)
*/
function Field({ label, k, type = 'text', placeholder, autoComplete, value, onChange }) {
return (
<div className="field">
<label htmlFor={`su-${k}`}>{label}</label>
<input
id={`su-${k}`}
className="input"
type={type}
value={value}
onChange={(e) => onChange(k, e.target.value)}
placeholder={placeholder}
autoComplete={autoComplete}
/>
</div>
);
}
export default function SignupPage() {
const { user } = useAuth();
const navigate = useNavigate();
@ -102,23 +132,7 @@ export default function SignupPage() {
}
}
// .
function Field({ label, k, type = 'text', placeholder, autoComplete }) {
return (
<div className="field">
<label htmlFor={`su-${k}`}>{label}</label>
<input
id={`su-${k}`}
className="input"
type={type}
value={form[k]}
onChange={(e) => set(k, e.target.value)}
placeholder={placeholder}
autoComplete={autoComplete}
/>
</div>
);
}
// (Field) , .
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
@ -144,9 +158,9 @@ export default function SignupPage() {
) : (
<form onSubmit={handleSubmit}>
<div className="section-title" style={{ marginTop: 0 }}>계정</div>
<Field label="아이디 (영문·숫자 4자 이상)" k="username" autoComplete="username" />
<Field label="비밀번호 (8자 이상)" k="password" type="password" autoComplete="new-password" />
<Field label="비밀번호 확인" k="passwordConfirm" type="password" autoComplete="new-password" />
<Field label="아이디 (영문·숫자 4자 이상)" k="username" autoComplete="username" value={form.username} onChange={set} />
<Field label="비밀번호 (8자 이상)" k="password" type="password" autoComplete="new-password" value={form.password} onChange={set} />
<Field label="비밀번호 확인" k="passwordConfirm" type="password" autoComplete="new-password" value={form.passwordConfirm} onChange={set} />
<div className="field">
<label>트랙</label>
@ -162,7 +176,7 @@ export default function SignupPage() {
</div>
<div className="section-title">인적사항</div>
<Field label="이름" k="name" autoComplete="name" />
<Field label="이름" k="name" autoComplete="name" value={form.name} onChange={set} />
{/* 회사 이메일 — 아이디만 입력하고 도메인은 고정으로 붙여 보여준다 */}
<div className="field">
@ -192,16 +206,16 @@ export default function SignupPage() {
)}
</div>
<Field label="생년월일" k="birthDate" type="date" />
<Field label="연락처" k="phone" placeholder="010-1234-5678" autoComplete="tel" />
<Field label="주소" k="address" placeholder="시/군/구 이하" />
<Field label="학교" k="school" placeholder="미림마이스터고등학교" />
<Field label="전공(학과)" k="major" />
<Field label="생년월일" k="birthDate" type="date" value={form.birthDate} onChange={set} />
<Field label="연락처" k="phone" placeholder="010-1234-5678" autoComplete="tel" value={form.phone} onChange={set} />
<Field label="주소" k="address" placeholder="시/군/구 이하" value={form.address} onChange={set} />
<Field label="학교" k="school" placeholder="미림마이스터고등학교" value={form.school} onChange={set} />
<Field label="전공(학과)" k="major" value={form.major} onChange={set} />
<div className="section-title">비상 연락처</div>
<Field label="이름" k="emergencyName" />
<Field label="관계 (예: 부, 모)" k="emergencyRelation" />
<Field label="전화번호" k="emergencyPhone" placeholder="010-1234-5678" />
<Field label="이름" k="emergencyName" value={form.emergencyName} onChange={set} />
<Field label="관계 (예: 부, 모)" k="emergencyRelation" value={form.emergencyRelation} onChange={set} />
<Field label="전화번호" k="emergencyPhone" placeholder="010-1234-5678" value={form.emergencyPhone} onChange={set} />
{/* ·
학습 포인트: 동의를 받으려면 "무엇을, 왜, 얼마나" 수집하는지 먼저 알려야 한다