내 에이전트는 드롭다운을 볼 수는 있었지만, 아무 것도 선택하지 못했다.

발행: (2026년 5월 24일 PM 05:41 GMT+9)
9 분 소요
원문: Dev.to

출처: Dev.to

에이전트에게 리스트가 있었습니다. 아이템을 하나 골라 달라고 요청했지만 거절했습니다.
요소를 찾을 수 없다고 나오고, 새로 고침해도 같은 결과였습니다.
그래서 DevTools를 열어 다음을 붙여넣었습니다.

document.querySelector('select[name="status"]')
// null

null. 명백히 드롭다운이 존재하는 페이지였는데도 말이죠. 저는 드롭다운을 눈으로 확인하고, 클릭도 할 수 있었고, 마우스로 펼칠 수도 있었습니다. 그런데 document.querySelector는 존재하지 않는다고 고집했습니다.

이 이야기는 서로 대화하지 못하는 세 겹의 DOM과 Safari MCP 2.11.3 버전에서 safari_select_option이 그 사이를 잇기 위해 배운 교훈에 관한 것입니다.

페이지는 고객 포털에 삽입된 Salesforce/Lightning 지원 양식이었습니다. 포털이 부모 문서이고, 양식은 다른 (하지만 동일 출처) 호스트에서 제공되는 <iframe> 안에 있었습니다. 그 <iframe> 안에서 Lightning은 각각 자체 섀도우 루트를 가진 커스텀 엘리먼트들의 그래프를 조합해 UI를 구성합니다.

따라서 개발자가 Lightning에서 “Status” 드롭다운을 작성하면 실제 <select> 엘리먼트는 다음과 같은 구조 안에 렌더링됩니다.

top document
└── <iframe>
    └── <lightning-base-combobox>
        └── #shadow-root
            └── <div>
                └── #shadow-root
                    └── <select>    ← 에이전트가 필요로 했던 엘리먼트

document.querySelector('select[name="status"]')를 최상위 문서에서 호출하면 위 구조를 전혀 탐색하지 못합니다. <iframe>도, 섀도우 루트도 전혀 보지 못하니, 그 <select>는 존재하지 않는 셈이죠.

이 문제를 눈치채기 어렵게 만든 두 가지 요인은 다음과 같습니다.

  • safari_snapshot이 이를 포착했습니다. 에이전트가 페이지의 접근성 스냅샷을 찍었을 때, 드롭다운은 레퍼런스, 라벨, aria-expanded 상태, 옵션 등 모든 정보를 가지고 있었습니다. 빠진 것이 없었습니다.
  • safari_click은 정상적으로 동작했습니다. 저는 몇 주째 깊은 DOM 요소들을 클릭해 왔고, 그때는 전혀 생각하지 않았습니다. 같은 양식을 여는 버튼 자체가 다른 섀도우 루트 안에 있었지만 클릭은 문제없이 해결했습니다.

그래서 에이전트는 스스로에게 계속 물었습니다. “방금 이 폼을 클릭했는데, 드롭다운이 눈앞에 있는데 왜 아무것도 선택하지 못하지?”

답은 부끄럽게도 clickselect_option이 서로 다른 찾기 방식을 사용하고 있었기 때문이었습니다.

Safari MCP는 페이지 안에서 두 가지 엘리먼트 탐색 경로를 제공합니다.

  • mcpFindRef(ref)safari_snapshot에서 얻은 레퍼런스를 받아, 문서, 모든 동일 출처 <iframe>, 그리고 접근 가능한 모든 섀도우 루트를 순회해 해당 레퍼런스가 가리키는 엘리먼트를 찾습니다.
  • mcpQuerySelectorDeep(selector) — CSS 선택자를 받아 위와 같은 깊은 순회를 수행하지만, 레퍼런스 대신 선택자로 매칭합니다.

