Skip to Content
페이먼츠 미션1단계 안티패턴anti-01. 한 칸 에러가 다른 칸을 덮어쓴다

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 카드로 별도 작성할 가치가 있기 때문.

같은 “에러 흐름의 책임 위치를 명확히 하지 않은” 패밀리다.

토론 질문

코드를 보면서 페어와 함께 이야기해보자.

  1. 카드번호 4칸의 에러는 서로 같이 변하는가? 같이 변하지 않는다면 한 변수로 묶은 이유는?
  2. “한 칸의 입력이 들어왔을 때, 어떤 에러가 사라져야 하고 어떤 에러가 유지되어야 하는가?”를 명시할 수 있는가?
  3. setErrorStatus(null)을 통과 분기에 추가하면 — 다른 칸의 에러 표시는 어떻게 될까?

연관 카드

Last updated on