이메일 기반 고객 문의를 채널톡 유저챗으로 연결하기
App Store가 메일 권한과 라우팅 경계를 맡고, 연동 앱은 고객 문의 처리에 집중하도록 나눈 이야기
Nari 🔅 • Core Web - Service
- Backend
안녕하세요, 채널톡 앱스토어팀의 Software Engineer 나리입니다.
앱스토어팀은 채널톡 안에서 외부 서비스와 채널톡을 연결하는 앱 플랫폼을 만들고 운영합니다. 고객사가 사용하는 쇼핑몰, 마켓플레이스, CRM 같은 도구를 채널톡과 이어주고, 앱이 설치, 실행, 권한, 설정, SDK 계약 안에서 안전하게 동작하도록 하는 기반을 맡고 있습니다.
이 글에서 말하는 앱은 채널톡 모바일 앱이나 데스크톱 앱이 아니라, Channel App Store에 등록되는 외부 연동 앱을 뜻합니다. 고객사가 사용하는 외부 서비스를 채널톡과 연결하고, 각 서비스의 도메인 로직을 담당하는 통합 앱이라고 봐주시면 됩니다.
채널톡은 고객 문의가 모이는 곳입니다. 고객이 웹 채팅으로 문의하든, 카카오 상담으로 문의하든, 외부 쇼핑몰의 메시지로 문의하든 상담자는 채널톡 유저챗 안에서 고객과 대화하고, 담당자를 지정하고, 이전 맥락을 확인하고, 응답 이력을 남길 수 있어야 합니다.
그런데 모든 외부 서비스가 채팅 API를 제공하지는 않습니다. 마켓플레이스 메시지나 쇼핑몰 문의 알림처럼 고객 문의가 이메일 알림이나 메일 포워딩으로만 전달되는 경우가 있습니다. 이 메일이 판매자의 개인 메일함이나 별도 관리자 페이지에만 남아 있으면 상담자는 채널톡과 외부 도구를 오가야 하고, 문의 누락이나 중복 응대가 생기기 쉽습니다.
그래서 이메일로 들어오는 고객 문의도 채널톡 유저챗으로 연결하는 기능이 필요했습니다. 상담자는 평소처럼 유저챗에서 답장하고, 연동 앱은 뒤에서 해당 답장을 외부 서비스의 이메일 스레드로 이어 보내는 구조가 필요했습니다.
처음엔 각 연동 앱이 메일 수신과 발송 권한을 직접 갖고 처리하면 되지 않을까 생각할 수 있습니다. 하지만 그렇게 하면 앱마다 메일 권한이 퍼지고, 어떤 앱이 어떤 relay 주소를 소유하는지 검증하는 책임도 앱마다 흩어집니다.
그래서 App Store에 Mail Relay Extension을 만들었습니다. 핵심은 간단합니다.
수신: 메일 수신 인프라 -> App Store -> Queue -> App Store worker -> 연동 앱의 Mail Relay handler
발신: 연동 앱 -> App Store native function -> 메일 발송 인프라그림 1. Mail Relay 수신 흐름
연동 앱은 서비스별 메일 도메인과 비즈니스 로직을 소유하고, App Store는 메일 권한과 공통 라우팅 경계를 소유합니다.
기존 이메일 수신 경로와 분리하기
채널톡에는 이미 일반 이메일 medium 수신 경로가 있습니다. 하지만 App Store 앱으로 들어오는 메일은 일반 이메일 문의와 다릅니다.
예를 들어 어떤 쇼핑몰 연동 앱의 relay 주소로 들어온 메일은 일반 이메일 medium이 아니라 해당 쇼핑몰 연동 앱이 처리해야 합니다.
shop-123@{app-relay-domain}그래서 앱 전용 메일 도메인으로 들어온 메일은 기존 이메일 수신 경로보다 먼저 분리합니다. raw MIME을 저장하고 App Store 쪽으로 이벤트를 발행한 뒤, 일반 이메일 수신 경로까지 이어지지 않게 멈춥니다.
이렇게 하면 기존 이메일 medium의 동작을 건드리지 않으면서도, 앱 전용 relay 주소로 들어온 메일만 App Store 경로로 안전하게 분리할 수 있습니다.
App Store는 메일을 해석하지 않는다
App Store가 하는 일은 메일 내용을 해석하는 것이 아닙니다. App Store는 공통 relay입니다.
수신 경로에서 App Store는 요청이 신뢰할 수 있는 출처에서 왔는지 검증하고, 메일 이벤트에서 수신자, 메시지 ID, 원문 위치 같은 routing에 필요한 정보만 추출합니다. 그리고 이 이벤트를 내부 queue에 넣습니다.
worker는 queue를 읽고 수신 주소에서 어떤 앱으로 라우팅해야 하는지 판단합니다.
shop-123@{app-relay-domain}
^^^^^^^^^^^^^^^^^^
routing key그 다음 Mail Relay Extension을 등록한 앱 중 routing key가 일치하는 앱을 찾고, 앱 서버의 inbound handler를 호출합니다.
여기까지가 App Store의 책임입니다. 어떤 메일인지, 어떤 고객 문의인지, 유저챗을 새로 만들지 기존 스레드에 붙일지는 연동 앱이 판단합니다.
메일 원문도 앱이 직접 저장소에서 읽지 않는다
메일 본문과 attachment는 raw MIME에 들어 있습니다. 하지만 raw MIME 전체를 처음부터 function input에 넣지는 않습니다. function input에는 원문을 찾을 수 있는 참조 정보만 들어갑니다.
앱이 실제 본문이 필요할 때는 App Store가 제공하는 native function을 호출합니다.
이때 App Store는 호출한 앱의 권한, 수신자 소유권, 메시지 ID, 원문 위치가 서로 맞는지 검증합니다. 검증이 통과하면 저장소에서 raw MIME을 읽어 앱에 돌려줍니다.
즉 앱은 메일 원문 저장소 권한을 직접 갖지 않습니다. App Store가 이 앱이 이 메일을 읽어도 되는지 확인한 뒤 broker 역할을 합니다.
그림 2. raw MIME 조회 흐름
유저챗 생성은 연동 앱의 책임
각 연동 앱은 Mail Relay inbound handler에서 provider별 로직을 수행합니다.
relay 주소의 local-part에서 shop id나 reply token을 파싱한다
앱 설정에서 Mail Relay 사용 여부를 확인한다
App Store native function으로 raw MIME을 읽는다
메일 header와 본문을 파싱한다
중복 수신인지 판단한다
새 UserChat을 만들거나 기존 thread에 이어 붙인다
App Store가 UserChat을 만들지 않는 이유는 이 판단들이 모두 앱별 도메인 지식이기 때문입니다. 서비스마다 주소 포맷, 원문 구조, 중복 판단 키, 답장 스레딩 방식이 다를 수밖에 없습니다.
답장도 앱이 직접 메일 발송 권한을 갖지 않는다
이번 구조에서 가장 중요한 변경점은 outbound입니다.
상담원이 UserChat에서 답장하면 연동 앱은 메시지 이벤트를 받고, 이메일 답장용 raw MIME을 조립합니다. 이때 From, To, Reply-To, Message-ID, In-Reply-To, References 같은 thread 유지에 필요한 header를 앱이 구성합니다.
하지만 앱이 메일 발송 인프라를 직접 호출하지는 않습니다. 대신 App Store native function을 호출합니다.
App Store는 sender가 앱 전용 relay domain에 속하는지, 해당 앱이 Mail Relay Extension을 등록했는지, token scope와 idempotency key가 유효한지 검증한 뒤 메일을 발송합니다.
이렇게 하면 메일 발송 권한은 App Store 한 곳에 남고, 앱은 무엇을 보낼지만 결정합니다.
그림 3. Mail Relay 답장 발신 흐름
SDK에 extension으로 등록한 이유
Mail Relay도 앱이 명시적으로 구현해야 하는 extension입니다. 그래서 SDK에 Mail Relay Extension 타입을 등록해두면 앱 개발자는 정해진 entrypoint와 input 타입을 기준으로 구현할 수 있고, App Store는 registry를 통해 라우팅 가능한 앱만 호출할 수 있습니다.
SDK에 드러나는 계약은 단순해야 합니다. inbound handler는 메일 이벤트를 받고, native function은 raw MIME 조회와 raw email 발송을 broker합니다. 실제 인프라 권한과 검증은 App Store 내부에 숨깁니다.
왜 이렇게 나눴나
이 설계의 핵심은 권한과 도메인 로직을 분리하는 것입니다.
App Store는 공통 보안 경계를 담당합니다.
수신 이벤트 검증
queue retry와 DLQ 기반 재처리 경로
Mail Relay Extension registry 기반 routing
raw MIME 접근 권한 검증
outbound sender 검증
메일 수신/발송 인프라 권한 보유
각 연동 앱은 provider별 의미 해석을 담당합니다.
relay 주소 포맷
설치 채널과 상점 매핑
메일 파싱
UserChat 생성과 스레딩
답장 MIME 구성
중복 처리
덕분에 새로운 이메일 기반 앱을 만들 때마다 메일 수신 rule, 저장소 권한, 발송 권한을 앱별로 복제하지 않아도 됩니다. 앱은 Mail Relay Extension만 구현하고, App Store가 제공하는 native function을 통해 안전하게 raw MIME을 읽고 답장을 보낼 수 있습니다.
만들면서 바뀐 것들
이 기능은 처음부터 지금의 모양으로 정해져 있던 것은 아닙니다. 처음에는 앱 전용 메일 도메인으로 들어온 메일을 기존 이메일 수신 경로와 분리하고, 별도 이벤트로 전달하면 끝이라고 생각했습니다. 그런데 실제로 만들어보니 어려운 부분은 메일을 받는 코드보다 “어디까지가 공통 경계이고, 어디부터가 앱의 책임인가”를 정하는 일이었습니다.
그림 4. Mail Relay 설계가 단단해진 과정
첫 번째로 정리한 것은 기존 이메일 경로를 건드리지 않는 방법이었습니다. 앱 전용 메일도 같은 메일 수신 인프라로 들어오기 때문에, 단순히 새 rule을 추가하면 일반 이메일 medium까지 같이 흘러갈 수 있었습니다. 그래서 앱 전용 주소는 먼저 분리하고, 원문 저장과 이벤트 발행이 끝나면 그 자리에서 멈추도록 했습니다. 이 덕분에 기존 이메일 문의 흐름은 그대로 두고, 앱 메일만 별도 경로로 보낼 수 있었습니다.
두 번째 고민은 어느 서비스가 외부 이벤트를 받을지였습니다. 초반에는 기존 중계 계층이 SNS webhook을 받고 App Store로 넘기는 구조도 검토했습니다. 하지만 메일을 어느 앱으로 보낼지 판단하는 registry, 앱 권한, native function 계약은 App Store가 이미 갖고 있었습니다. 결국 외부 webhook도 App Store가 직접 받고, 별도 중계 계층은 이 흐름에서 제외하는 쪽으로 정리했습니다.
세 번째는 “확인된 뒤에 열어야 한다”는 운영 순서였습니다. HTTPS subscription은 endpoint 소유자가 confirmation을 완료해야 실제 이벤트가 전달됩니다. 이 상태를 확인하지 않고 메일 수신 rule부터 열면, 메일은 저장되더라도 App Store까지 이벤트가 오지 않을 수 있습니다. 그래서 운영 순서를 subscription confirmation, queue와 권한 확인, 마지막으로 메일 수신 rule 적용 순서로 바꿨습니다.
네 번째로 SQS와 DLQ가 들어갔습니다. SNS에서 바로 앱 handler까지 호출하면 겉으로는 단순하지만, 앱 function 호출은 외부 webhook 요청 시간 안에서 끝낼 일이 아니었습니다. 파싱 실패, 일시적인 앱 서버 실패, 중복 이벤트 같은 상황에서 재시도와 재처리 경로가 필요했습니다. 그래서 App Store의 public endpoint는 이벤트를 검증하고 내부 queue에 넣는 역할만 하고, worker가 queue를 읽어 Mail Relay handler를 호출하도록 나눴습니다.
마지막으로 exp에서 성공한 뒤에도 prod에서 다른 종류의 문제가 차례로 드러났습니다. 원문을 읽기 위한 저장소 권한, 답장을 보내기 위한 메일 발송 권한, 그리고 메일 발송 계정의 production access 상태가 각각 별개의 체크포인트였습니다. 하나가 해결되면 다음 경계에서 실패가 드러나는 식이었고, 이 과정을 거치며 로그에 routing key, idempotency key, provider message id를 남기는 일이 얼마나 중요한지도 다시 확인했습니다.
결국 Mail Relay의 핵심은 “메일을 잘 파싱하는 코드” 하나가 아니라, 메일을 잃지 않고, 중복으로 처리하지 않고, 실패했을 때 다시 시도할 수 있는 운영 경계를 만드는 일이었습니다.
마무리
Mail Relay는 이메일 inbox polling 기능이 아닙니다. 외부 서비스가 이메일로만 이벤트를 보내는 상황에서, 그 이메일을 App Store 앱까지 안전하게 전달하고 다시 답장할 수 있게 해주는 공통 relay layer입니다.
수신은 Mail Relay inbound handler로 앱에 전달하고, raw MIME 조회와 답장 발송은 App Store native function으로 broker합니다.
이 구조 덕분에 앱은 자신의 도메인 로직에 집중하고, App Store는 공통 메일 권한과 검증 경계를 한 곳에서 관리할 수 있게 되었습니다.