click은 오래전부터 이 두 함수를 모두 활용해 왔습니다. 그래서 Lightning 폼, React 컴포넌트 라이브러리, 포털에 렌더링되는 모달 대화상자에서도 “클릭만 하면” 동작했습니다.

반면 safari_select_option은 여전히 다음과 같이 단순히 한 줄만 실행하고 있었습니다.

var el = document.querySelector(sel);

최상위 프레임만 살피고, <iframe>도 섀도우 루트도 전혀 고려하지 않았습니다. 일반적인 페이지의 일반적인 <select>라면 이 한 줄이면 충분했으며, 도구가 처음 만들어졌을 때부터 그대로였기에 아무도 건드리지 않았던 것이죠.

하지만 어느 날 사용자가 실제 Salesforce 포털에 Safari MCP를 투입하면서, 이 한 줄이 모든 호출에서 틀렸다는 것이 드러났습니다.

v2.11.3 패치는 작지만 핵심을 바꿉니다. safari_select_option에게 safari_click이 이미 알고 있던 방식을 가르쳐 줍니다.

let finder;
if (ref) {
  finder = `mcpFindRef('${ref}')`;
} else if (selector) {
  finder = `(document.querySelector('${sel}')||mcpQuerySelectorDeep('${sel}'))`;
}

두 가지 경로:

  • ref 경로safari_snapshot을 통해 얻은 레퍼런스에 대해 사용합니다. click과 동일한 깊은 탐색기를 통해 <iframe>, 섀도우 루트, 모든 중첩 컴포넌트를 해결합니다.
  • selector 경로 — 먼저 가벼운 최상위 프레임 쿼리를 시도하고(대부분 95% 경우에 정상), null이 반환되면 깊은 탐색기로 넘어갑니다.

도구의 나머지 부분—React의 제어 입력 bookkeeping을 깨우는 _valueTracker 리셋, 값이 아닌 텍스트로 표시되는 <select>를 위한 value‑then‑text‑then‑substring 매칭, input/change/blur 이벤트 시퀀스—는 그대로 유지됩니다. 깨진 부분은 오직 엘리먼트 조회뿐이었습니다.

전체 v2.11.3 릴리스 노트는 여기에서 확인할 수 있습니다.

솔직히 말하면, 저는 두 개의 찾기 함수를 가지고 있었고, click에서는 하나를, select_option을 구현할 때는 그 존재 자체를 잊어버렸습니다. 수정하는 데 한 시간도 안 걸렸고, 버그는 이미 3개월째 숨어 있었습니다.

제가 이 도구를 만들면서 계속 되새기는 교훈은 DOM 도구는 “행복 경로”를 가져서는 안 된다는 것입니다. 엘리먼트를 찾는 모든 도구는 서로 동일한 방식으로 찾아야 합니다. 페이지는 어느 도구가 다음에 호출될지 알 수 없기 때문이죠. 섀도우 루트 안을 클릭한 뒤 필드를 채우려 하면, 필드 채우기 도구도 클릭이 찾은 바로 그 위치를 찾아야 합니다.

safari_fill, safari_get_element, safari_hover, safari_get_computed_style 등은 모두 v2.x 시리즈 초기에 같은 마이그레이션을 거쳤으며, 버그 리포트 하나씩 해결해 왔습니다. safari_select_option만 마지막으로 감사를 받지 못했는데, v2.11.3이 그 격차를 메웠습니다.

브라우저 자동화 도구—MCP든 아니든—를 만들고 있다면, 오늘 밤 코드베이스에 다음 질문을 던져 보세요: “내 모든 엘리먼트 탐색 경로가 ‘그 엘리먼트’가 무엇인지에 대해 일치하고 있는가?” 일치하지 않을 때, 에이전트가 바로 그 사실을 깨닫게 될 테니까요.

Safari MCP는 MIT 라이선스로 GitHub에서 오픈 소스입니다. macOS에서 AI 에이전트를 위한 네이티브 브라우저 자동화—실제 Safari, 실제 로그인, Chrome 없이.

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.