본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 23. 04:02

AI 에이전트의 이메일 답장을 올바른 스레드에 유지하는 방법

요약

AI 에이전트가 이메일 답장을 기존 대화 스레드에 올바르게 유지하기 위한 기술적 방법을 설명합니다. 제목 매칭 방식의 한계를 지적하고, Message-ID, In-Reply-To, References 헤더를 활용한 정확한 스레딩 구현법을 다룹니다.

핵심 포인트

  • 제목(Subject) 기반 매칭은 수정이나 중복 발생 시 실패할 위험이 높음
  • Message-ID, In-Reply-To, References 헤더가 스레딩의 핵심
  • Nylas API와 CLI를 활용한 이메일 대화 그룹화 구현 가능
  • 정확한 헤더 관리가 에이전트의 대화 연속성을 보장함

AI 에이전트가 이메일을 보내고 3시간 후에 답장이 도착했을 때, 에이전트가 유용한 작업을 수행하기 전에 먼저 답해야 할 두 가지 질문이 있습니다. 바로 '이것이 어떤 대화인가?'와 '내가 마지막으로 무엇이라고 말했는가?'입니다. 첫 번째 질문을 틀리면 에이전트의 답장은 기존 스레드에 끼워지는 대신 수신자의 편지함에 완전히 새로운 메시지로 나타납니다. 상대방 입장에서는 에이전트가 시작했던 대화를 잊어버린 것처럼, 시스템이 고장 난 것처럼 보일 것입니다.

스레딩 (Threading)은 에이전트 이메일 기능 중 거의 맞을 뻔했지만 조용히 틀리기 쉬운 부분입니다. 해결책은 대부분의 개발자가 건드리지 않는 몇 가지 이메일 헤더 (Email headers)와 메시지를 대화 단위로 그룹화해 주는 Threads API에 있습니다. 이 포스트에서는 백엔드를 위한 HTTP API와 터미널을 위한 Nylas CLI라는 두 가지 관점에서 이 두 가지를 모두 살펴봅니다. 저는 CLI를 담당하고 있으므로, 아래의 터미널 명령어들은 제가 답장 루프를 테스트할 때 사용하는 것들입니다.

스레딩을 작동하게 하는 세 가지 헤더

스레딩은 제목 (Subject lines)이 아니라 세 가지 이메일 헤더를 기반으로 작동합니다. 모든 메시지는 송신 서버가 찍어주는 전역적으로 고유한 식별자인 Message-ID를 포함합니다. 누군가 답장을 보낼 때, 그들의 메일 클라이언트는 In-Reply-To (답장 대상이 되는 메시지의 Message-ID)와 References (가장 오래된 것부터 최신 것까지의 전체 Message-ID 체인)를 추가합니다. 이 두 헤더는 모든 메일 클라이언트가 어떤 메시지들이 서로 연결되어 있는지를 결정하는 방식입니다.

한 번의 교환 과정에서 체인이 어떻게 구성되는지 보여드리겠습니다. 에이전트의 첫 번째 메시지는 Message-ID를 할당받고, 답장은 이를 가리키며, 에이전트의 후속 메시지는 이 두 가지를 모두 참조합니다:

# 에이전트의 발신 메시지
Message-ID: <abc123@agents.yourcompany.com>
Subject: Following up on your demo request
...

References 헤더는 메시지가 추가될 때마다 늘어납니다. 스레드가 5개의 메시지 깊이에 도달하면, 순서대로 5개의 Message-ID 값을 포함하게 됩니다. 이는 Gmail, Outlook, Apple Mail, Thunderbird가 모두 동일한 방식으로 읽는 대화의 완전한 감사 추적(audit trail) 역할을 합니다.

제목 일치 방식이 실패하는 이유

제목(subject line)으로 답장을 매칭하는 것은 대부분의 에이전트 구현체가 빠지는 함정입니다. 만약 제목이 Re:로 시작하고 원문 텍스트를 포함하고 있다면 답장으로 처리하는 방식입니다. 이 방식은 테스트 단계에서는 작동하지만, 실제 운영 환경(production)에서는 세 가지 구체적인 이유로 실패합니다. 제목 매칭은 제목이 동일할 때 두 대화를 구분할 방법이 없으며, 제목이 변경되었을 때 대화를 추적할 방법도 없습니다.

  • 수신자가 제목을 수정합니다. "Q3 budget review"에 대한 답장이 "Re: Q3 budget review — updated numbers attached"로 돌아올 수 있습니다. 단순한 포함 여부 매칭(contains-match)은 작동하겠지만, 수정 과정에서 원문 단어가 완전히 사라지면 더 이상 작동하지 않습니다.
  • 여러 스레드가 동일한 제목을 공유합니다. 두 명의 잠재 고객이 모두 "Following up on your demo request"라는 제목을 받습니다. 어느 쪽의 답장이든 두 스레드 모두에 매칭되어 버리며, 에이전트는 어떤 잠재 고객이 답변했는지 구분할 수 없습니다.
  • 전달(Forward) 시 제목이 재사용됩니다. 누군가 스레드를 동료에게 전달하고 그 동료가 답장을 보냅니다. 제목은 변경되지 않았지만 대화의 맥락은 완전히 달라집니다.

