매일 반복되는 이메일 발송, 파이썬으로 100명에게 맞춤형 대량 발송해본 후기

파이썬 smtplib로 100명에게 이름·내용이 다른 맞춤형 이메일을 자동 발송하는 방법, 직접 3개월 돌려보니 하루 2시간이 통째로 사라졌거든요.

저도 처음엔 그냥 Outlook에서 일일이 복붙했어요. 거래처 담당자 이름 바꾸고, 견적서 금액 고치고, 첨부파일 달리 넣고. 하루에 80~100통 정도 보내는 날은 점심시간이 그냥 증발하더라고요. 특히 이름을 잘못 넣어서 “김 과장님”한테 “박 대리님 안녕하세요”라고 보낸 적이 두 번이나 있었는데, 그때 등에서 식은땀이 줄줄 났습니다.

그래서 파이썬으로 자동화를 만들었고, 지금은 CSV 파일 하나에 수신자 정보를 정리해두면 버튼 한 번에 100명에게 각각 다른 내용의 이메일이 나갑니다. smtplib, email.mime, CSV 조합이면 충분하고, 좀 더 복잡한 템플릿이 필요하면 Jinja2까지 쓰면 돼요. 오늘은 이 과정을 처음부터 끝까지 다 풀어볼게요.

매일 100통씩 보내던 이메일, 왜 자동화가 필요했나

회사에서 매주 월요일마다 거래처에 주간 리포트를 보내거든요. 수신자가 약 80명인데, 각 담당자 이름이 다르고 첨부하는 리포트 파일도 부서별로 달라요. 수동으로 할 때는 이걸 한 사람당 평균 1분 30초씩 걸렸으니까, 단순 계산으로 2시간이 날아갔습니다.

근데 진짜 문제는 시간보다 실수였어요. 이름을 잘못 넣는 건 애교고, 한번은 A사에 보내야 할 견적서를 B사 담당자한테 보낸 적이 있거든요. 다행히 금액 차이가 크지 않아서 큰 일은 안 됐지만, 그날 이후로 “이건 사람이 할 일이 아니다” 싶었습니다.

파이썬을 선택한 건 단순해요. 별도 프로그램 설치 없이 기본 라이브러리(smtplib, email)만으로 가능하고, CSV 파일에서 수신자 데이터를 읽어와서 for 루프 한 번 돌리면 끝이니까요. 엑셀 다룰 줄 알고 파이썬 기초 문법만 알면 누구나 할 수 있어요.

자동화 후에는 같은 작업이 3분이면 끝납니다. CSV 파일 업데이트하고, 스크립트 실행하면 돼요. 남는 시간에 커피를 마실 수 있다는 게 이렇게 행복한 일인 줄 몰랐어요.

Gmail SMTP 연결과 앱 비밀번호 세팅 과정

가장 먼저 해야 할 건 Gmail SMTP 서버에 접속할 수 있도록 세팅하는 거예요. Gmail의 SMTP 서버 주소는 smtp.gmail.com이고, 포트는 TLS 기준 587번을 사용합니다. SSL을 쓴다면 465번 포트를 쓰면 되는데, 저는 STARTTLS 방식인 587을 추천해요.

여기서 많은 분들이 막히는 게 비밀번호 부분인데요. 2024년부터 구글은 “보안 수준이 낮은 앱 허용” 옵션을 완전히 없앴거든요. 그래서 반드시 앱 비밀번호를 발급받아야 합니다. 절차는 이래요. 구글 계정 관리 페이지에 들어가서, 보안 탭을 클릭하고, 2단계 인증을 먼저 활성화해요. 그다음 검색창에 “앱 비밀번호”를 검색하면 생성 페이지가 나옵니다. 앱 이름을 아무거나 입력하고 만들기를 누르면 16자리 비밀번호가 뜨는데, 이걸 복사해서 코드에 넣으면 돼요.

⚠️ 주의

앱 비밀번호는 생성 직후 딱 한 번만 보여줍니다. 창을 닫으면 다시 확인할 수 없어요. 저는 처음에 이걸 몰라서 비밀번호를 메모하지 않고 닫았다가, 삭제 후 재발급받느라 10분을 허비했거든요. 반드시 생성 즉시 안전한 곳에 저장하세요. 그리고 코드에 비밀번호를 직접 하드코딩하지 말고, 환경 변수(os.environ)나 .env 파일로 관리하는 게 보안상 훨씬 안전합니다.

기본적인 SMTP 연결 코드는 이렇게 생겼어요. smtplib.SMTP로 서버에 연결하고, starttls()로 암호화 통신을 시작한 다음, login()으로 인증하는 구조입니다. 이 세 줄이 이메일 자동화의 뼈대예요.

