---
title: "셀렉터는 다 살아 있는데 자동화가 깨졌다, 요소가 아니라 단계가 옮겨갔을 때"
description: "화면 개편 뒤 가입 폼 자동화가 멈췄는데 셀렉터 13개 중 11개는 그대로였습니다. 원인은 입력칸, 팝업, 단계의 위치가 바뀐 것이었고, 자동화가 단계 진입 시점에 스스로 검증하도록 바꾼 과정을 정리했습니다."
date: 2026-11-06
updated: 2026-11-06T05:09:59.319Z
tags: [e2e, test-automation, debugging, frontend]
canonical: https://blog.wooncloud.com/posts/automation-broke-selectors-alive-step-moved
---

여러 단계로 나뉜 가입 폼을 자동으로 채우는 도구가 화면 개편 직후부터 동작하지 않았습니다.

먼저 의심한 건 두 가지였습니다. 셀렉터가 바뀌었나, 아니면 화면이 리액티브 프레임워크로 다시 만들어져서 `element.value = '...'` 만으로는 상태가 안 잡히나.

둘 다 아니었습니다.

## 셀렉터는 멀쩡했습니다

자동화가 쓰는 id 13개 중 **11개가 그대로** 있었고 `grep` 으로 전부 찾아졌습니다. 값을 제출 시점에 DOM 에서 직접 읽는 구조라 프레임워크 문제도 아니었습니다.

그런데도 안 됐습니다. 원인은 세 가지였고, 전부 "무엇이 있느냐"가 아니라 "어디에 있느냐"의 문제였습니다.

### 입력칸이 다른 단계로 이사했습니다

회사명 입력칸이 뒷단계에서 앞단계로 옮겨졌고, 필수 입력이 됐습니다. 자동화는 여전히 뒷단계에서 채우고 있었죠.

증상이 고약했습니다. 검증에 걸리면 에러를 던지지 않고 조용히 멈춥니다.

```js
if (isEmpty(companyName)) {
    showFieldError(...);
    return;          // 예외도 없고 콘솔도 조용하다
}
```

바깥에서 보면 "제출 버튼을 눌렀는데 아무 일도 안 일어남" 입니다. 뭐가 부족한지 알려 주는 신호가 없습니다.

![셀렉터가 모두 살아 있어도 입력칸이 다른 단계로 옮겨가면 자동화는 조용히 실패한다.](/images/posts/automation-broke-selectors-alive-step-moved/8d91792f-a4c0-421b-91b4-5a05fafe58ab.webp)

### 팝업은 교체됐는데 옛 코드가 남아 있었습니다

약관 동의 팝업도 새것으로 바뀌면서 앞단계로 옮겨갔습니다. 그런데 옛 팝업 파일이 지워지지 않고 남아 있었습니다.

그래서 옛 id 를 `grep` 하면 멀쩡히 나옵니다. "살아 있네" 하고 넘어가기 쉽습니다. 하지만 그 컴포넌트를 만드는 코드가 어디에도 없는 죽은 파일이라, 화면에 절대 뜨지 않는 id 를 계속 기다리고 있었던 겁니다.

그래서 셀렉터가 살아 있는지는 정의만 찾으면 안 되고, 그걸 렌더하는 호출부까지 따라가야 합니다.

```bash
grep -r "id=\"targetElement\"" src/        # 정의: 있음
grep -r "new ThatComponent" src/           # 호출부: 없음 (죽은 코드)
```

### 중간에 새 단계가 끼어들었습니다

설문 화면이 새로 생겼습니다. 조건 분기 없이 항상 지나는 경로인데, 자동화는 다음 단계로 바로 갈 거라고 가정하고 있었습니다.

## 왜 이런 실패는 진단이 어려울까요?

자동화가 기대는 건 두 가지입니다. 하나는 요소의 존재로, 셀렉터와 `grep` 으로 확인됩니다. 다른 하나는 화면의 구조로, 어느 단계에 무엇이 있고 무엇이 필수이며 어떤 순서로 지나가는지입니다.

