개발 slecs

핀코드 자릿수 변경이 표시·검증·CSS까지 번진 이야기

목차

처음엔 핀코드 자릿수만 바꾸면 끝날 줄 알았음. 12자리에서 16자리로. 유틸 한 줄 수정하고 커피 마실 작정이었는데 그게 함정이었음.

사실 이런 작업을 "단순 상수 변경"으로 분류하는 순간부터 이미 위험 신호다. 스칼라 값처럼 보이는 것들이 코드베이스 곳곳에 조용히 흘러들어가 있는 경우가 많고, 그게 드러나는 건 항상 변경 후다. 이번에도 딱 그랬음.

숨어있던 호출 지점들

생성 유틸 한 곳만 보면 될 것 같았는데, 추적해보니 같은 값이 표시·검증·전송·저장 네 축에 다 박혀있었음.

영역 기존 변경
생성 12자리 랜덤 16자리 랜덤
주문 완료 화면 4-4-4 그룹 4-4-4-4 그룹
검증 정규식 ^\d{12}$ ^\d{16}$
마스킹 앞 4 노출 앞 4 + 뒤 4 노출
발송 메시지 템플릿 길이 가정 12 길이 가정 16

각 축이 서로 다른 파일, 심지어 다른 레이어에 있으니까 grep 한 번으로는 잘 안 잡힘. 생성 쪽 유틸을 고치면 검증 정규식은 자동으로 따라오지 않는다. 마스킹 로직은 또 별도 헬퍼에 있고, 발송 메시지 템플릿은 그 어디에도 상수를 참조하지 않고 그냥 숫자를 문자열로 박아뒀음. 이런 게 "묵시적 결합"이다. 인터페이스 계약이 없어서 컴파일러도 테스트도 잡아주지 못함.

특히 주문 완료 페이지에서 핀코드 그룹을 보여주는 박스 너비가 12자리 기준 고정 픽셀로 박혀 있었음. 16자리 넣으니까 줄바꿈되면서 한 그룹이 다음 줄로 떨어짐. 이런 건 단위 테스트로 절대 안 잡힘. JS 단에서 값이 맞게 나와도, 렌더링 결과는 브라우저 열어봐야 알 수 있음.

CSS 쪽 얘기를 좀 더 하면, width: 280px 같은 식으로 박혀 있는 컨테이너를 만나면 그건 거의 항상 특정 데이터 길이를 머릿속에 가정하고 쓴 값이다. 그 가정이 어딘가 명시적으로 문서화되는 경우는 드물고, 변경 시점에 비로소 존재가 드러남. 이번처럼. min-content 기준으로 흘러가게 하거나, 그룹 개수에 따라 동적으로 너비를 계산하게 해두는 게 맞는데, 처음부터 그렇게 짜지 않으면 나중에 자릿수 하나 바뀔 때마다 CSS 파일을 열어야 하는 상황이 됨.

그룹 분리 함수도 마찬가지임. 16은 4로 딱 나뉘니까 이번엔 괜찮았지만, 함수 내부에 "길이가 그룹 크기의 배수가 아닐 때" 처리가 없으면 다음번 변경에서 터진다. 빈 그룹이나 짧은 그룹이 렌더링 시 깨지는 건 예상하기 쉬운 케이스라, 가드를 추가해뒀음.

// 수정 전 - 16이 딱 맞아서 우연히 통과했던 버전
function groupPincode(pin, size = 4) {
  const chunks = [];
  for (let i = 0; i < pin.length; i += size) {
    chunks.push(pin.slice(i, i + size));
  }
  return chunks;
}

// 수정 후 - 빈 chunk 방어
function groupPincode(pin, size = 4) {
  const chunks = [];
  for (let i = 0; i < pin.length; i += size) {
    const chunk = pin.slice(i, i + size);
    if (chunk.length > 0) chunks.push(chunk);
  }
  return chunks;
}

지금 보면 별 차이 없어 보이지만, 이 가드가 없으면 입력값 길이가 size의 배수가 아닌 상황에서 마지막 빈 문자열이 배열에 들어가고, 그걸 렌더링하면 구분자가 하나 더 붙는 등 미묘하게 깨짐.

"최종" 다음에 "최최종"이 오는 이유

스스로 끝났다 싶을 때마다 Codex 검증 돌리는 습관이 있음. 이번엔 세 라운드 돌았음.

[1차] "다 됐다"    → Codex: 정규식 12 남음
[2차] "이번엔 진짜" → Codex: 분리 함수 가드 빠짐
[3차] "최종"       → Codex: 화면 너비 안 맞음
[4차] "최최종"     → 통과

커밋 메시지에 "최최종"이라고 적는 순간 자존심이 좀 꺾이지만, 안 적었으면 누구도 이 자릿수 변경에 화면 깨짐이 묶여 있는 줄 몰랐을 것임.

자기 검증의 한계는 자기 만족도와 비례한다는 게 이번에 다시 체감됨. 본인이 "이 정도면 됐다"고 느낀 상태에서 하는 리뷰는 이미 찾고 싶은 버그를 찾는 방향으로 흘러가기 쉬움. 보고 싶지 않은 결함은 눈에 잘 안 들어옴. 외부 시점이 들어와야 만족 너머의 결함이 보이는 이유가 거기 있음.

라운드를 거듭할수록 잡히는 버그의 종류가 달라지는 것도 흥미로웠음. 1차는 로직 버그, 2차는 엣지케이스, 3차는 UI 렌더링. 각 라운드가 다른 레이어를 커버하는 셈이었는데, 이걸 처음부터 레이어별로 체크리스트를 나눠서 접근했다면 라운드가 줄었을 수도 있었음.

그래서 다음엔 어떻게 할 건가

이번 작업이 남긴 실용적인 결론은 몇 가지로 정리됨.

  • 자릿수 같은 스칼라 상수도 표시·검증·전송·저장 네 축으로 영향 그래프를 그려야 함. 상수 하나가 어떤 레이어에 어떻게 흘러가는지 먼저 파악하고 나서 손대는 것.
  • "한 줄 수정"이 정말 한 줄로 끝나는 경우는 거의 없음. 호출 지점 grep을 먼저 깔고 시작하기. 특히 하드코딩된 리터럴 값(12, \d{12} 같은 것들)은 상수명으로 grep이 안 되니까 값 자체로 따로 한 번 더 검색해야 함.
  • CSS도 변경 영향범위에 포함시키기. 기능 변경이 레이아웃에 닿는지 안 닿는지는 데이터 길이나 그룹 수가 바뀌는 순간 항상 확인 대상임.
  • 커밋 메시지에 "최종" 쓰지 말고 그냥 버전 번호 박는 게 정신건강에 좋음. pincode-v2, pincode-16d 같은 식으로.

단순해 보이는 변경일수록 조심해야 한다는 걸 또 배웠음. 복잡한 기능 추가는 처음부터 긴장하고 시작하는데, "이건 간단하다"는 느낌이 오히려 긴장을 풀게 만들어서 더 위험한 것 같기도 함.

댓글 0

첫 댓글 달아줘.