셀렉터는 다 살아 있는데 자동화가 깨졌다, 요소가 아니라 단계가 옮겨갔을 때
화면 개편 뒤 가입 폼 자동화가 멈췄는데 셀렉터 13개 중 11개는 그대로였습니다. 원인은 입력칸, 팝업, 단계의 위치가 바뀐 것이었고, 자동화가 단계 진입 시점에 스스로 검증하도록 바꾼 과정을 정리했습니다.
여러 단계로 나뉜 가입 폼을 자동으로 채우는 도구가 화면 개편 직후부터 동작하지 않았습니다.
먼저 의심한 건 두 가지였습니다. 셀렉터가 바뀌었나, 아니면 화면이 리액티브 프레임워크로 다시 만들어져서 element.value = '...' 만으로는 상태가 안 잡히나.
둘 다 아니었습니다.
셀렉터는 멀쩡했습니다
자동화가 쓰는 id 13개 중 11개가 그대로 있었고 grep 으로 전부 찾아졌습니다. 값을 제출 시점에 DOM 에서 직접 읽는 구조라 프레임워크 문제도 아니었습니다.
그런데도 안 됐습니다. 원인은 세 가지였고, 전부 "무엇이 있느냐"가 아니라 "어디에 있느냐"의 문제였습니다.
입력칸이 다른 단계로 이사했습니다
회사명 입력칸이 뒷단계에서 앞단계로 옮겨졌고, 필수 입력이 됐습니다. 자동화는 여전히 뒷단계에서 채우고 있었죠.
증상이 고약했습니다. 검증에 걸리면 에러를 던지지 않고 조용히 멈춥니다.
if (isEmpty(companyName)) {
showFieldError(...);
return; // 예외도 없고 콘솔도 조용하다
}바깥에서 보면 "제출 버튼을 눌렀는데 아무 일도 안 일어남" 입니다. 뭐가 부족한지 알려 주는 신호가 없습니다.
팝업은 교체됐는데 옛 코드가 남아 있었습니다
약관 동의 팝업도 새것으로 바뀌면서 앞단계로 옮겨갔습니다. 그런데 옛 팝업 파일이 지워지지 않고 남아 있었습니다.
그래서 옛 id 를 grep 하면 멀쩡히 나옵니다. "살아 있네" 하고 넘어가기 쉽습니다. 하지만 그 컴포넌트를 만드는 코드가 어디에도 없는 죽은 파일이라, 화면에 절대 뜨지 않는 id 를 계속 기다리고 있었던 겁니다.
그래서 셀렉터가 살아 있는지는 정의만 찾으면 안 되고, 그걸 렌더하는 호출부까지 따라가야 합니다.
grep -r "id=\"targetElement\"" src/ # 정의: 있음
grep -r "new ThatComponent" src/ # 호출부: 없음 (죽은 코드)중간에 새 단계가 끼어들었습니다
설문 화면이 새로 생겼습니다. 조건 분기 없이 항상 지나는 경로인데, 자동화는 다음 단계로 바로 갈 거라고 가정하고 있었습니다.
왜 이런 실패는 진단이 어려울까요?
자동화가 기대는 건 두 가지입니다. 하나는 요소의 존재로, 셀렉터와 grep 으로 확인됩니다. 다른 하나는 화면의 구조로, 어느 단계에 무엇이 있고 무엇이 필수이며 어떤 순서로 지나가는지입니다.
깨지면 사람은 반사적으로 첫 번째를 확인합니다. 도구가 있으니까요. 그런데 개편에서 실제로 바뀌는 건 대개 두 번째입니다. 컴포넌트를 재배치할 때 id 는 그대로 들고 다니기 때문입니다.
확인하기 쉬운 쪽이 전부 통과라서, 정작 원인인 쪽을 안 보게 됩니다.
자동화가 스스로 막아야 합니다
요소가 아니라 단계를 검증합니다
각 단계에 진입할 때 그 단계에 있어야 할 요소 집합을 먼저 확인하고, 하나라도 없으면 무엇이 없는지 남기면서 즉시 멈춥니다. 조용히 건너뛰면 실패 지점이 한참 뒤로 밀립니다.
const REQUIRED = {
profile: ['#name', '#password', '#companyName'],
companyInfo: ['#companySector', '#expectedMembers'],
};
function assertStep(step) {
const missing = REQUIRED[step].filter(sel => !document.querySelector(sel));
if (missing.length) throw new Error(`[${step}] 요소 없음: ${missing.join(', ')}`);
}모르는 단계를 만나면 이름을 남깁니다
핸들러에 없는 단계에 도착하면 그냥 통과시키지 말고 단계 이름을 남깁니다. 새 단계가 끼어든 걸 다음 개편 때 바로 알 수 있습니다.
const handler = handlers[step];
if (!handler) {
console.warn(`[자동화] 처리기 없는 단계: ${step}`);
return;
}로그에는 다음 행동까지 적습니다
사실 위 로그를 이미 남겨 뒀는데도 아무도 안 봤습니다. 낯선 화면을 만난 사람은 화면에 보이는 문구를 단서로 삼아 물어보러 갔고, 정작 화면을 특정해 주는 단계 이름은 콘솔에 찍힌 채 방치됐습니다. 그 문구는 코드에 없는 값이라(번역을 서버에서 관리했습니다) 역추적에 한참 걸렸습니다.
정보는 다 있었습니다. 없었던 건 그걸로 무엇을 하면 되는지였습니다.
console.warn(
`[자동화] 처리기 없는 단계: ${step}\n` +
` 이 화면도 자동화하려면 이 줄을 그대로 전달하세요(단계 이름이 곧 화면 식별자입니다).`
);로그는 남기는 게 목적이 아니라 읽히는 게 목적입니다. 읽을 사람이 코드를 모르는 QA 나 운영이라면 더 그렇습니다.
이름 규칙은 추론하지 않습니다
한 단계에서 companyInfoInput 의 컨테이너가 companyInfoInputSection 인 걸 보고 "<단계이름>Section 규칙"이라고 단정했습니다. 그런데 다른 단계는 userProfileInput 에 profileInputSection 이었습니다. 규칙이 아니라 우연이었던 거죠. 조합해서 만든 셀렉터는 빗나가도 조용합니다.
덧붙여 그 컨테이너 id 는 서로 다른 세 화면이 공유하고 있었습니다. 컨테이너가 있다고 해서 지금 그 화면이라고 판정하면 안 됩니다. 화면 판별은 라우팅 상태 같은 단일 출처로 하고, 컨테이너는 검색 범위를 좁히는 용도로만 씁니다.
선택지는 화면에서 읽습니다
드롭다운 값을 코드에 박지 말고 실제 <option> 중에서 고릅니다. 원하는 값이 있으면 쓰고, 없으면 플레이스홀더가 아닌 첫 값으로 물러섭니다.
const pick = (select, preferred) => {
const opts = [...select.options].filter(o => o.value !== '');
return (opts.find(o => o.value === preferred) ?? opts[0])?.value;
};이런 목록은 지역(국내, 해외)에 따라 다르거나 외부 시스템 분류와 묶여 있어서, 내 사정과 무관하게 바뀝니다.
셀렉터가 전부 살아 있다는 사실은 아무것도 증명하지 못했습니다. 다음에 자동화가 멈추면 grep 보다 단계 순서와 필수값부터 볼 생각입니다.