참고로 네이버 메일을 쓴다면 SMTP 서버는 smtp.naver.com, 포트는 587이에요. 다음 메일은 smtp.daum.net에 465 포트를 쓰고요. 어떤 메일 서비스를 쓰든 구조는 동일하니까, 서버 주소와 포트만 바꾸면 됩니다.

CSV + 템플릿으로 이름·내용 맞춤형 발송하기

여기가 핵심이에요. 100명에게 똑같은 메일을 보내는 건 BCC로도 되지만, 각각 이름이나 금액, 날짜가 다른 맞춤형 이메일을 보내려면 CSV 파일과 파이썬 템플릿을 조합해야 합니다.

먼저 CSV 파일을 이렇게 만들어요. 첫 번째 열에 이름, 두 번째 열에 이메일 주소, 세 번째 열에 맞춤 내용(예: 견적 금액)을 넣습니다. 엑셀에서 작성하고 CSV로 저장하면 편해요.

파이썬 코드에서는 csv 모듈로 이 파일을 읽고, for 루프를 돌면서 각 행의 데이터를 메시지 템플릿에 .format()으로 삽입합니다. 예를 들어 message 변수에 “안녕하세요 {name}님, 이번 달 견적 금액은 {amount}원입니다”라고 써두면, CSV에서 읽은 이름과 금액이 자동으로 채워지는 거예요.

제가 실제로 쓰는 구조를 좀 더 구체적으로 말하면, MIMEMultipart(“alternative”)로 메시지 객체를 만들고, Subject·From·To 헤더를 설정한 다음, MIMEText로 본문을 붙여요. 이때 본문에 HTML을 넣으면 깔끔한 서식의 이메일이 가능합니다. 중요한 건 next(reader)로 CSV 헤더 행을 건너뛰는 것, 그리고 sendmail() 호출 시 수신자 이메일을 정확히 넣는 것, 이 두 가지만 기억하면 돼요.

💬 직접 써본 경험

처음 이 코드를 돌렸을 때, CSV 파일의 인코딩이 EUC-KR이라서 한글 이름이 전부 깨졌거든요. open(“contacts.csv”, encoding=”utf-8″)로 명시적으로 지정하니까 바로 해결됐는데, 이거 모르면 한참 헤맵니다. 엑셀에서 CSV 저장할 때 “CSV UTF-8” 옵션을 선택하는 것도 잊지 마세요.

HTML 본문과 첨부파일까지 한 번에 보내는 코드

단순 텍스트 메일은 좀 밋밋하잖아요. 회사 로고 넣고, 표 형식으로 정리하고, PDF 견적서를 첨부하고 싶은 경우가 대부분일 거예요. 이때 필요한 게 MIMEMultipart(“mixed”)입니다.

“alternative”는 텍스트와 HTML 중 하나를 선택해서 보여주는 용도고, “mixed”는 본문 + 첨부파일을 함께 묶는 용도예요. 구조를 정리하면 이렇습니다. MIMEMultipart(“mixed”)를 최상위로 만들고, 그 안에 MIMEText(html, “html”)로 HTML 본문을 붙이고, MIMEBase(“application”, “octet-stream”)으로 첨부파일을 붙이는 거예요.

첨부파일 처리에서 놓치기 쉬운 게 두 가지 있어요. 하나는 파일을 “rb” (바이너리 읽기) 모드로 열어야 한다는 것, 다른 하나는 encoders.encode_base64()로 반드시 인코딩해야 한다는 거예요. 이걸 빠뜨리면 첨부파일이 깨져서 열리지 않습니다.

구분 MIMEMultipart(“alternative”) MIMEMultipart(“mixed”)
용도 텍스트/HTML 택1 표시 본문 + 첨부파일 동시 전송
첨부파일 불가 PDF, 이미지, 엑셀 등 가능
주로 쓰는 상황 안내 메일, 뉴스레터 견적서·보고서 발송
코드 복잡도 낮음 중간 (인코딩 처리 필요)

실제로 저는 두 가지를 결합해서 씁니다. MIMEMultipart(“mixed”)를 최상위에 두고, 그 안에 MIMEMultipart(“alternative”)를 넣어서 텍스트 폴백과 HTML 본문을 동시에 제공하고, 같은 레벨에 첨부파일을 붙여요. 이렇게 하면 HTML을 지원하지 않는 메일 클라이언트에서도 텍스트 버전이 보이거든요. 처음엔 좀 복잡해 보이지만, 한번 구조를 만들어두면 계속 재사용할 수 있어요.

스팸 차단과 발송 한도, 실제로 겪은 문제들

이 부분을 모르고 100통을 한꺼번에 쏘면 큰일 납니다. 제가 직접 겪었거든요.

