파이썬 smtplib로 Gmail 메일 보내기 — 앱 비밀번호 발급부터 자동화 실전까지
TL;DR — 파이썬 표준 라이브러리 smtplib로 Gmail 메일을 보내는 방법을 정리했다. 앱 비밀번호 발급, 첨부파일·HTML 본문 처리, 자주 만나는 인증 오류 해결법까지 실무 관점으로 다룬다.
파이썬으로 회원가입 인증 메일을 보내거나, 매일 아침 리포트를 자동 발송하거나, 서버 장애 알림을 메일로 받고 싶을 때 가장 먼저 마주치는 것이 smtplib다. 파이썬 표준 라이브러리에 기본 포함돼 있어 별도 설치가 필요 없고, 코드 몇 줄이면 메일이 날아간다. 그런데 막상 따라 해 보면 십중팔구 SMTPAuthenticationError라는 빨간 글씨를 만난다. 원인은 거의 항상 하나다 — 구글 계정의 일반 비밀번호를 그대로 넣었기 때문이다.
이 글에서는 smtplib의 동작 원리를 짚고, Gmail 앱 비밀번호(App Password)를 발급받는 정확한 절차, 그리고 단순 텍스트 메일을 넘어 HTML 본문과 첨부파일까지 보내는 실전 코드를 단계별로 정리한다.
왜 일반 비밀번호로는 안 될까
2022년 5월부터 구글은 "보안 수준이 낮은 앱"의 비밀번호 직접 로그인을 전면 차단했다. 즉, 파이썬 스크립트가 평소 쓰던 계정 비밀번호로 SMTP 로그인을 시도하면 구글이 이를 거부한다. 대신 구글은 앱 비밀번호라는 16자리 전용 키를 발급해 준다. 이 키는 특정 애플리케이션 하나에만 권한을 주는 1회성 토큰 같은 개념이라, 유출돼도 메인 계정 비밀번호는 안전하고 언제든 해당 키만 폐기할 수 있다.
정리하면 메일 자동화의 전제 조건은 다음 두 가지다.
- 구글 계정에 2단계 인증(2FA)이 켜져 있을 것 — 앱 비밀번호 메뉴는 2FA가 켜져야만 나타난다.
- 발급받은 16자리 앱 비밀번호를 코드의 로그인 비밀번호 자리에 사용할 것.
앱 비밀번호 발급 절차
화면 구성은 구글 정책에 따라 조금씩 바뀌지만, 핵심 경로는 동일하다.
- 2단계 인증 활성화: Google 계정 보안 페이지에 들어가 "2단계 인증"을 먼저 켠다. 이게 안 켜져 있으면 다음 단계 메뉴 자체가 보이지 않는다.
- 앱 비밀번호 페이지 이동: 검색창에 "앱 비밀번호"를 입력하거나 보안 페이지에서 해당 항목을 찾아 들어간다.
- 앱 이름 지정: "앱 이름" 입력란에
python-mailer처럼 식별 가능한 이름을 넣고 생성한다. 나중에 어떤 스크립트용 키인지 구분하기 쉽다. - 16자리 키 확보: 생성 직후 공백으로 구분된 16자리 문자열(예:
abcd efgh ijkl mnop)이 한 번만 노출된다. 이걸 복사해 둔다. 코드에 넣을 때는 공백을 빼도 되고 그대로 넣어도 인증된다.
이 16자리가 곧 SMTP 로그인 비밀번호다. 창을 닫으면 다시 볼 수 없으니, 그 자리에서 안전한 곳(환경변수, 비밀 관리 도구)에 보관하자.
1단계 — 가장 단순한 텍스트 메일
먼저 동작 원리를 이해하기 위해 최소 코드부터 보자. Gmail SMTP 서버는 smtp.gmail.com이며, TLS 암호화에는 587 포트를 쓴다.
import smtplib
from email.mime.text import MIMEText
SENDER = "[email protected]" # 발신 계정
APP_PASSWORD = "abcdefghijklmnop" # 16자리 앱 비밀번호
RECEIVER = "[email protected]" # 수신자
# 본문과 헤더 구성
message = MIMEText("파이썬 smtplib 테스트 메일입니다.", _charset="utf-8")
message["Subject"] = "자동 발송 테스트"
message["From"] = SENDER
message["To"] = RECEIVER
# SMTP 연결 → TLS 시작 → 로그인 → 전송
server = smtplib.SMTP("smtp.gmail.com", 587)
server.starttls() # 평문 연결을 암호화로 업그레이드
server.login(SENDER, APP_PASSWORD) # 여기서 앱 비밀번호 사용
server.sendmail(SENDER, RECEIVER, message.as_string())
server.quit() # 연결 정리
print("메일 전송 완료")
starttls()는 처음에 평문으로 맺은 연결을 TLS 암호화로 끌어올리는 단계다. 이 호출을 빠뜨리면 구글이 인증 자체를 거부하므로 반드시 login() 앞에 와야 한다.
2단계 — with 문으로 안전하게
위 코드는 중간에 예외가 나면 quit()이 실행되지 않아 연결이 남는다. 실무에서는 with 구문을 써서 연결을 자동으로 닫는 편이 안전하다. 또 비밀번호는 코드에 박지 말고 환경변수에서 읽어 오자.
import os
import smtplib
from email.mime.text import MIMEText
SENDER = os.environ["GMAIL_USER"] # 환경변수에서 계정 읽기
APP_PASSWORD = os.environ["GMAIL_APP_PW"] # 환경변수에서 앱 비밀번호 읽기
def send_plain_mail(to_addr: str, subject: str, body: str) -> None:
msg = MIMEText(body, _charset="utf-8")
msg["Subject"] = subject
msg["From"] = SENDER
msg["To"] = to_addr
# with 블록을 벗어나면 자동으로 연결이 닫힌다
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
smtp.starttls()
smtp.login(SENDER, APP_PASSWORD)
smtp.send_message(msg) # sendmail보다 헤더 처리가 깔끔하다
send_plain_mail("[email protected]", "리포트", "오늘의 처리 건수: 1,204건")
send_message()는 sendmail()과 달리 메시지 객체의 From/To 헤더를 알아서 읽어 주므로 인자가 간결하다.
3단계 — HTML 본문과 첨부파일 보내기
실무에서는 표나 버튼이 들어간 HTML 메일, 그리고 CSV·이미지 같은 첨부가 필요한 경우가 많다. 이때는 MIMEMultipart로 본문과 첨부를 하나의 봉투에 담는다.
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
SENDER = os.environ["GMAIL_USER"]
APP_PASSWORD = os.environ["GMAIL_APP_PW"]
def send_rich_mail(to_addr: str, subject: str, html: str, file_path: str) -> None:
msg = MIMEMultipart() # 여러 파트를 담는 컨테이너
msg["Subject"] = subject
msg["From"] = SENDER
msg["To"] = to_addr
# HTML 본문 파트 추가
msg.attach(MIMEText(html, "html", _charset="utf-8"))
# 첨부파일 파트 추가
with open(file_path, "rb") as f:
part = MIMEApplication(f.read())
filename = os.path.basename(file_path)
part.add_header("Content-Disposition", "attachment", filename=filename)
msg.attach(part)
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
smtp.starttls()
smtp.login(SENDER, APP_PASSWORD)
smtp.send_message(msg)
html_body = """
<h2>일일 처리 리포트</h2>
<p>첨부된 CSV에서 상세 내역을 확인하세요.</p>
"""
send_rich_mail("[email protected]", "일일 리포트", html_body, "report.csv")
한글 파일명이 깨진다면 add_header에 filename=("utf-8", "", "리포트.csv") 형태의 튜플을 넘기면 RFC 2231 방식으로 인코딩돼 정상 표시된다.
흔한 오류와 해결법
SMTPAuthenticationError: Username and Password not accepted
가장 빈번한 오류. 원인 1순위는 일반 비밀번호 사용이다. 앱 비밀번호 16자리를 넣었는지 확인하자. 2FA가 꺼져 있어 앱 비밀번호를 발급조차 못 받았을 수도 있다.
SMTPServerDisconnected 또는 타임아웃
회사 네트워크나 일부 클라우드 환경에서 587 포트가 막혀 있는 경우다. SSL 전용인 465 포트 + smtplib.SMTP_SSL 조합으로 우회할 수 있다. 이 방식은 starttls()가 필요 없다.
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login(SENDER, APP_PASSWORD) # SSL_SMTP는 starttls 호출 불필요
smtp.send_message(msg)
한글 제목·본문이 =?...?= 처럼 깨짐
MIMEText에 _charset="utf-8"을 명시했는지 확인한다. 제목에 한글을 쓸 때는 email.header.Header로 감싸 주면 더 안전하다.
메일이 스팸함으로 분류됨 발신량이 갑자기 많거나 본문에 링크만 가득하면 스팸 처리될 수 있다. 발신자 표시 이름을 명확히 하고, 대량 발송은 시간 간격을 두는 것이 좋다.
Gmail 일일 발송 한도 일반 Gmail 계정은 하루 약 500통 제한이 있다. 대량·운영용 발송이라면 SendGrid·Amazon SES 같은 전용 서비스를 검토하는 편이 안정적이다.
보안 체크리스트
- 앱 비밀번호를 소스 코드나 깃 저장소에 절대 커밋하지 않는다. 환경변수나
.env(+.gitignore)로 분리한다. - 키가 유출됐다고 의심되면 구글 보안 페이지에서 해당 앱 비밀번호만 즉시 폐기하면 된다. 메인 계정은 영향이 없다.
- 스크립트마다 별도의 앱 비밀번호를 발급하면 어느 스크립트가 문제인지 추적하고 회수하기 쉽다.
요약
- Gmail 메일 자동화의 핵심은 코드가 아니라 앱 비밀번호 발급이다. 2FA를 켜고 16자리 키를 받아 일반 비밀번호 자리에 넣는다.
- 연결은
smtp.gmail.com:587 + starttls()또는465 + SMTP_SSL두 가지 경로가 있다. - 단순 메일은
MIMEText, HTML·첨부는MIMEMultipart로 구성한다. SMTPAuthenticationError는 거의 항상 비밀번호 문제이고, 타임아웃은 포트 문제다.
AI에게 물어볼 때 (프롬프트 팁)
이 주제를 ChatGPT나 Claude에게 물어볼 때, 막연히 "파이썬으로 메일 보내는 법 알려줘"라고 하면 일반론만 돌아온다. 상황·제약·기대 출력을 함께 넣으면 바로 쓸 수 있는 코드를 얻는다.
- "파이썬 smtplib로 Gmail HTML 메일에 CSV 첨부를 보내는 함수를 작성해줘. 인증은 환경변수에서 읽는 16자리 앱 비밀번호를 쓰고, 587 포트가 막힌 환경을 대비해 465 SSL 폴백 로직도 포함해줘. 예외 처리와 한글 주석을 달아줘."
- "
SMTPAuthenticationError: Username and Password not accepted가 나는데, 가능한 원인을 가장 흔한 순서대로 나열하고 각각의 확인 방법을 단계별로 알려줘. 내 코드는 다음과 같아: [코드 붙여넣기]" - "사내 배치 서버에서 매일 오전 9시에 리포트를 메일 발송하려고 해. Gmail 일일 한도와 스팸 분류를 피하기 위한 발송 설계 원칙을 알려주고, 운영용이라면 SES/SendGrid 전환 기준도 비교해줘."
좋은 프롬프트의 공통점은 제약(포트·인증 방식), 입력 데이터(에러 로그·코드), **원하는 출력 형식(주석·폴백·비교표)**을 명시한다는 점이다. 더 정교한 프롬프트 설계가 필요하다면 Prompt Architect의 분석기로 작성한 프롬프트의 구체성과 완성도를 점검해 보길 권한다.