휴대폰 인증 하나 붙였을 뿐인데

리드 플랫폼에서 리드의 품질은 전화번호 하나로 결정됩니다.

번호가 진짜여야 상담 연결이 되고, 상담이 연결돼야 파트너가 리드값을 인정하기 때문입니다.

그래서 옵티스랩과 자회사 리드 폼 전체에 휴대폰 인증 게이트를 붙였습니다.
인증을 완료해야 다음 단계로 넘어가는 구조입니다.

기능 자체는 단순했습니다. 발송 웹훅, 검증 웹훅, 인증 플래그, 제출 차단.
쉽게 생각하고 방심한 곳에서 잦은 실수와 함정이 반복 됬습니다.

그리고 5개의 장애가 연달아 터졌습니다.

이 글은 그 원인들의 기록입니다.

같은 코드를 복붙하면 다른 버그가 생긴다

data: JSON.stringify({ "phone": phoneNumber, "site": "" }),

복붙 과정에서 사이트명이 지워진 채 배포된 겁니다.

코드는 정상 동작하고 문자도 갑니다.
다만 어느 사이트에서 온 인증인지 식별이 안 될 뿐입니다.

이런 버그는 에러를 안 던지기 때문에 데이터가 쌓인 뒤에야 발견됩니다.

사이트별로 달라져야 하는 값은 코드 본문이 아니라 배포 체크리스트로 관리해야 합니다.

"복붙 후 바꿀 것: site 값, 필드명" 이 두 줄이 있었으면 안 겪었을 일입니다.

전화번호 형식은 하나가 아니다

가장 오래 발목을 잡은 문제입니다.
어느 날 인증 시트에 이런 번호가 찍혔습니다.

82821084573229 14자리. 존재하지 않는 번호

글로벌 진출을 목표로 하기 때문에 옵티스랩과 자회사들은 국가코드가 필수입니다.
그런데 어느날 국가코드 82가 두 번 붙었습니다.

원인은 두 시스템의 암묵적 전제가 어긋난 것이었습니다.

폼의 전화 필드는 국제 형식 입력을 지원하기 때문에 사용자가 010으로 입력해도 값이 +8210... 형태로 나올 수 있습니다.

그런데 발송 파이프라인의 변환 수식은 입력이 항상 010-XXXX-XXXX 하이픈 형식이라고 전제하고 앞자리를 자른 뒤 82를 붙입니다.

이미 82가 붙은 값에 82를 또 붙인 겁니다.

발송 시스템은 에러 없이 문자를 보냈고, 그 번호의 주인은 세상에 없으니 아무도 문자를 받지 못했습니다.

"인증번호가 안 와요"라는 증상의 뿌리가 형식 전제의 어긋남이었습니다.

해결은 형식을 통일하는 게 아니라, 어떤 형식이 들어와도 흡수하는 정규화 계층을 만드는 것이었습니다.

function cleanPhone(raw) {
  var digits = (raw || '').replace(/[^0-9]/g, '');
  if (digits.substring(0, 2) === '82') digits = '0' + digits.substring(2);
  if (digits.length < 10 || digits.length > 11) return null;
  if (digits.substring(0, 2) !== '01') return null;

  var mid = digits.length === 11 ? 4 : 3;
  return digits.substring(0, 3) + '-'
       + digits.substring(3, 3 + mid) + '-'
       + digits.substring(3 + mid);
}

국제 필드를 끄는 선택지도 있었지만 유지했습니다.

해외 진출을 준비하는 입장에서 국가코드 입력은 언젠가 필수가 되고, 그때 폼을 다시 뜯는 것보다 정규화 함수에 국가 분기를 추가하는 쪽이 싸기 때문입니다.

입력단은 유연하게, 저장단은 엄격하게.

어제 온 리드가 오늘 매칭됐다? 시계가 틀렸다

미스터리였던 증상이 있습니다.
전날 오후 4시대에 신청된 리드가 다음 날에야 매칭된 것처럼 보였습니다.

동기화 지연을 의심하고 자동화 히스토리를 뒤졌는데 실행은 전부 정상이었습니다.

답은 워드프레스 설정 한 줄에 있었습니다.

시간대: UTC+0

리드는 사실 밤 1시 32분(KST)에 들어왔는데, 시스템이 UTC로 기록해서 전날 16:32처럼 보였던 겁니다.

16:32 GMT는 새벽 1:32 KST니까 지연은 없었고 시계가 9시간 뒤에 있었을 뿐입니다.

무서운 건 이 설정이 건드리는 범위입니다.

일부 자회사의 시간 표기를 대한민국 서울로 바꾸지 않은 실수 때문이였습니다.

