관리자 페이지 없이 Gmail 답변 상태 자동화하기
배경
사이드 프로젝트에서 고객 문의 기능을 만들었는데, 아직 관리자 페이지가 없어서 도메인 이메일(Gmail 클라이언트)에서 직접 발송하는 구조를 가지고 있었다.
문제는 Gmail에서 직접 답변을 보내다보니, 서버에서는 문의 답변이 발송되었는지 알 방법이 없었다. 관리자 페이지 개발 전까지 최대한 간단하게 상태를 변경할 방법이 필요했다.
고민한 방법들
처음에는 여러 방법을 검토했다.
Gmail API Watch 방식은 이론상 가능하지만, historyId 관리가 복잡하고 “이 메일이 어떤 문의 답변인지” 정확히 매칭하기 어렵다는 문제가 있었다. 운영 비용 대비 효용도 낮았다.
메일 본문 파싱 방식은 Gmail HTML 구조가 클라이언트/버전에 따라 달라질 수 있어서 위험한 구조다. 예를 들어 getPlainBody()로 파싱하던 코드가 Gmail 에디터 업데이트 이후 줄바꿈 방식이 바뀌면서 정규식 매칭이 실패할 수도 있다.
결국 선택한 방법은 Gmail Apps Script + 메일 제목 파싱 방식이다. 본문을 건드리지 않고 제목에서 문의 ID만 추출하는 방식이라 상대적으로 안정적이다.
전체 흐름
Gmail에서 문의 답변 발송
↓
제목에 (#ID) 형태로 문의 번호 포함
↓
Apps Script가 10분마다 보낸 메일함 스캔
↓
제목에서 ID 추출 → 백엔드 API 호출
↓
DB의 문의 답변 상태를 ANSWERED로 변경
메일 제목 규칙
답변 메일을 작성할 때 제목에 아래 형태로 문의 ID를 포함시킨다.
[서비스명] 문의(#123) 답변 드립니다.
고객 입장에서는 괄호 안의 숫자가 거슬릴 수 있지만, 실제로는 거의 신경 쓰지 않는다. 나중에 관리자 페이지가 생기더라도 같은 ID를 그대로 활용할 수 있다.
Apps Script 코드
Google Apps Script에 접속해서 새 프로젝트를 만들고 아래 코드를 붙여넣는다.
function checkSentMails() {
// 처리 완료 라벨 (없으면 생성)
const labelName = "processed-inquiry";
const processedLabel = GmailApp.getUserLabelByName(labelName) || GmailApp.createLabel(labelName);
// 최근 10분 + 제목에 문의 + 아직 처리 안 된 것
const query = 'newer_than:10m subject:문의 -label:' + labelName;
const threads = GmailApp.search(query);
threads.forEach(thread => {
const subject = thread.getFirstMessageSubject();
if (!subject.includes("문의")) return;
const messages = thread.getMessages();
messages.forEach(message => {
const subject = message.getSubject();
// (#123) 패턴 추출
const match = subject.match(/\(#\s*(\d+)\s*\)/);
if (match && match[1]) {
const inquiryId = match[1];
const apiUrl = `https://api.example.com/api/support/inquiries/${inquiryId}`;
UrlFetchApp.fetch(apiUrl, {
method: "patch",
headers: {
"X-ADMIN-CODE": "your-secret-key"
}
});
thread.addLabel(processedLabel); // 처리 완료 표시
}
});
});
}
작성 후 실행 버튼을 한 번 눌러서 Gmail 접근 권한을 허용해야 한다. 그다음 트리거를 설정한다.
- 왼쪽 메뉴 → Triggers → Add Trigger
- Function:
checkSentMails - Event source: Time-driven
- Type: Every 10 minutes
이제 매 10분마다 자동으로 체크된다.
JWT 필터 수정
기존에 JWT 인증 필터(OncePerRequestFilter)가 있었는데, 이 엔드포인트는 JWT 대신 X-ADMIN-CODE 헤더로 인증해야 한다. JWT 처리 전에 internal API 여부를 먼저 분기했다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
// 내부 자동화 API 먼저 체크
if (checkIfInternalRequest(request, response, uri)) {
filterChain.doFilter(request, response);
return;
}
// ... 기존 JWT 처리 로직
filterChain.doFilter(request, response);
}
private boolean checkIfInternalRequest(HttpServletRequest request, HttpServletResponse response, String uri)
throws IOException {
if (!pathMatcher.match("문의답변경로", uri)) {
return false;
}
String adminCode = request.getHeader("X-ADMIN-CODE");
if (!xAdminCode.equals(adminCode)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false; // ← 인증 실패 시 false 반환
}
return true; // 인증 성공 시 true 반환
}
보안 고려사항
X-ADMIN-CODE는 환경변수로 관리하고 32자 이상의 랜덤값을 사용한다.- HTTPS 환경에서만 운영한다.
- 현재 구조는 MVP 수준의 간단한 인증 방식을 사용하고 있으며, 추후 관리자 페이지로 개선할 예정이다.
정리
관리자 페이지 없이 Gmail로만 운영하는 상황에서 최대한 단순하게 상태 변경을 자동화했다. 메일 본문 파싱을 피하고 제목의 ID만 읽는 방식이라 Gmail 업데이트에도 비교적 안전하고, 나중에 관리자 페이지가 생기면 같은 API 엔드포인트를 그대로 활용할 수 있다.
다만 Apps Script가 특정 Gmail 계정에 종속되는 구조라, 해당 계정에 문제가 생기거나 담당자가 바뀌면 스크립트를 다시 설정해야 하는 번거로움이 있다. 실제로 사용해보니 별도 인프라 없이 빠르게 자동화할 수 있다는 점에서 MVP 단계에서는 충분히 실용적인 방법이었다.