Gmail 무료 계정은 하루 500통이 발송 한도예요. Google Workspace(유료) 계정은 하루 2,000통까지 가능하고, 대량 발송(다중 전송)의 경우 1,500통으로 제한됩니다. 이 한도를 넘기면 24시간 동안 메일 발송 자체가 차단돼요. 저는 테스트 중에 for 루프를 딜레이 없이 돌렸다가, 약 70통쯤 보내고 “전송 제한에 도달했습니다” 에러를 만났어요. 500통 이하인데도 너무 빠르게 보내면 구글이 비정상 활동으로 감지하는 거예요.

해결책은 간단합니다. time.sleep()으로 메일 사이에 딜레이를 넣으면 돼요. 저는 한 통당 3~5초 간격을 두는데, 100통 기준으로 총 5~8분 정도 걸립니다. 이 정도면 스팸 필터에 걸리지 않으면서도 충분히 빨라요.

스팸함에 빠지는 것도 무시 못 할 문제예요. 구글의 이메일 발신자 가이드라인을 보면, SPF·DKIM·DMARC 같은 이메일 인증이 설정되어 있지 않으면 수신측에서 스팸으로 분류할 확률이 높아진다고 나와 있습니다. 개인 Gmail 계정으로 보내는 경우 SPF는 구글이 알아서 처리해주지만, 자체 도메인을 쓴다면 DNS에 SPF 레코드를 반드시 추가해야 해요.

📊 실제 데이터

Gmail 무료 계정 일일 발송 한도는 500통, Google Workspace 계정은 2,000통(대량 발송 시 1,500통)입니다. 한도 초과 시 24시간 동안 발송이 차단되며, 비정상적으로 빠른 발송 패턴도 차단 사유가 됩니다. 구글 이메일 발신자 가이드라인에 따르면, 하루 5,000통 이상 보내는 발신자는 SPF·DKIM·DMARC 인증이 필수입니다.

흔한 오해 하나를 바로잡자면, “BCC로 보내면 대량 발송이 아니다”라고 생각하는 분들이 있어요. 아닙니다. BCC든 개별 발송이든 구글은 총 발송 수를 카운트해요. 오히려 BCC로 한 번에 100명에게 보내면 수신측 메일 서버가 스팸으로 판단할 가능성이 더 높아요. 개별 발송이 스팸 회피에는 유리합니다.

Jinja2 템플릿 엔진으로 한 단계 업그레이드

.format()으로 변수를 치환하는 건 간단한 메일엔 충분해요. 그런데 조건부 문구를 넣고 싶을 때가 있거든요. “VIP 고객에게는 할인 코드를 넣고, 일반 고객에게는 넣지 않는다”처럼요. 이때 Jinja2가 빛을 발합니다.

Jinja2는 pip install jinja2로 설치하는 템플릿 엔진인데, HTML 파일 안에 {{ name }}이나 {% if vip %}…{% endif %} 같은 문법을 쓸 수 있어요. 별도의 HTML 템플릿 파일을 만들어두고, 파이썬에서 Template 객체의 render() 메서드에 딕셔너리를 넘기면 완성된 HTML이 반환됩니다.

제가 이걸 도입한 이유가 있어요. 월말 보고 메일에서 매출이 전월 대비 증가한 거래처에는 “축하” 문구를, 감소한 곳에는 “지원 방안” 문구를 넣고 싶었거든요. .format()으로는 이런 분기 처리가 깔끔하지 않은데, Jinja2에서는 {% if growth > 0 %} 한 줄이면 끝이에요. CSV에 growth 열을 추가하고, 템플릿에서 조건 분기를 태우니까 완전히 다른 세상이었습니다.

다만 Jinja2를 쓸 때 한 가지 주의할 점이요. 템플릿 파일의 인코딩을 UTF-8로 맞춰야 하고, Environment 객체를 생성할 때 FileSystemLoader에 템플릿 디렉토리 경로를 정확히 지정해야 합니다. 경로가 틀리면 TemplateNotFound 에러가 나는데, 이게 메시지가 좀 불친절해서 처음에 원인 파악에 시간 걸렸어요.

3개월 운영 후 정리한 실전 팁과 주의사항

3개월 동안 매주 100통 이상을 자동 발송하면서 쌓인 노하우를 정리해볼게요.

첫째로, 에러 핸들링을 반드시 넣어야 해요. try-except로 smtplib.SMTPException을 잡고, 실패한 수신자를 별도 리스트에 저장해두면 나중에 재발송이 가능합니다. 저는 처음에 이걸 안 넣었다가, 50번째 메일에서 네트워크 에러가 나면서 나머지 50통이 전부 안 나간 적이 있어요. 루프가 그냥 멈춰버린 거예요.