신청 시각 표기만이 아니라 날짜 기반으로 분기하는 모든 로직, 가격 정책 전환일이나 환불 기준일 계산까지 전부 9시간씩 밀려 있었습니다.

스택에 시계는 하나가 아닙니다.

CMS, 자동화 도구, 스프레드시트, 스크립트 런타임 넷 다 같은 시간대로 맞추고, 테스트 리드 하나로 전 구간의 타임스탬프가 일치하는지 검증하는 것까지가 작업입니다.

캡차가 사람을 거부한 이유

hCaptcha를 사이트 수에 맞춰 다섯 개 신청했는데, 한 사이트에서만 계속 인증에 실패했습니다. 체크해도 "인증 실패"가 뜨는 상황이었습니다.

원인은 제가 같은 키를 두 사이트에 붙여놓은 것이었습니다.

각 사이트에 맞는 키를 넣어야 하는데 복사하는 과정에서 하나를 중복으로 쓴 겁니다.

문제는 이게 테스트에서는 안 잡혔다는 점입니다.

사이트를 하나씩 열어 확인할 때는 정상 동작했고, 실무에 올라가서 두 사이트가 같은 키로 동시에 요청을 보내기 시작하자 그때 터졌습니다.

앞의 사이트명 누락과 같은 종류의 사고입니다.

코드는 정상이고 문법 에러도 없으며, 단지 사이트마다 달라야 할 값이 같았을 뿐입니다.
이런 건 단위 테스트로는 안 잡히고 여러 곳이 동시에 도는 상황에서만 드러납니다.

그래서 배포 체크리스트에 키 항목을 추가했습니다.
사이트마다 고유해야 하는 값이 무엇인지 목록으로 두고 배포 전에 하나씩 대조하는 방식입니다.

매칭 키로 쓸 수 없는 것들

인증 기록을 파트너 데이터와 대조하는 과정에서 같은 종류의 문제를 하나 더 만났습니다.

파트너 정보를 시트에서 찾아 갱신하는 자동화가 "필수 파라미터 없음" 에러로 멈췄는데, 원인은 앞 단계의 검색이 0건이었던 것입니다.

검색 키가 이메일과 담당자명 조합이었는데, 이메일은 오타와 대소문자에 깨지고 이름은 띄어쓰기 하나에 깨집니다.

키를 사업자등록번호 하나로 바꿨습니다.

고유하고, 형식 변형이 없고, 사람이 다르게 쓸 여지가 없는 값이기 때문입니다.

매칭 키의 조건은 "그 사람을 잘 설명하는 값"이 아니라 "절대 두 가지 방식으로 표기될 수 없는 값"입니다.

그리고 검색 0건일 때의 처리, 그러니까 신규 등록이라 행이 원래 없는 경우를 분기하지 않으면 키를 뭘로 바꿔도 같은 에러가 재발합니다.

검색해서 있으면 갱신하고 없으면 추가하는 라우팅은 키 설계와 한 세트입니다.

클라이언트 검증은 방어가 아니다

인증 게이트를 다 붙이고 나서 자문했습니다. 이제 캡차 없어도 봇을 막을 수 있나.

아니었습니다.

인증 게이트는 브라우저에서 도는 자바스크립트이고 봇은 버튼을 누르지 않습니다.
제출 엔드포인트에 직접 요청을 쏘면 게이트는 존재조차 모릅니다.

그래서 진짜 방어선은 서버 쪽에 뒀습니다.

리드 저장 파이프라인이 저장 직전에 해당 번호의 인증 완료 여부를 대조하고, 기록이 없으면 저장하지 않는 구조입니다.

문자 인증은 봇이 위조할 수 없는 관문이라, 이 검문 하나가 클라이언트의 어떤 장치보다 단단합니다.

프론트의 게이트는 UX이고 방어는 서버에서 합니다.

디버깅 고통 속 얻은 것은

다섯 장애의 공통점은 하나입니다. 코드는 전부 정상 동작했다는 것.

에러를 던진 건 마지막 하나뿐이고 나머지는 조용히 잘못된 데이터를 쌓았습니다.
형식이 어긋난 번호로 문자를 보내고, 9시간 밀린 시각을 기록하고, 빈 사이트명을 저장하면서요.

시스템이 무너지는 방식은 대부분 크래시가 아니라 이런 조용한 어긋남입니다.

그래서 기능을 붙인 뒤의 검증은 "동작하는가"가 아니라 "데이터가 끝까지 맞는 형태로 도착하는가"를 봐야 합니다.

폼에서 시트까지, 시트에서 매칭까지, 매칭에서 알림까지 전 구간을 한 번에.

profile
옵티스랩 리드 생성 인프라 SaaS 기업 CEO

0개의 댓글