개발하다 보면 에러 메시지는 늘 후순위로 밀린다. 기능이 돌아가면 일단 배포하고, 에러 문구는 나중에 다듬자는 생각이 팀 어딘가에 쌓인다. 하지만 사용자가 서비스에서 가장 먼저 마주치는 것은 코드가 아니라 텍스트다. 그리고 이제 그 텍스트를 읽는 독자가 한 명 더 늘었다. AI 에이전트다.
"오류가 발생했습니다"가 나쁜 이유
velog에 올라온 UX 라이팅 관련 글은 이 문제를 정확하게 짚는다. "오류가 발생했습니다."라는 문장을 본 사용자는 무엇을 알 수 있을까? 사실상 아무것도 없다. 무엇이 잘못됐는지, 내가 뭔가를 잘못한 건지, 다시 시도하면 되는 건지—아무런 단서가 없다. 만능 에러 메시지는 오히려 사용자의 다음 행동을 차단한다.
좋은 에러 메시지는 최소 세 가지를 담아야 한다. 무엇이 잘못됐는지, 왜 그런 건지, 다음에 뭘 해야 하는지. "로그인에 실패했습니다" 대신 "비밀번호가 일치하지 않습니다. 다시 확인해 주세요."로 바꾸면 사용자는 즉시 다음 행동을 결정할 수 있다. 에러 메시지의 목적은 상황을 설명하는 게 아니라, 사용자가 막히지 않고 앞으로 나아가게 돕는 것이다.
LINE의 사례: 원칙보다 사용자 이해가 먼저
같은 글에서 소개된 LINE의 사례가 흥미롭다. AR 기능을 테스트할 때 "평평한 곳을 비춰주세요"라는 안내 문구를 썼는데, 사용성 테스트에서 사용자들이 "평평한 곳이 정확히 어디를 말하는 건가요?"라고 되물었다. 개발자나 기획자에게는 자명한 문장이 실제 사용자에게는 모호했다. 결국 LINE은 '바닥'이나 '책상'처럼 구체적인 예시를 추가했고 이해도가 크게 올랐다.
LINE은 원칙적으로 지나치게 구체적인 예시 사용을 지양한다. 하지만 사용자가 처음 접하는 새로운 기능에서는 그 원칙을 유연하게 적용했다. 여기서 배울 점은 UX 라이팅에 절대적인 정답은 없다는 것이다. 기준은 항상 '사용자가 이 문장을 보고 다음 행동을 결정할 수 있는가'여야 한다.
AI 에이전트에게 에러 메시지는 세계의 전부다
dev.to에 올라온 글 "Your Error Messages Are an API Now"은 이 문제를 완전히 다른 각도에서 바라본다. 저자는 약 100개의 MCP 툴을 연결해 무감독으로 동작하는 에이전트를 운용하는데, 에이전트가 가장 많은 시간을 낭비하는 원인이 바로 "사람을 위해 쓰인 에러 메시지"라고 말한다.
사람은 에러를 만나면 주변을 살핀다. 어제 설정 파일을 바꿨는지 떠올리고, 서비스 상태 페이지를 확인하고, 동료에게 묻는다. 에러 메시지는 더 넓은 조사의 출발점일 뿐이다. 하지만 에이전트에게 에러 문자열은 종종 관측 가능한 세계의 전부다. 메시지가 잘못된 방향을 가리키면, 에이전트는 그것을 사실로 받아들이고 존재하지도 않는 문제를 자신 있게 해결하려 든다.
저자의 실제 경험이 이를 잘 보여준다. 브라우저 자동화 중 모든 페이지 로드가 NS_ERROR_UNKNOWN_HOST로 실패했다. example.com조차도. 셸에서는 모든 도메인이 정상적으로 resolve됐다. DNS 장애처럼 보였지만 진짜 원인은 SOCKS 프록시가 stale 상태였던 것—서비스 매니저에는 "active"로 표시된 채로 모든 아웃바운드 연결을 조용히 실패시키고 있었다. 브라우저는 정상 동작하는 도메인 이름을 에러 메시지에 담았다. 멀쩡하게 작동하는 컴포넌트가 에러의 이름이 된 것이다.
AI 에이전트를 위한 에러 메시지 설계 3원칙
저자는 툴 에러 메시지에 담겨야 할 세 가지를 제시한다. 기술적으로 어렵지 않지만, 대부분의 시스템이 하지 않는 것들이다.
첫째, 찾던 대상이 아니라 실패한 레이어를 명시하라. "unknown host: example.com"은 문제 없는 곳을 가리킨다. "upstream proxy 10.200.0.2:1080에 연결할 수 없음"은 조사를 즉시 끝낸다. MCP 서버가 데이터베이스, HTTP API, 캐시를 앞단에 두고 있다면 어느 것이 깨졌는지 말해야 한다. 에러는 호출자에게 내부를 들여다볼 수 있는 유일한 창이다.
둘째, 재시도가 의미 있는지 알려라. 에이전트가 불투명한 실패를 만났을 때 기본 행동은 재시도다. 잘못된 인자가 원인이라면 그 재시도는 순수한 낭비이며, 집요한 에이전트는 다섯 번이고 반복한다. "인자가 바뀌지 않으면 동일하게 실패함" 또는 "일시적 오류, 잠시 후 재시도 가능"—이 한 문장의 차이가 에이전트의 다음 30분을 결정한다.
셋째, 빈 결과와 실패를 구분하라. []와 "쿼리 실패"는 완전히 다른 상태다. 그런데 놀랍도록 많은 툴이 후자에 전자를 반환한다. 빈 리스트를 받은 에이전트는 "해당 항목이 존재하지 않는다"고 결론 내리고 넘어간다—때로는 나중에 다른 인스턴스가 사실로 읽을 메모에 그 내용을 기록하면서. 조용한 실패는 현재 호출만 망치는 게 아니라 에이전트의 메모리를 오염시킨다.
에러 메시지도 인터페이스다—그리고 인터페이스는 움직인다
Next.js 15 + Framer Motion 애니메이션 패턴을 다룬 dev.to 글은 다른 맥락의 이야기처럼 보이지만, 에러 UX와 연결되는 지점이 있다. 저자는 상태 변화(state change)에도 애니메이션이 필요하다고 강조한다. 단순히 요소가 등장할 때만이 아니라, pending → active → canceled처럼 상태가 바뀌는 순간에도 AnimatePresence로 전환을 처리해야 한다고.
에러 상태도 마찬가지다. "오류가 발생했습니다"가 갑자기 화면을 덮치는 것과, 부드럽게 등장하며 사용자의 시선을 자연스럽게 안내하는 것은 전혀 다른 경험이다. 에러 토스트, 인라인 에러, 상태 배지—이것들은 단순한 텍스트 컴포넌트가 아니라 상태 전환 UI다. Framer Motion의 AnimatePresence와 key prop을 활용하면 에러 메시지의 등장과 퇴장에 의도를 담을 수 있다. 단, 이 글이 강조하듯 useReducedMotion으로 모션 민감 사용자를 반드시 배려해야 한다.
클라이언트가 LLM이 된 세계에서 에러 메시지의 방향이 바뀐다
예전 원칙은 이랬다: 운영자에게는 상세한 로그를, 클라이언트에게는 짧은 에러 코드를. 클라이언트는 프로그램이고, 프로그램은 코드로 분기하니까. 하지만 이 원칙이 뒤집혔다. 이제 클라이언트는 언어 모델이다. ERR_7734로는 아무것도 할 수 없지만, 잘 쓰인 자연어 설명은 최고 대역폭의 인터페이스가 된다. 반대로 사람 운영자에게는 대시보드와 구조화된 로그, 트레이스가 있다.
저자의 조언은 간결하다. 에러 문자열을 "막 들어온 유능한 동료에게 쓰는 메모"처럼 작성하라. 무엇을 하고 있었는지, 어느 단계에서 실패했는지, 무엇이 바뀌어야 정상화되는지. 두 문장으로 충분하다. 그리고 이 조언은 사람 사용자에게도 그대로 통한다.
작은 문장 하나가 경험의 전부가 된다
버튼 하나, 팝업 제목 하나, 에러 메시지 하나—모두 사용자의 다음 행동에 영향을 준다. velog 글의 마무리가 정확하다. "사용자가 가장 먼저 만나는 것은 코드가 아니라 텍스트다." 기능을 구현하고 나서 에러 메시지를 붙이는 게 아니라, 에러 상태를 어떻게 안내할지를 기능 설계와 함께 고민해야 한다.
그리고 이제 그 에러 메시지를 읽는 독자가 사람만이 아닌 시대가 됐다. 사람에게는 다음 행동의 단서를, 에이전트에게는 탐색 공간의 지도를—에러 메시지 하나로 두 독자 모두를 위한 설계가 가능하다. 원칙은 사실 같다. 무엇이 잘못됐고, 어디서 잘못됐고, 무엇을 해야 하는가. 이 세 가지를 담는 것만으로도 에러 경험은 완전히 달라진다.