헤더(header) 접근 방식은 이러한 실패 모드가 전혀 발생하지 않습니다. 왜냐하면 In-Reply-ToReferences는 사람이 읽을 수 있는 텍스트가 아니라 특정 Message-ID 값을 가리키기 때문입니다. 먼저 헤더를 기준으로 매칭하고, 헤더가 누락된 경우에만 제목으로 대체하십시오. 헤더 누락은 클라이언트 오류와 같은 예외적인 상황(edge case)으로 간주할 수 있을 만큼 매우 드뭅니다.

Nylas가 스레딩을 유지하는 방법

Nylas는 메시지가 사서함을 어떻게 이동하든 스레딩 체인(threading chain)을 온전하게 유지합니다. 즉, 여러분의 에이전트가 Message-ID를 생성하거나 References 헤더를 직접 조립할 필요가 없음을 의미합니다. 스레딩은 아웃바운드(outbound) 경로와 인바운드(inbound) 메일 모두에서 유지됩니다.

  • API 전송 (POST /v3/grants/{grant_id}/messages/send): reply_to_message_id를 전달하면 Nylas가 원본의 Message-ID를 가져온 뒤, 아웃바운드(outbound) 메시지에 In-Reply-ToReferences를 자동으로 설정합니다. 기존 초안(draft)을 전송하는 것도 동일한 경로를 거치므로 같은 방식으로 동작합니다.
  • SMTP 제출 (port 465 또는 587): 사용자가 SMTP를 통해 연결된 메일 클라이언트에서 답장을 보내는 경우, Nylas는 클라이언트가 설정한 Message-ID, In-Reply-To, References를 그대로 보존합니다.
  • 인바운드(Inbound) 메시지: 답장이 도착하면 Nylas는 전체 헤더를 저장합니다. 전체 헤더 세트를 읽으려면 fields=include_headers를 사용하고, 메시지 본문보다 큰 경우가 많은 전체 헤더 페이로드를 건너뛰고 Message-ID, In-Reply-To, References만 가져오려면 fields=include_basic_headers를 사용하세요.

이러한 일관성 덕분에 에이전트가 API를 통해 메시지를 보내고 사람이 IMAP을 통해 후속 조치를 취하더라도 스레드가 끊기지 않고 유지될 수 있습니다. 두 경로 모두 동일한 메일함에 기록되며 동일한 헤더 체인을 공유하기 때문입니다.

Threads API를 사용하여 스레드 목록 조회 및 가져오기

In-Reply-ToReferences를 직접 파싱하는 대신, Threads API를 호출하여 그룹화된 대화(메시지 ID, 참여자 및 메타데이터)를 한 번의 호출로 요청하세요. grant의 threads 컬렉션에 대해 GET 요청을 보내면 각 스레드가 이미 그룹화된 상태로 반환되므로, 에이전트는 메시지를 수동으로 이어 붙일 필요 없이 대화 전체를 하나의 단위로 읽을 수 있습니다.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/threads?limit=10" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

각 스레드(thread) 객체는 에이전트가 무엇을 할지 결정하는 데 필요한 필드들을 포함하고 있습니다: message_ids (순서대로 나열된 모든 메시지), participants (대화에 참여한 모든 사람), latest_message_received_datelatest_message_sent_date, 가장 최근 메시지의 snippet (요약본), 그리고 subject (제목), unread (읽지 않음), starred (즐겨찾기), folders (폴더)입니다. 답장이 발송되어 message.created 웹훅(webhook)이 트리거되면, 페이로드(payload)에는 thread_id가 포함되며, 이를 통해 여기서 전체 이력을 조회하여 응답을 준비할 수 있습니다.