깨지면 사람은 반사적으로 첫 번째를 확인합니다. 도구가 있으니까요. 그런데 개편에서 실제로 바뀌는 건 대개 두 번째입니다. 컴포넌트를 재배치할 때 id 는 그대로 들고 다니기 때문입니다.

확인하기 쉬운 쪽이 전부 통과라서, 정작 원인인 쪽을 안 보게 됩니다.

![회사명 입력칸이 앞단계로 옮겨졌는데 자동화가 뒷단계에서 채우면 검증이 조용히 return 해 아무 신호가 없고, 단계 진입 시점에 필수 요소를 검사하면 없는 요소 이름과 함께 즉시 실패한다.](/images/posts/automation-broke-selectors-alive-step-moved/6dc0d1de-4861-42d3-a00c-3f6341936507.svg)

## 자동화가 스스로 막아야 합니다

### 요소가 아니라 단계를 검증합니다

각 단계에 진입할 때 그 단계에 있어야 할 요소 집합을 먼저 확인하고, 하나라도 없으면 무엇이 없는지 남기면서 즉시 멈춥니다. 조용히 건너뛰면 실패 지점이 한참 뒤로 밀립니다.

```js
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(', ')}`);
}
```

### 모르는 단계를 만나면 이름을 남깁니다

핸들러에 없는 단계에 도착하면 그냥 통과시키지 말고 단계 이름을 남깁니다. 새 단계가 끼어든 걸 다음 개편 때 바로 알 수 있습니다.

```js
const handler = handlers[step];
if (!handler) {
  console.warn(`[자동화] 처리기 없는 단계: ${step}`);
  return;
}
```

### 로그에는 다음 행동까지 적습니다

사실 위 로그를 이미 남겨 뒀는데도 아무도 안 봤습니다. 낯선 화면을 만난 사람은 화면에 보이는 문구를 단서로 삼아 물어보러 갔고, 정작 화면을 특정해 주는 단계 이름은 콘솔에 찍힌 채 방치됐습니다. 그 문구는 코드에 없는 값이라(번역을 서버에서 관리했습니다) 역추적에 한참 걸렸습니다.

정보는 다 있었습니다. 없었던 건 그걸로 무엇을 하면 되는지였습니다.

```js
console.warn(
  `[자동화] 처리기 없는 단계: ${step}\n` +
  `  이 화면도 자동화하려면 이 줄을 그대로 전달하세요(단계 이름이 곧 화면 식별자입니다).`
);
```

로그는 남기는 게 목적이 아니라 읽히는 게 목적입니다. 읽을 사람이 코드를 모르는 QA 나 운영이라면 더 그렇습니다.

### 이름 규칙은 추론하지 않습니다

한 단계에서 `companyInfoInput` 의 컨테이너가 `companyInfoInputSection` 인 걸 보고 "`<단계이름>Section` 규칙"이라고 단정했습니다. 그런데 다른 단계는 `userProfileInput` 에 `profileInputSection` 이었습니다. 규칙이 아니라 우연이었던 거죠. 조합해서 만든 셀렉터는 빗나가도 조용합니다.

덧붙여 그 컨테이너 id 는 서로 다른 세 화면이 공유하고 있었습니다. 컨테이너가 있다고 해서 지금 그 화면이라고 판정하면 안 됩니다. 화면 판별은 라우팅 상태 같은 단일 출처로 하고, 컨테이너는 검색 범위를 좁히는 용도로만 씁니다.

### 선택지는 화면에서 읽습니다

드롭다운 값을 코드에 박지 말고 실제 `<option>` 중에서 고릅니다. 원하는 값이 있으면 쓰고, 없으면 플레이스홀더가 아닌 첫 값으로 물러섭니다.

```js
const pick = (select, preferred) => {
  const opts = [...select.options].filter(o => o.value !== '');
  return (opts.find(o => o.value === preferred) ?? opts[0])?.value;
};
```

이런 목록은 지역(국내, 해외)에 따라 다르거나 외부 시스템 분류와 묶여 있어서, 내 사정과 무관하게 바뀝니다.

셀렉터가 전부 살아 있다는 사실은 아무것도 증명하지 못했습니다. 다음에 자동화가 멈추면 `grep` 보다 단계 순서와 필수값부터 볼 생각입니다.