로그를 남기는 것도 중요합니다. print()만으로는 부족하고, logging 모듈을 써서 발송 시각, 수신자 이메일, 성공/실패 여부를 파일에 기록해두세요. 나중에 “이 사람한테 메일 갔나?”라는 질문에 바로 답할 수 있거든요.

💡 꿀팁

비밀번호를 코드에 직접 적지 마세요. python-dotenv 라이브러리를 설치하고, 프로젝트 폴더에 .env 파일을 만들어서 EMAIL_PASSWORD=xxxx 형식으로 저장한 뒤, os.environ.get(“EMAIL_PASSWORD”)로 불러오는 게 정석입니다. .env 파일은 반드시 .gitignore에 추가하세요. 깃허브에 앱 비밀번호가 올라가면 구글이 자동으로 비밀번호를 폐기합니다.

SMTP 연결을 매번 새로 여는 것보다, with 문을 써서 한 번 열고 루프 안에서 sendmail()을 반복 호출하는 게 훨씬 빠릅니다. 연결 자체에 2~3초가 걸리거든요. 100통이면 연결만으로 200~300초가 추가되는 셈이니까, 이건 꼭 최적화해야 해요.

마지막으로 테스트 환경을 별도로 만들어두세요. 제 경우엔 본인 이메일 주소 3~4개를 CSV에 넣고 먼저 테스트 발송을 합니다. 실제 거래처에 “테스트 메일입니다 {name}님”이 나가는 참사를 한 번 겪고 나서 이 습관이 생겼어요. 그 메일을 회수할 수 없다는 사실이 그날 하루 종일 마음에 걸렸습니다.

자주 묻는 질문

Q. Gmail 말고 네이버·다음 메일로도 대량 발송이 가능한가요?

네, 가능합니다. 네이버는 smtp.naver.com 포트 587, 다음은 smtp.daum.net 포트 465를 사용하면 돼요. 다만 네이버는 하루 발송 한도가 Gmail보다 적을 수 있으니 공식 고객센터에서 최신 정책을 확인하는 걸 추천합니다.

Q. 100통 이상을 보내야 할 때는 어떤 방법이 좋나요?

500통 이상이라면 Gmail SMTP 대신 SendGrid, Mailgun, Amazon SES 같은 전문 이메일 서비스를 쓰는 게 낫습니다. API 방식이라 속도도 빠르고, 발송 리포트와 바운스 관리까지 지원해요.

Q. 첨부파일 용량 제한이 있나요?

Gmail 기준으로 첨부파일 포함 전체 메일 크기가 25MB를 넘을 수 없어요. 큰 파일은 구글 드라이브 링크로 공유하는 게 현실적입니다. base64 인코딩 시 원본 대비 약 33% 용량이 증가하는 것도 계산에 넣어야 합니다.

Q. 발송 중간에 에러가 나면 처음부터 다시 보내야 하나요?

try-except로 에러를 잡고, 실패한 수신자만 별도 리스트에 담아두면 됩니다. 성공/실패 로그를 CSV로 저장해두면, 실패 목록만 추출해서 재발송 스크립트를 돌릴 수 있어요.

Q. 수신자가 메일을 읽었는지 확인할 수 있나요?

smtplib 자체로는 수신 확인이 불가능합니다. 읽음 확인이 필요하다면 HTML 본문에 1×1 픽셀 트래킹 이미지를 삽입하거나, Mailgun·SendGrid 같은 서비스의 웹훅 기능을 활용해야 해요. 다만 트래킹 이미지는 수신자의 이미지 로드 설정에 따라 100% 정확하진 않습니다.

본 포스팅은 개인 경험과 공개 자료를 바탕으로 작성되었으며, 전문적인 의료·법률·재무 조언을 대체하지 않습니다. 정확한 정보는 해당 분야 전문가 또는 공식 기관에 확인하시기 바랍니다.

파이썬 smtplib와 CSV 조합이면 100명 맞춤형 이메일 발송을 3분 만에 끝낼 수 있고, Jinja2를 더하면 조건 분기까지 가능합니다. 다만 발송 딜레이와 에러 핸들링, 비밀번호 보안은 반드시 챙기세요.

업무 메일이 많은 분이라면 smtplib부터 시작하는 걸 추천드리고, 마케팅 대량 발송이 목적이라면 처음부터 SendGrid나 Amazon SES를 검토해보는 게 시간 낭비를 줄여줄 거예요. 개인 프로젝트나 소규모 업무 자동화에는 오늘 다룬 방법이 비용 제로에 효과는 확실합니다.


이 글이 도움이 됐다면 댓글로 어떤 업무에 활용할 건지 알려주세요. 비슷한 자동화 주제로 더 쓸 예정이니까, 궁금한 게 있으면 편하게 남겨주시고요. 공유도 환영합니다.

댓글 남기기