이메일 에이전트를 사용하여 여행 일정 전송 및 업데이트하기
요약
Nylas의 '에이전트 계정(Agent Account)'을 활용하여 이메일과 캘린더를 통해 여행 일정을 관리하는 에이전트 구현 방법을 설명합니다. 에이전트가 직접 메일함과 캘린더를 소유하며, 변경 사항을 실시간으로 반영하고 업데이트하는 워크플로우를 다룹니다.
핵심 포인트
- 에이전트 계정은 자체 메일함과 캘린더를 가진 독립적인 권한(grant)임
- Messages, Threads, Events, Webhooks API를 통해 상호작용 구현 가능
- OAuth 절차 없이 사용자의 도메인에 연결하여 즉시 사용 가능
- 일정 변경 시 전체 계획 재전송 대신 특정 이벤트만 업데이트 가능
여행 관련 커뮤니케이션은 일정(itinerary) 비중이 높고 끊임없이 변화합니다. 하나의 "여행"은 결코 단일 이벤트가 아닙니다. 출발 항공편, 호텔, 렌터카, 저녁 식사 예약, 그리고 돌아오는 항공편이 결합된 형태입니다. 그러다 항공사가 출발 시간을 3시간 변경하면, 여행자는 "호텔 숙박을 하루 미룰 수 있을까요?"라고 이메일을 보내고, 그에 따라 모든 후속 세그먼트(segment)가 이동하게 됩니다. 대부분의 "AI 여행 비서" 데모는 모델을 사용자의 편지함(inbox)에 연결하는 것으로 끝납니다. 하지만 에이전트가 직접 참여자가 되어, 자신의 주소로 일정을 보내고, 답장을 기다리며, 변경된 캘린더(calendar) 항목을 조용히 수정하기를 원한다면 이야기가 달라집니다.
이것이 바로 **에이전트 계정 (Agent Account)**입니다. 이는 자체적인 실제 메일함과 실제 캘린더를 가진 Nylas 권한(grant)이며, 다른 편지함과 마찬가지로 주소를 지정할 수 있습니다. 여행자가 trips@yourcompany.com으로 이메일을 보내면 사람처럼 보이는 스레드(thread)가 답장으로 돌아옵니다. 그 이면에서는 여러분의 코드가 메시지를 가져오고, 변경 요청을 분류하며, Events API를 호출하여 각 세그먼트를 업데이트합니다. 저는 Nylas CLI를 담당하고 있으므로, 아래의 터미널 명령어들은 제가 수동으로 이러한 흐름을 테스트할 때 사용하는 정확한 명령어들이며, 이 모든 명령어는 프로덕션 환경에 연결할 수 있는 API 쌍(twin)을 가지고 있습니다.
실제로 얻게 되는 것
에이전트 계정은 _단순한 권한 (grant)_일 뿐입니다. 이것이 핵심이며, 데이터 평면(data plane)에서 새로 배울 것은 없다는 뜻이기도 합니다. 반환받은 grant_id는 여러분이 이미 알고 있는 모든 권한 범위 엔드포인트(grant-scoped endpoint)와 함께 작동합니다:
- Messages (메시지): 일정을 전송하고 들어오는 답장을 읽기 위해 사용합니다.
- Threads (스레드): 여행에 관한 전체 주고받은 대화 내용을 가져오기 위해 사용합니다.
- Events (이벤트): 변경 요청이 들어왔을 때 항공편, 호텔 또는 저녁 식사 세그먼트를 업데이트하기 위해 사용합니다.
- Webhooks (웹훅): 여행자가 답장을 보내는 즉시 알림을 받기 위해 사용합니다.
새로운 리프레시 토큰도 필요 없고, 실제 제공업체와의 복잡한 OAuth 절차도 없으며, 접근 권한을 부여해 줄 사람의 개입(human in the loop)도 없습니다. 사용자가 계정을 생성하고 자신이 소유한 도메인에 연결하기만 하면 되며, 마치 달력이 부착된 사서함처럼 작동합니다. 특히 여행의 경우, 달력 기능이 핵심적인 이점입니다. 각 일정 세그먼트는 여행자가 Google Calendar, Microsoft 365 또는 Apple Calendar에서 수락하는 이벤트가 되며, 무언가 변경되면 전체 계획을 다시 보내는 대신 _해당 이벤트_만 업데이트하면 됩니다.
더 나아가기 전에 솔직하게 범위(scoping)에 대해 말씀드립니다. 이 에이전트는 자신만의 달력과 자신만의 이벤트를 기반으로 작동합니다 — 즉, 권한 부여(grant)가 제공하는 이벤트들만 다룰 뿐입니다. 여기에는 스케줄러 왕복 처리(Scheduler round-trip)나 공유 가능한 가용성 마법 같은 것은 없습니다. 만약 협상 과정이 많은 '세 가지 시간대를 제안하고 승자를 예약하는' 흐름이 필요하다면, 그것은 Scheduler라는 다른 도구입니다. 이 게시물은 더 간단하고 일반적인 경우를 다룹니다: 에이전트는 이미 일정을 알고 있으며, 단지 이를 전송하고 최신 상태로 유지하기만 하면 됩니다.
여행 상태 문제 (빌드하기 전에 읽어보세요)
여기서 사람들이 어려움을 겪는 부분이 있습니다. Nylas는 사용자의 여행 모델을 저장하지 않습니다. 권한 부여(grant)에 '여행 TRIP-8842에는 이 다섯 개의 세그먼트가 있고, 세그먼트 2는 호텔이다'와 같은 내용을 보관할 수 있는 사용자 지정 메타데이터 필드가 없습니다. 이러한 구조화된 여행 상태를 위한 사용자 지정 메타데이터는 지원되지 않으므로, 사용자의 데이터베이스가 여행 정보를 소유해야 합니다.
구체적으로, 다음을 매핑하는 테이블을 유지합니다:
- **여행 ID(trip ID)**와 해당 여행자의 이메일 스레드.
- 각 세그먼트(출발 항공편, 호텔, 차량, 저녁 식사, 복귀 항공편)를 생성했을 때 받은 Nylas의
event_id. - 일정 이메일의 **스레드 ID(thread_id)**를 유지하여 수신된 답장을 특정 여행에 연결할 수 있도록 합니다.
변경 요청이 도착하면, 코드는 thread_id를 통해 여행 정보를 조회하고, 여행자가 _어느 구간(segment)_에 대해 이야기하고 있는지 파악합니다(이는 Nylas의 역할이 아니라 LLM의 분류(classification) 작업입니다). 그 후 해당 구간의 event_id를 가져와 Events API를 호출합니다. Nylas는 전송 수단(transport)이자 캘린더 역할을 수행하며, 여행 그래프(trip graph)는 여러분이 관리합니다. 이 경계를 명확히 유지하면 나머지는 기계적인 과정이 됩니다.
시작하기 전에
Nylas에 등록한 도메인에 Agent Account가 필요합니다. 커스텀 도메인을 사용하거나 Nylas의 *.nylas.email 체험용 서브도메인을 사용하여 테스트해 볼 수 있습니다. 다만, 새로운 발송 도메인은 약 4주에 걸쳐 워밍업(warm up) 기간이 필요하므로, 첫날부터 도달률(deliverability)을 판단하지 마십시오. 무료 플랜은 계정당 하루 메시지 200개로 제한되는데, 이는 단일 여행자의 일정 변경(itinerary churn)을 처리하기에는 충분하지만, 대량의 확인 메일을 발송할 계획이라면 유념해야 할 사항입니다.
아직 계정이 없다면 Agent Accounts 문서를 통해 프로비저닝(provisioning) 과정을 확인할 수 있습니다. 이 포스트 전반에 걸쳐 에이전트의 권한(grant)에는 <GRANT_ID>를, API 키에는 <NYLAS_API_KEY>를, 기본 호스트로는 https://api.us.nylas.com을 사용하겠습니다. 여러분의 실제 값으로 교체하여 사용하세요.
일정 이메일 전송하기
첫 번째 단계는 여행자에게 일정 이메일을 보내는 것입니다. 이는 에이전트 주소에서 발송되는 일반적인 아웃바운드(outbound) 메시지이며, 특별한 것은 없습니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
제가 실제로 테스트할 때 입력하는 CLI 버전은 다음과 같습니다:
nylas email send <GRANT_ID> \
--to traveler@example.com \
--subject "Your trip to Lisbon — itinerary TRIP-8842" \
...
본문 끝에 있는 프롬프트에 주목하세요: 이 이메일에 답장하여 내용을 변경하세요(reply to this email to change anything). 이는 의도된 것입니다. 답장은 동일한 스레드(thread) 내에서 에이전트의 메일함으로 다시 들어오며, 이것이 바로 다음에 우리가 모니터링해야 할 대상입니다. 전송 응답에서 받은 thread_id를 지금 바로 여행 기록에 저장해 두십시오.
캘린더 세그먼트 생성하기 (Create the calendar segments)
여행자가 일정을 수락하고 휴대폰에서 확인할 수 있도록, 각 여행 일정(itinerary) 라인은 에이전트의 캘린더(calendar) 상의 이벤트(event)가 되어야 합니다. 각 세그먼트(segment)당 하나의 이벤트를 생성하고, 반환된 각 event_id를 데이터베이스(DB)의 해당 세그먼트에 저장하십시오. notify_participants=true를 전달하여 초대가 에이전트의 주소로 실제로 발송되도록 합니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
이러한 에포크 타임스탬프(epoch timestamps)는 직접 계산할 필요가 없습니다. date 명령어를 사용하여 생성하십시오:
date -j -f "%Y-%m-%d %H:%M" "2025-04-14 09:40" +%s
CLI(Command Line Interface)는 사람이 읽을 수 있는 시간을 직접 입력받아 에포크(epoch) 값을 자동으로 계산해 줍니다:
nylas calendar events create <GRANT_ID> \
--title "Flight TP204 — JFK→LIS" \
--start "2025-04-14 09:40" \
...
반드시 숙지해야 할 한 가지 사항이 있습니다: notify_participants 토글은 **API 쿼리 파라미터 (API query parameter)**이지, CLI 플래그(flag)가 아닙니다. nylas calendar events 명령어 세트에는 create, update, list, delete, rsvp가 있지만, events send라는 명령어는 없습니다. 에이전트가 참가자에게 직접 이메일을 보내게 하려면 캘린더 동사(verb)가 아닌 nylas email send를 사용해야 합니다. 이 두 가지 작업은 머릿속에서 별개로 분리해 두십시오.
변경 요청 감지하기 (Hear about the change request)
여행자가 "호텔 체크인을 하루 미룰 수 있을까요?"라고 답장하면, 표준 message.created 웹훅(webhook)이 발생합니다. 이것이 전체 업데이트 루프(update loop)의 트리거(trigger)이므로, 연결 설정을 정확하게 해야 합니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/webhooks" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
또는 CLI에서 다음과 같이 수행할 수 있습니다:
nylas webhook create \
--url https://yourapp.com/webhooks/nylas \
--triggers message.created \
...
여러분의 핸들러(handler)에 영구 마커로 새겨두어야 할 두 가지 규칙은 다음과 같습니다:
- 최상위 알림
id를 기준으로 중복을 제거(Dedupe)하세요. Nylas는 최소 한 번 전달(at-least-once delivery)을 보장하므로, 동일한 이벤트가 최대 세 번까지 도착할 수 있습니다. 최상위id는 하나의 이벤트에 대한 모든 재시도(retry) 과정에서 일정하게 유지되므로, 이를 중복 제거 키(dedup key)로 사용하면 됩니다. 내부의data.object.id(메시지 ID)는 메시지 자체를 식별합니다. 동일한 변경 요청에 대해 두 번 동작하지 않도록 이 ID를 추가적인 방어 기제로 사용할 수 있습니다. - 메시지 본문(body)을 페이로드(payload)만 믿고 처리하지 마세요. 웹훅(webhook)을 "무언가 발생했으니, 여기 ID가 있습니다"라는 신호로 취급하고, 텍스트가 필요한 시점에
GET /v3/grants/{grant_id}/messages/{message_id}를 호출하여 전체 메시지를 직접 가져오세요.message.created.truncated를 기준으로 분기 처리를 하십시오. 이 변형(variant)은 매우 큰 메시지에 대해 발생하며, 이때 본문을 다시 가져와야(re-fetch) 합니다. 요약 필드(summary fields)만으로 라우팅(routing)하기에는 충분할 수 있지만, 실제 변경 요청은 가져온 본문에서 파싱(parse)하십시오.
그리고 서명(signature)을 검증하세요. Nylas는 웹훅의 비밀키(webhook secret)를 사용하여 원시 요청 본문(raw request body)의 hex HMAC-SHA256 값을 X-Nylas-Signature 헤더에 담아 각 웹훅에 서명합니다. 만약 Node.js의 crypto.timingSafeEqual과 같은 함수를 사용하여 상수 시간 비교(constant-time compare)를 수행한다면, 먼저 두 버퍼의 길이가 동일한지 확인하십시오. 길이가 다르면 오류가 발생하기 때문입니다. 로컬 개발 환경에서는 nylas webhook verify가 검증을 대신 수행해 줍니다.
여행자의 답장 읽기
웹훅이 메시지 ID를 전달하면, 분류기(classifier)가 처리할 수 있는 실제 텍스트를 확보할 수 있도록 전체 메시지를 가져오세요.
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/<MESSAGE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
터미널에서:
nylas email read <MESSAGE_ID> <GRANT_ID>
에이전트의 편지함에 답장을 처리했음을 반영하고 싶다면, 명시적으로 읽음 처리(mark it read)를 해야 합니다. 이는 GET의 부수 효과(side effect)가 아니라 별도의 PUT 요청입니다:
curl --request PUT \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/<MESSAGE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI에서는 read 명령의 플래그를 통해 인라인으로 이를 수행합니다:
nylas email read <MESSAGE_ID> <GRANT_ID> --mark-read
더 넓은 맥락의 대화(conversation)를 파악하기 위해 — 즉, "내일로 미뤄줘"라는 말이 원래 제안된 내용과 비교했을 때만 의미가 있는 경우 — 전체 스레드(thread)를 가져옵니다:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/threads/<THREAD_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
터미널에서도 동일하게 수행할 수 있습니다:
nylas email threads show <THREAD_ID> <GRANT_ID>
이 thread_id는 여행 정보와 다시 연결해주는 조인 키(join key) 역할을 합니다. 이를 데이터베이스(DB)에서 조회하면, 여행자가 어떤 여행의 (그리고 분류 후에는 어떤 세그먼트(segment)의) 내용을 말하는지 알 수 있습니다. "호텔을 15일로 옮기고 싶어 한다"라는 실제 정보 추출(extraction)은 여러분의 LLM/앱 코드의 역할입니다. Nylas는 텍스트를 전달하고, 여러분의 모델이 이를 구조화된 변경 사항으로 변환합니다.
세그먼트의 이벤트 업데이트하기
이것이 여행별 업데이트의 핵심입니다. 세그먼트를 식별했고, DB에서 해당 event_id를 확보했다면, _그 이벤트_에 변경 사항을 적용합니다. 일정의 한 세그먼트를 업데이트하는 것은 하나의 이벤트를 업데이트하는 것과 같으며, 그 이상의 복잡한 과정은 없습니다. notify_participants=true를 전달하여 여행자의 캘린더(및 확인 이메일)에 새로운 세부 사항이 반영되도록 하세요.
curl --request PUT \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events/<EVENT_ID>?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI 업데이트는 새로운 시간을 직접 입력받습니다:
nylas calendar events update <EVENT_ID> <GRANT_ID> \
--start "2025-04-15 15:00" \
--location "Hotel Bairro Alto"
이 변경 사항은 여행자가 확인하는 모든 곳으로 전파됩니다. 에이전트가 해당 이벤트에 대한 초대장을 다시 보내기 때문에 Google, Microsoft 또는 Apple 캘린더가 제자리에서 업데이트됩니다. 만약 변경 사항이 연쇄적으로 발생한다면(예: 출발 항공편이 변경되어 공항 저녁 식사 시간도 변경되어야 하는 경우), 영향을 받는 _각각_의 세그먼트(segment) event_id에 대해 업데이트를 반복합니다. 어떤 세그먼트가 연쇄적으로 변경되는지는 귀하의 여행 모델(trip model)이 알려주며, 해당 세그먼트들을 루프(loop)로 돌며 이벤트당 한 번씩 업데이트를 실행하면 됩니다.
생성(create)과 마찬가지로, notify_participants는 API 전용입니다. CLI의 update는 캘린더에 변경 사항을 조용히 반영합니다. 여행자에게 이 내용을 이메일로 알리고 싶을 때는, 업데이트가 완료된 후 nylas email send를 사용하여 확인 메일을 보내는 것이 신뢰할 수 있는 패턴입니다.
답장을 통해 루프를 완성하기
여행자가 에이전트의 말을 제대로 들었는지 궁금해하게 만들지 마세요. 스레드 내에서 답장을 보내 변경 사항을 확인해 줍니다. 답장을 보내면 reply_to_message_id를 통해 스레드가 유지되므로, 확인 메일이 여행자의 메일 클라이언트에서 원래의 일정과 함께 그룹화됩니다.
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
...
CLI에는 수신자와 제목을 자동으로 채우기 위해 원본을 가져오는 전용 reply 명령어가 있어, 본문(body)만 제공하면 됩니다:
nylas email reply <MESSAGE_ID> <GRANT_ID> \
--body "완료되었습니다 — 호텔 체크인이 4월 15일로 변경되었습니다. 캘린더가 업데이트되었습니다. 이 여행에 대해 더 궁금한 점이 있으신가요?"
이것이 전체 사이클입니다: 일정 전송 → 세그먼트 생성 → 여행자 답장 → 웹훅(webhook) → 읽기 → 이벤트 업데이트 → 확인. 첫 번째 루프 이후의 모든 루프는 다른 세그먼트를 대상으로 동일한 형태를 가집니다.
가드레일(Guardrails) 및 주의사항
첫 번째 사고가 발생한 후에 덧붙이기보다는, 처음부터 구축해 두어야 할 몇 가지 사항들이 있습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기