이메일 테스트가 로컬 노트북에서는 완벽하게 돌아가는데 CI에만 올라가면 무너진다면, 당신만 그런 것이 아닙니다. 보통은 테스트 코드 곳곳에 sleep 호출을 뿌리거나, 빌드가 통과할 때까지 재시도 횟수를 늘리는 식으로 대응하곤 합니다. 그런 방식은 하루 정도는 소음을 잠재울 수 있겠지만, 버그를 고치는 것은 아닙니다. 단지 숨길 뿐입니다.
진짜 문제는 테스트가 어떤 이메일을 열어야 할지 식별하는 방식에 있습니다.
공유 인박스 문제 (The Shared Inbox Problem)
로컬 환경에서는 한 번에 하나의 테스트만 실행합니다. 이메일이 한 통 도착하면, 그것을 가져오면 됩니다. 간단하죠.
하지만 CI는 완전히 다른 환경입니다. 단 하나의 풀 리퀘스트가 4개, 8개, 혹은 16개의 병렬 작업을 트리거할 수도 있습니다. 만약 이 작업들이 모두 하나의 테스트 인박스를 공유한다면—Mailosaur 서버, Mailtrap 인박스, 혹은 스테이징 도메인의 실제 계정 등 무엇이든 간에—모두가 동시에 동일한 버킷에 데이터를 쓰고 있는 셈입니다. 작업 A는 비밀번호 재설정 메일을 보내고, 작업 B는 초대 메일을 보냅니다. 작업 C는 실패한 환영 플로우를 재시도합니다. 그 사이 백그라운드 워커와 전달 큐가 제어할 수 없는 지터(jitter)를 발생시킵니다.
모든 작업이 공유 인박스에 접근해 제목이 "비밀번호를 재설정하세요"인 가장 최신 메시지를 요청할 때, 경합 상태(race)가 발생합니다. 승리한 테스트는 올바른 이메일을 받습니다. 하지만 패배한 테스트는 다른 작업용 링크를 클릭하고, 잘못된 콘텐츠를 검증하며, 타이밍 문제처럼 보이는 에러와 함께 실패합니다. 이것은 타이밍 문제가 아니라 식별(identity) 문제입니다.
"최신 메시지" 방식이 실패하는 이유
이 취약한 패턴은 직관적으로 느껴지기 때문에 빠지기 쉽습니다:
- 사용자 플로우를 트리거합니다.
- 몇 초마다 인박스를 폴링(poll)합니다.
- 제목이 일치하는 가장 최근 메시지를 엽니다.
- 첫 번째 링크를 클릭하고 검증(assertion)을 실행합니다.
이는 단순한 병렬 처리 외에도 여러 이유로 무너집니다. 이전 실패한 실행의 재시도가 늦게 도착하여, 현재 테스트가 폴링하는 순간 갑자기 최신 메시지가 될 수도 있습니다. 애플리케이션 내부의 백그라운드 워커가 두 개의 이메일을 큐에 넣고 첫 번째보다 두 번째를 먼저 보낼 수도 있습니다. 제목만으로는 식별력이 약합니다. 스테이징 애플리케이션이 서로 다른 경로에서 유사한 이메일을 보낼 수도 있기 때문입니다. 타임스탬프 정렬은 보기보다 더 위험합니다. CI 러너와 메일 제공자 간의 클록 스큐(clock skew)가 실제로 존재하며, 메일 API는 종종 인덱스를 캐싱하거나 배치(batch) 처리하기 때문입니다.
바쁜 환경에서는 타임스탬프가 불분명해집니다. 더 직접적인 무언가가 필요합니다.
런 토큰(Run Token)이란 무엇인가
런 토큰은 테스트 시작 시 생성되어 애플리케이션이 보내는 이메일에 삽입되는 고유 문자열일 뿐입니다. 사용자에게 보여줄 필요도 없고, 우아하게 보일 필요도 없습니다. 단지 이 특정 메시지가 이 특정 테스트 실행에 속한다는 것을 증명할 수만 있으면 됩니다.
구체적인 예시가 가장 좋습니다. 테스트를 시작하기 전에 다음과 같은 토큰을 생성하세요:
- UUID:
550e8400-e29b-41d4-a716-446655440001 - 빌드 범위의 요청 ID:
req_ci_build_4821_a7f3 - 초대 슬러그 또는 메타데이터 접미사:
signup-token-8k2m9n - 테스트 러너가 생성한 랜덤 헥사 문자열:
test-run-a4f9c2d1
백엔드 코드를 제어할 수 있다면, 이메일 컨텍스트에 토큰을 전달하고 본문 어딘가에 렌더링하세요. 블랙박스 애플리케이션을 테스트하는 중이라면, 앱이 이미 가로챌 수 있는 참조 필드를 허용하는지 확인하세요. 그렇지 않다면, 플러스 주소 지정(plus addressing)을 사용하여 수신자 로컬 파트(local-part)에 토큰을 삽입할 수도 있습니다—testuser+a4f9c2d1@example.com—단, 이는 애플리케이션이 이를 보존하여 이메일에 다시 반영할 때만 작동합니다.
핵심은 메일 시스템이 이미 소유하고 있는 메타데이터로 매칭하는 것을 멈추고, 테스트가 소유한 데이터로 매칭하는 것입니다.
신뢰할 수 있는 패턴
"최신 메시지" 알고리즘을 좁은 범위의 토큰 기반 검색으로 교체하세요:
- 어떤 플로우를 트리거하기 전에 런 토큰을 생성합니다.
- 사용자 액션을 시작하며, 애플리케이션이 발송 이메일에 토큰을 포함하도록 보장합니다.
- 해당 토큰으로 제한된 필터를 사용하여 메일 제공자를 폴링합니다. API가 본문 검색을 지원하면 사용하세요. 그렇지 않다면, 후보 메시지들을 가져와 클라이언트 측에서 본문을
grep하세요. - 링크, 버튼, 또는 인증 코드를 건드리기 전에 메시지 본문에 토큰이 존재하는지 검증(assert)합니다.
- 그 후에만 확인 URL이나 코드를 추출하여 계속 진행합니다.
이 순서가 중요합니다. 링크를 먼저 추출하고 토큰을 나중에 확인한다면, 이미 잘못된 이메일을 클릭한 것입니다. 검증(assertion)이 당신의 문지기(gatekeeper)가 되어야 합니다.
실질적으로, 헬퍼는 Subject:"Welcome to AppName" sort:-received보다는 Subject:"Welcome to AppName" AND Body:"a4f9c2d1"를 검색해야 합니다. 많은 메일 테스트 서비스는 본문 내용 필터를 허용하는 검색 API를 제공합니다. 이를 활용하세요. 더 단순한 제공업체를 사용 중이라면, 모든 테스트에서 일관되게 클라이언트 측 필터링을 추가할 수 있도록 폴링(polling) 로직을 한곳에 모아두세요.
시스템의 신뢰성을 유지하기 위한 세 가지 규칙
런 토큰(run token)은 선택 대상을 고정해 주지만, 폴링 방식과 문제가 발생했을 때의 대처 방식에 대해서는 여전히 엄격한 규칙이 필요합니다.
실패 시 인박스(inbox) 상태를 기록하세요. 테스트가 실패하면 인박스 식별자, 쿼리한 제목, 정확한 타임스탬프 범위, 그리고 기준에 부합하는 메시지 개수를 출력하세요. 이렇게 하면 모호한 "이메일을 찾을 수 없음" 오류가 구체적인 상황 설명으로 바뀝니다. 만약 작업(job) 7823이 3초 늦게 도착한 작업 7821의 재시도 메시지를 가져온 것이라면, 로그를 통해 이를 명확히 알 수 있어야 합니다. 이러한 맥락이 없다면, 단순히 타이밍 문제라고 치부하며 sleep을 또 추가하게 될 것입니다.
모든 이메일 폴링 로직을 하나의 헬퍼 파일에 유지하세요. setTimeout이나 cy.task 호출을 20개의 테스트 파일에 흩뿌려 놓지 마세요. 메시지를 기다리고, API 호출을 재시도하며, 백오프(backoff)를 적용하는 로직을 중앙 집중화하세요. 모든 테스트가 동일한 헬퍼를 사용하면 필터링 규칙이 일관되게 유지되며, 검색 로직을 개선할 때 모든 테스트가 그 혜택을 입습니다. 또한 토큰 체크를 강제하기가 더 쉬워집니다. 헬퍼가 토큰 인자를 요구하도록 만들면, 아무도 실수로 "최신 메시지"라는 편법에 의존할 수 없습니다.
재시도(retry)를 주의 깊게 살피세요. CI 환경에서 테스트 재시도는 흔한 일이지만, 재시도할 때마다 인박스에는 또 다른 이메일이 생성됩니다. 세 번째 시도에서 테스트가 통과하면 그냥 축하하고 넘어가고 싶을 것입니다. 하지만 놓치고 있는 사실은, 첫 번째와 두 번째 시도에서 레이스 컨디션(race condition), 중복 전송, 또는 인덱스 누락과 같은 실제 버그가 발생했을 수 있으며, 추가된 메시지들이 이를 가려버렸을 수도 있다는 점입니다. 재시도를 반드시 사용해야 한다면, 실패 후 인박스에 예상치 못한 중복 메시지가 있는지 확인하세요. 더 좋은 방법은 제공업체가 동적 인박스를 지원한다면 인박스를 비우거나 작업마다 고유한 주소를 사용하는 것입니다. 재시도가 불안정한 선택 로직을 덮어버리는 전략이 되어서는 안 됩니다.
핵심 요약
인박스를 날짜순으로 정렬하고 최상단 결과를 가져오는 것은 테스트가 아닙니다. 그것은 코드로 포장된 추측일 뿐입니다. 런 토큰을 사용하는 데는 비용이 거의 들지 않습니다. 문자열 변수 하나, 필터 파라미터 하나, 혹은 약간의 템플릿 수정 정도면 충분하며, 이를 통해 테스트에 결정론적인(deterministic) 정체성을 부여할 수 있습니다. 이는 눈앞의 메시지가 현재 실행 중인 작업에 속한 것임을 증명해 줍니다.
sleep을 추가하며 네트워크가 잘 작동하기를 기도하는 일을 멈추세요. 토큰을 생성하여 이메일에 포함시키고, 이를 직접 검색하세요. 그러면 CI 실행 속도는 빨라지고, 로그는 읽기 쉬워지며, 마침내 이메일 테스트 스위트가 알려주는 결과를 신뢰할 수 있게 될 것입니다.
