컨테이너 이름 예약어 검증 버그
컨테이너 만들 때 이름에 “workspace”같은 예약어를 못쓰게 막는 검증 로직이 있는데, 이게 생성할 때랑 이름변경할 때 동작이 다르다는 제보를 받아서 파보게 되었다. 같은 검증인데 왜 한쪽만 이상하게 동작하는지 궁금했다.
증상
이름변경(rename) 플로우에서 예약어를 넣으면 “workspace는 사용할 수 없는 이름입니다” 같은 안내 토스트가 정상적으로 뜬다. 그런데 컨테이너를 새로 만들 때(create) 같은 이름으로 시도하면 안내 문구 대신 “알 수 없는 오류가 발생했습니다” 같은 뜬금없는 토스트가 뜨거나, 심하면 문구 중간에 플레이스홀더가 깨진 채로 노출된다.
같은 검증 로직을 타는데 왜 결과가 다를까 싶어서 두 플로우의 코드를 나란히 봤다.
두 플로우 비교
이름변경 쪽은 구조가 단순하다. 이름 입력 컴포넌트에서 validateName(name)을 클라이언트에서 바로 실행하고, 실패하면 그 자리에서 catch해서 err.code로 메시지를 조회해 띄운다. 서버 왕복이 없다.
생성 쪽은 두 단계다. 먼저 클라이언트에서 똑같이 사전검증을 하고(이것도 err.code를 정상적으로 쓴다), 여기를 통과하면 소켓으로 서버에 생성 요청을 보낸다. 서버가 다시 한번 검증해서 실패하면 에러를 소켓 이벤트로 돌려주는데, 이걸 받는 핸들러가 문제였다.
const handleMakeErrorSocket = async (err) => {
if (err.errorCode === 'GIS_500' && typeof window !== 'undefined') {
window.location.reload();
return;
}
errorToast(
localUtil.getMsgIdByErrorCode(CONTAINER_NEW.MAP_ERR_CODE_TO_MSG_ID, err.errorCode),
{ br: <br /> },
);
};
서버가 실제로 내려주는 에러 객체 필드는 code인데, 이 핸들러는 err.errorCode를 읽고 있다. 그러니 err.errorCode는 항상 undefined고, getMsgIdByErrorCode는 매핑을 못 찾아서 fallback 메시지(“Common.error.unknown”)로 빠진다. 게다가 errorToast에 넘기는 두번째 인자도 err.data를 spread 안 해서 {excludedNames} 같은 플레이스홀더가 채워지지도 않는다.
재밌는 건 같은 파일에 있는 GPU 에러 핸들러(비슷한 역할을 하는 형제 핸들러)는 err?.errorCode || err?.code처럼 fallback이 들어가 있었다는 거다. 딱 이 핸들러만 fallback이 빠져 있었다.
git blame으로 추적
언제부터 이랬는지 궁금해서 blame을 찍어봤다.
이 예약어 검증 로직 자체는 꽤 오래전에(수년 전) 도입된 거였고, 그때부터 지금까지 로직 자체는 안 바뀌었다. 문제는 그보다 한참 뒤, 몇 년 전 커밋에서 생겼다. 커밋 메시지를 보니 “컨테이너 개수 초과 상태에서 이미지팩으로 생성 시 알 수 없는 오류 토스트”를 고치려던 커밋이었는데, 그 과정에서 err.code === 'GIS_500'를 err.errorCode === 'GIS_500'로 바꾸고, 메시지 조회하는 부분도 같이 err.errorCode로 바꿔버렸다. 특정 케이스 하나(GIS_500) 고치려다가 같은 함수 안의 다른 조건까지 필드명을 통째로 바꿔버린 셈이다.
그 뒤로 이 파일이 클래스 컴포넌트에서 함수형 컴포넌트로 전면 재작성되는 리팩터링을 한 번 더 거쳤는데, 이때도 이 버그는 그대로 복사되어 넘어왔다. 리팩터링 시점에 GPU 핸들러 쪽엔 이미 fallback이 있었으니 나란히 비교했으면 눈에 띄었을 것 같은데, 넘어간 것 같다. 그 이후로 지금까지 다시 손댄 흔적이 없었다.
몇 년 동안 아무도 못 알아챈 이유도 짐작이 간다. 실제로 이 코드까지 도달하려면 클라이언트 사전검증을 통과해야 하는데, 생성 버튼 자체가 사전검증에서 예약어면 비활성화되도록 되어 있어서 일반적인 사용 흐름에서는 서버까지 요청이 안 간다. 템플릿이나 자동생성 이름처럼 클라이언트 검증을 우회하는 경로로 들어와야 이 버그를 만나게 된다. 흔한 경로가 아니라서 오래 묻혀있었던 것 같다.
결국 원인은 하나였다. 어떤 필드는 code로, 어떤 필드는 errorCode로 부르고 있었고, 두 이름이 뒤섞이면서 한쪽 핸들러가 조용히 망가진 거다. 타입 체크나 빌드가 이걸 못 잡아준 것도 아쉬운 부분인데, 자바스크립트다보니 err.errorCode가 undefined여도 에러 없이 그냥 통과해버린다. 그래서 겉보기엔 멀쩡하게 동작하는 것처럼 보이는 거고.
형제 핸들러(GPU 쪽)에 이미 fallback 패턴이 있었다는 게 제일 아쉬웠다. 비슷한 역할을 하는 코드가 옆에 있으면 한번 비교해보는 습관이 있었으면 리팩터링 때라도 잡혔을 수도 있었겠다 싶다. 비슷하게 생긴 핸들러라도 각자 기대하는 필드 계약이 다를 수 있다는 걸, 그리고 그 계약이 어긋나면 에러조차 안 나고 그냥 조용히 다른 결과를 보여준다는 걸 새삼 느꼈다.