웹훅 페이로드에만 의존하지 않고 스레드를 가져와야 하는 이유가 하나 더 있습니다. 메시지 본문이 약 1MB를 초과하면 트리거 이름이 message.created.truncated로 변경되며, 페이로드 크기를 작게 유지하기 위해 본문이 생략됩니다. 이 경우 에이전트는 thread_idmessage_id는 가지고 있지만 텍스트는 없는 상태가 되므로, 후속 작업으로 GET /messages/{message_id}를 호출하여 답장에 필요한 전체 본문을 가져와야 합니다. 스레드를 가져오면 대화의 메시지 요약본과 ID를 얻을 수 있으며, 개별 메시지의 전체 본문은 메시지 엔드포인트(messages endpoint)를 통해 가져옵니다.

SDK를 사용할 경우, 스레드와 그 메시지들을 가져오는 것은 몇 번의 호출로 이루어집니다. 이것은 웹훅이 트리거된 후 제가 사용하는 패턴입니다. 즉, 스레드를 가져온 다음 대화 내용을 재구성하는 방식입니다:

// message.created 웹훅을 수신한 후:
const thread = await nylas.threads.find({
  identifier: AGENT_GRANT_ID,
...

API 및 CLI를 통한 스레드 내 답장 (Reply in-thread)

올바르게 스레드가 유지되는 답장은 일반적인 전송과 동일하지만, 답장 대상이 되는 메시지로 설정된 reply_to_message_id라는 필드가 하나 더 추가됩니다. Nylas는 해당 ID를 읽어 원본의 Message-ID를 추출한 뒤, 발신 메시지에 In-Reply-ToReferences 헤더를 찍어줍니다. 이를 통해 모든 수신자의 클라이언트와 에이전트 자신의 편지함에서 해당 메시지가 올바른 스레드에 위치하게 됩니다. API를 사용하는 방법은 다음과 같습니다:

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
...

CLI에서도 플래그 하나로 동일한 작업을 수행할 수 있습니다. nylas email sendreply_to_message_id를 자동으로 설정해 주는 --reply-to 옵션을 제공하며, 이를 통해 Nylas가 스레딩 헤더 (threading headers)를 찍어주어 메시지가 올바른 스레드에 안착하게 합니다. 수신자와 제목은 여전히 명시적으로 제공해야 합니다. 즉, 스레딩이 적용되는 일반적인 전송 방식이며, 이는 동일한 호출을 코드에 연결하기 전에 에이전트의 답장이 제대로 스레딩되는지 빠르게 확인할 수 있는 방법입니다.

# 특정 메시지에 스레드 내 답장하기
nylas email send \
  --to alice@example.com \
...

두 인터페이스 모두 별도의 '전체 답장 (reply-all)' 기능은 없습니다. 대화에 모든 사람을 포함시키려면 CLI의 --to--cc 옵션에, 또는 API 바디 (body)의 tocc 배열에 목록을 작성하면 됩니다. 답장하기 전에 무엇에 답장하는지 먼저 확인하려면, nylas email read <message-id>로 메시지를 출력하거나 nylas email list로 사서함을 확인할 수 있습니다. 어떤 인터페이스를 통해 보내든, 전송된 답장은 GET /threads를 통해 볼 수 있는 것과 동일한 스레드에 나타납니다.

스레드를 에이전트의 상태에 매핑하기

Threads API는 에이전트에게 어떤 메시지들이 서로 연결되어 있는지는 알려주지만, 대화가 시작되었을 때 에이전트가 무엇을 하고 있었는지(어떤 작업, 어떤 워크플로 단계, 어떤 세션인지)는 알려주지 않습니다. 그 매핑 정보는 thread_id를 키(key)로 하여 귀하의 애플리케이션 내에 존재해야 합니다. 신뢰할 수 있는 패턴은 두 단계로 나뉩니다: 에이전트가 메시지를 보낼 때 매핑을 저장하고, 답장이 도착했을 때 이를 조회하는 것입니다.

// thread_id -> { sessionId, taskId, step, ... }
const threadState = new Map();

...

프로덕션 환경에서 이 맵 (map)은 메모리가 아닌 데이터베이스나 영구 저장소 (durable store)에 있어야 합니다. 이메일 대화는 몇 시간 또는 며칠에 걸쳐 진행되며, 인메모리 (in-memory) 맵은 프로세스 재시작 시 유지되지 않습니다. 하필이면 3시간 전의 답장이 도착하는 시점이 바로 프로세스가 재시작되는 때일 수 있습니다. thread_id는 Nylas가 할당하며 대화 전체를 포괄하기 때문에 적절한 키가 되며, 반면 단일 Message-ID는 단 하나의 메시지만 포괄합니다.

에이전트 자신의 메시지에 반응하지 마세요

message.created 웹훅은 사서함의 모든 새로운 메시지에 대해 발생하며, 여기에는 에이전트가 보내는 메시지도 포함됩니다. 방향성을 확인하지 않고 모든 message.created 이벤트에 답장하는 에이전트는 자신이 보낸 아웃바운드 (outbound) 메일에 스스로 답장하며 루프 (loop)에 빠지게 됩니다. 에이전트가 동작하기 전에, 해당 메시지가 다른 사람으로부터 온 것인지 확인하십시오. 즉, 발신자가 에이전트 계정 (Agent Account) 자신의 주소가 아닌지 확인하고, 에이전트가 방금 보낸 메시지는 건너뛰어야 합니다.

인바운드 (inbound) 측면에서의 중복 제거 (Deduplication) 또한 중요합니다. 수신자가 몇 초 사이에 두 번 답장을 보내면 동일한 스레드 (thread)에서 두 개의 message.created 이벤트가 발생할 수 있으며, 엔드포인트 (endpoint)의 응답이 느릴 경우 Nylas는 재시도 (retry) 시 웹훅을 다시 전달합니다. 에이전트가 이미 답변한 message_id 값들을 추적하고, 응답을 보낸 후에는 해당 답장을 처리된 것으로 간주하십시오. 해당 기록의 키 (key)를 인바운드 message_id로 설정하면 에이전트가 동일한 메시지에 두 번 답장하는 것을 방지할 수 있습니다.

에이전트 답장 루프에서 주의해야 할 사항

몇 가지 스레딩 (threading) 동작은 특히 에이전트를 곤란하게 만드는데, 이는 인간은 판단력을 사용하는 반면 에이전트는 자동으로 반응하기 때문입니다. 이러한 문제들은 존재한다는 것을 알고 나면 처리하기 어렵지 않지만, 모른 채로 방치하면 각각 눈에 띄는 버그를 유발합니다.

  • thread_idMessage-ID가 아닌 기본 키(primary key)입니다. Nylas가 할당하며 전체 대화에 걸쳐 유지되므로 애플리케이션 로직에 더 안정적입니다. 원시 헤더(raw headers)는 꼭 필요한 경우에만 사용하세요.
  • 아웃바운드(outbound) 메시지당 하나의 답장이 올 것이라고 가정하지 마세요. 잠재 고객이 두 번 답장하거나, 스레드 내의 두 사람이 동시에 응답할 수도 있습니다. 중복 답장을 보내지 않고 하나의 스레드에서 발생하는 여러 인바운드(inbound) 메시지를 처리하세요. 인바운드 message_id에 대한 중복 제거(dedup) 체크만으로 충분합니다.
  • 스레드는 휴면 상태가 되었다가 다시 활성화됩니다. 누군가 3주 전의 스레드에 답장을 보낼 수 있습니다. 상태 매핑(state mapping)에 TTL(Time To Live)이 설정되어 있다면, 컨텍스트(context)가 만료되었을 때 에이전트가 어떻게 행동할지 결정해야 합니다: 스레드 기록을 다시 읽을지, 사람에게 에스컬레이션(escalate)할지, 아니면 새로 시작할지 결정하세요.
  • 원시 헤더(Raw headers)는 파라미터 하나로 제어할 수 있습니다. 스레딩을 직접 디버깅해야 할 때, 전체 헤더 세트 대신 Message-ID, In-Reply-To, References만 필요하다면 메시지 GET 요청 시 fields=include_basic_headers를 전달하세요.
  • 답장하기 전에 스레드를 가져오세요. 단일 메시지 웹훅(webhook) 페이로드만 사용하여 답장 초안을 작성하는 에이전트는 내용을 반복하거나 이전 메시지와 모순되는 내용을 보낼 수 있습니다. 먼저 스레드를 불러오세요.

마무리

에이전트를 위한 스레딩(Threading)은 두 가지 습관으로 요약됩니다: 답장이 스레드에 묶이도록 항상 reply_to_message_id를 전달하는 것, 그리고 각 답장이 어디에 속하는지 알 수 있도록 에이전트의 상태(state)를 thread_id 기준으로 키를 지정하는 것입니다. Nylas는 모든 전송 경로에서 헤더 메커니즘을 처리하므로, 남은 작업은 애플리케이션의 몫입니다. 즉, 대화를 작업(task)에 매핑하고, 스레드가 조용해졌다가 다시 살아났을 때 어떻게 할지 결정하는 것입니다.

다음 단계:

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0