anti-01. 한 칸의 에러가 다른 칸을 덮어쓴다
빈도: 25건 중 8+건 — 1단계 PR에서 가장 자주 보이는 안티패턴이다.
패턴 한 줄
카드 번호 4칸·만료일 2칸·CVC 1칸을 단일 isError / errorStatus / errorMessage 한 변수로 묶어서, 새 입력이 들어올 때마다 이전에 다른 칸에서 발견된 에러 표시가 사라진다.
AS-IS 코드 (8기 PR 발췌)
🔍 읽기 전에
“두 번째 칸을 잘못 입력해서 빨간 테두리가 떴다. 그 상태에서 첫 번째 칸을 정상 입력하면 — 두 번째 칸의 빨간 테두리는 어떻게 될까?”
사례 A — 단일 boolean으로 4칸 묶기
PR #518 — InfoInputSection.tsx
// src/feature/CardRegister/components/InfoInputSection/InfoInputSection.tsx
const [isError, setIsError] = useState(false);
<NumberField cardNumbers={cardNumbers} setCardNumbers={setCardNumbers} setIsError={setIsError} />
<ExpiryField ... setIsError={setIsError} />
<CvcField cvcNumber={cvcNumber} setCvcNumber={setCvcNumber} setIsError={setIsError} />isError 하나를 세 필드가 공유한다. CVC에서 에러가 났는데 카드번호를 정상 입력하면 setIsError(false) 호출로 CVC 에러 표시까지 사라진다.
사례 B — errorStatus 단일 값으로 4칸 묶기
PR #515 — CardNumbersField.tsx
// src/components/domain/CardNumbersField.tsx
const [errorStatus, setErrorStatus] = useState<ErrorStatus | null>(null);
const handleBlur = (index: number) => {
if (cardNumbers[index].length !== 4) {
setErrorStatus('LENGTH');
}
// 통과 분기에서 setErrorStatus(null)이 없음
};PR 작성자도 이 한계를 인지하고 있다 — “여러 input 중 하나에서 발생한 에러가 다른 input 조작 시 덮어써지는 버그가 존재합니다.”
사례 C — 동일 패턴, 다른 이름
PR #498 , PR #519 , PR #524 등도 같은 형태로 발견된다. 이름만 errorMode / inputStatus / formError 등으로 다르다.
리뷰어 피드백 (실제 인용)
- PR #498
ExpiryField.tsx:95— “공유isError를 필드 단위로 덮어써 전체 검증 상태가 깨질 수 있습니다.” - PR #502
CardNumberInputField.tsx:46— “한 칸의 오류가 다른 칸의 에러 표시를 지우고 있습니다.” - PR #513
CardNumbersField.tsx:16— “다른 카드 번호 칸의 오류를 현재 칸 입력이 덮어쓰고 있습니다.” - PR #515
CardNumbersField.tsx:52— “통과 분기에서setErrorStatus(null)이 없어 이전 에러가 남을 수 있습니다.” - PR #519
InfoInputSection.tsx:41— “전역 에러 상태 집계가 덮어쓰기 구조입니다.”
리뷰어 5명이 거의 같은 문장으로 같은 안티패턴을 짚고 있다.
비슷하게 따라오는 신호
이 안티패턴이 있는 PR에는 종종 함께 나타나는 신호가 있다. 카드를 따로 만들지 않고 여기 정리한다.
prev미사용 setter (4건/25) —setFormValue({ ...formValue, ... })형태로 클로저 캡처. PR #511 PR #513 PR #515 PR #520 . 인용: PR #515 JUDONGHYEOK — “setFormValue((prev) => ...)대신 현재 formValue를 클로저로 잡은 이유가 있을까요?”touched/dirty누락 — 입력 시작 전에도 에러가 보인다 (3건/25) — onBlur 또는 최소 자릿수 도달 같은 이벤트로 분리하지 않고 onChange 직후 검증하는 패턴. PR #498 JUDONGHYEOK — “isTouched 패턴이 다른 필드에도 반복되고 있는데요!” / PR #517 cys4585 — “각 칸에 숫자만 들어가면 길이가 1자리든 3자리든 피드백이 뜨지” / PR #520 — 페어 본인 “실시간(onChange)으로 유효성 검사를 수행하여 에러 문구를 화면에 바로 노출”. 언제 에러를 보여줄지 가 어떤 에러인지 만큼 중요한 결정이다.- 검증 실패값이 그대로 state에 반영 —
setMonth(value)직후 검증해서 에러 표시만 함. PR #517 PR #520 PR #524 . - submit 기본 동작 미차단 —
<form onSubmit>또는preventDefault()누락. PR #500 PR #501 PR #505 PR #512 PR #515 PR #517 PR #521 . 빈도 9건 — 별도 카드로 빼지 않은 이유는 form 경계가 본 카드의 에러 흐름과 결이 다르고, 후속 회차에서 form 카드로 별도 작성할 가치가 있기 때문.
같은 “에러 흐름의 책임 위치를 명확히 하지 않은” 패밀리다.
토론 질문
코드를 보면서 페어와 함께 이야기해보자.
- 카드번호 4칸의 에러는 서로 같이 변하는가? 같이 변하지 않는다면 한 변수로 묶은 이유는?
- “한 칸의 입력이 들어왔을 때, 어떤 에러가 사라져야 하고 어떤 에러가 유지되어야 하는가?”를 명시할 수 있는가?
setErrorStatus(null)을 통과 분기에 추가하면 — 다른 칸의 에러 표시는 어떻게 될까?
연관 카드
- Q5 — 검증과 에러 책임 — 같은 주제, 고민 관점
- Q2 — state 구조 — 묶을까 나눌까의 원리
- anti-04 — 파생값을 state로 저장 — 유효성 자체도 파생값
Last updated on




