Application/Python

[Python] 15. 파이썬으로 양식 일괄 발송 시스템 만들기 (엑셀 명단 → 개인 문서 → 자동 메일)

devsalix 2026. 7. 22. 13:36
728x90

파이썬으로 양식 일괄 발송 시스템 만들기 (엑셀 명단 → 개인 문서 → 자동 메일)

100명한테 이름도 다르고 금액도 다른 안내문을 하나씩 만들어서, 그걸 또 각자 이메일로 보내본 적 있으세요?

문서 100개를 이름 바꿔가며 저장하고, 메일 창을 100번 열어서 첨부하고 전송 버튼 누르고... 하다 보면 어느새 밤 아홉 시예요. 중간에 한 명 이름 잘못 넣은 거 발견하면 정말 눈물 나잖아요.

그런데 여기서 재밌는 사실 하나. 이 반나절짜리 야근을, 우리가 지금까지 배운 것만으로 스크립트 한 번에 끝낼 수 있어요.

엑셀 명단 읽는 법(05), Word 문서 자동으로 만드는 법(12), 메일 보내는 법(13/14). 이 조각들이 다 따로따로 배운 거였는데, 사실 이게 전부 하나로 이어지는 부품이었던 거예요. 오늘은 새 문법을 배우는 게 아니라, 그 부품들을 딱 맞게 끼워 조립해볼 거예요.

이런 걸 원래 메일 머지(mail merge) 라고 불러요. 하나의 양식에 명단 데이터를 끼워 넣어서 개인화된 문서를 여러 개 찍어내는 거죠. MS워드에도 있는 기능인데, 파이썬으로 하면 "문서 만들기"에서 멈추지 않고 "메일 발송"까지 쭉 이어붙일 수 있다는 게 진짜 강점이에요.


우리가 만들 양식 자동 발송 시스템 미리보기

전체 그림부터 그려볼게요. 딱 세 단계예요.

  1. 엑셀 명단을 읽는다 (이름, 이메일, 금액이 적힌 표)
  2. 사람마다 개인화된 Word 문서를 만든다
  3. 그 문서를 첨부해서 각자에게 메일을 보낸다

이걸 코드로 옮길 때 한 덩어리로 몰아 쓰면 나중에 손대기가 정말 힘들어요. 그래서 단계마다 함수로 쪼갤 거예요. read_recipients(명단 읽기), make_document(문서 만들기), send_mail(메일 보내기), 그리고 이걸 다 지휘하는 main. 이렇게요.

함수로 나눠두면 "문서 내용만 고치고 싶다" 할 때 make_document만 열면 되거든요. 나머지는 안 건드려도 돼요. 이게 오늘의 진짜 배움이에요. 새 기능이 아니라, 조립하는 법.


Step 1. 엑셀 명단 읽기 (05 기억나시죠?)

 

[Python] 05. 파이썬 엑셀 자동화, openpyxl로 시작하기

파이썬 엑셀 자동화, openpyxl로 시작하기엑셀 파일에 데이터를 일일이 옮겨 적다가 야근해 보신 적, 다들 한 번쯤은 있으시잖아요. 거래처에서 보내준 PDF 보면서 셀에 숫자 옮기고, 월말 되면 부

devsalix.tistory.com

 

먼저 명단을 읽어와야죠. 엑셀에 이런 표가 있다고 해볼게요.

이름 이메일 금액
홍길동 hong@example.com 50000
김영희 kim@example.com 72000

이걸 openpyxl로 읽어서, 사람 한 명을 딕셔너리 하나로 만들 거예요.

from openpyxl import load_workbook  # 엑셀 읽는 도구 (05에서 배웠어요)

def read_recipients(path):
    wb = load_workbook(path)  # 엑셀 파일 열기
    ws = wb.active            # 첫 번째 시트 선택
    people = []               # 사람들을 담을 빈 리스트

    # min_row=2 → 첫 줄(헤더 "이름/이메일/금액")은 건너뜁니다
    for row in ws.iter_rows(min_row=2, values_only=True):
        name, email, amount = row[0], row[1], row[2]  # 각 칸을 나눠 담기
        if not email:        # 이메일 칸이 비어 있으면 (빈 줄)
            continue         # 그 줄은 건너뛰기
        people.append({"name": name, "email": email, "amount": amount})

    return people  # [{"name": "홍길동", "email": "...", "amount": 50000}, ...]

# 실행 결과 (people 안에 이렇게 들어와요):
# [{'name': '홍길동', 'email': 'hong@example.com', 'amount': 50000},
#  {'name': '김영희', 'email': 'kim@example.com', 'amount': 72000}]

 

왜 딕셔너리로 만드냐면요, 뒤에서 person["name"]처럼 이름표로 꺼내 쓸 수 있어서 코드가 훨씬 읽기 쉬워지거든요. row[0] 이렇게 숫자로 부르는 것보다 person["name"]이 딱 봐도 뭔지 알잖아요.

여기서 min_row=2, 이거 진짜 중요해요. 첫 줄은 "이름/이메일/금액" 같은 제목 줄이라 이걸 안 건너뛰면 "이름"이라는 사람한테 메일을 보내려고 시도하다 사고 나요.


Step 2. 한 사람용 Word 문서 만들기 — 파이썬 문서 자동화 (12 복습)

 

[Python] 12. 파이썬으로 Word 문서 자동 생성하기 (python-docx)

파이썬으로 Word 문서 자동 생성하기 (python-docx)이름이랑 날짜만 바꾼 똑같은 공문을 30번 새로 타이핑해 본 적 있으세요? 양식은 그대로인데 말이죠. 안내문, 확인서, 참석 요청 공문... 이런 걸 사

devsalix.tistory.com

 

이제 사람 한 명을 받아서, 그 사람만의 문서를 만드는 함수예요. 파이썬 문서 자동화의 핵심 조각이죠.

from docx import Document  # Word 문서 만드는 도구 (12)

def make_document(person):
    doc = Document()                          # 빈 문서 새로 만들기
    doc.add_heading("정산 안내문", level=1)   # 제목 넣기
    # f-string으로 이름과 금액을 문장에 끼워 넣습니다 (이게 메일 머지의 핵심!)
    doc.add_paragraph(f"{person['name']}님, 이번 달 정산 금액은 {person['amount']}원입니다.")

    # 파일명에 이름을 넣어 서로 안 겹치게! (이거 안 하면 다 덮어써요)
    path = f"output/안내문_{person['name']}.docx"
    doc.save(path)  # 저장
    return path     # 저장한 경로를 돌려줍니다 (다음 단계에서 첨부할 때 씀)

 

여기서 꼭 짚고 싶은 게 파일명이에요. 만약 모든 문서를 그냥 안내문.docx로 저장하면 어떻게 될까요? 홍길동 문서 만들고, 그다음 김영희 문서를 같은 이름으로 저장하면서 홍길동 걸 덮어써 버려요. 결국 파일 하나만 남죠. 그래서 안내문_{이름}.docx처럼 이름을 넣어 구분해줘요.

그리고 함수 마지막에 return path, 이게 왜 있냐면요. 방금 만든 문서를 다음 단계에서 첨부해야 하는데, "그 파일이 어디 저장됐는지"를 알려줘야 하잖아요. 그래서 경로를 돌려주는 거예요.


Step 3. 한 사람에게 첨부 메일 보내기 (13/14 복습)

 

[Python] 13. 파이썬 이메일 자동 발송: smtplib 기초

파이썬 이메일 자동 발송: smtplib 기초지난 글에서 공문 30장을 코드로 순식간에 찍어냈잖아요. 그런데 다 만들고 나면 바로 이 생각이 들죠."이걸 또 30명한테 일일이 메일로 보내야 하나…"매일

devsalix.tistory.com

 

 

[Python] 14. 파이썬으로 첨부파일과 HTML 메일 보내기

파이썬으로 첨부파일과 HTML 메일 보내기매달 말이면 비슷한 일을 하시죠. 보고서 엑셀 만들고, 메일 새로 쓰고, 폴더에서 파일 찾아 첨부하고, 받는 사람 주소 다시 확인하고.한 번에 5분이지만

devsalix.tistory.com

 

문서를 만들었으니 이제 보내야죠. 앞에서 돌려받은 파일 경로를 받아서 첨부해요.

from email.message import EmailMessage  # 메일 만드는 도구 (13/14)

def send_mail(person, file_path, server):
    msg = EmailMessage()
    msg["Subject"] = f"{person['name']}님 정산 안내문"  # 제목도 개인화
    msg["From"] = SENDER
    msg["To"] = person["email"]  # 받는 사람은 '그 한 명'만! (중요, 뒤에서 다시)
    msg.set_content(f"{person['name']}님, 첨부된 안내문을 확인해 주세요.")

    # Step 2에서 만든 문서를 첨부합니다
    with open(file_path, "rb") as f:  # rb = 바이너리로 파일 열기
        msg.add_attachment(
            f.read(),
            maintype="application",
            subtype="octet-stream",
            filename=file_path.split("/")[-1]  # 경로에서 파일명만 뽑아 첨부명으로
        )
    server.send_message(msg)  # 발송!

 

msg["To"]딱 한 명만 넣는 거, 이거 밑줄 쫙 그어놓으세요. 나중에 왜 이게 그렇게 중요한지 따로 얘기할게요.


Step 4. 반복문으로 전원에게

부품 세 개가 다 준비됐어요. 이제 명단을 쭉 돌면서 한 명씩 "문서 만들고 → 메일 보내고"를 반복하면 돼요.

for person in people:          # 명단의 사람들을 한 명씩
    path = make_document(person)  # 1) 그 사람 문서 만들고
    send_mail(person, path, server)  # 2) 그 문서 첨부해서 발송

 

이게 사실상 완성이에요. 세 줄이면 끝나잖아요? 그런데... 이대로 100명을 돌리면 큰일 나요. 왜 그런지가 오늘 이 편의 진짜 핵심이에요.


Step 5. 한 명 실패해도 나머지는 계속 가야 해요

상상해볼게요. 100명 명단을 돌리는데, 세 번째 사람 이메일에 오타가 있어요. kimexample.com처럼 @가 빠졌다고 해봐요.

위 코드 그대로면, 세 번째에서 에러가 나면서 프로그램이 그냥 죽어버려요. 앞의 두 명은 갔지만, 나머지 97명은 아예 시작도 못 해요. 그런데 여러분은 그걸 몰라요. "다 보냈겠지" 하고 퇴근하죠. 다음 날 "저 안 받았는데요?" 문의가 97건 쏟아지는 거예요.

솔직히 이 상황을 한 번 겪으면 try/except가 왜 필요한지 뼈저리게 느껴요. 저도 그랬거든요. 예외 처리는 선택이 아니라, 이런 반복 작업에선 생명줄이에요.

results = []  # 성공/실패 결과를 기록할 리스트

for person in people:
    try:  # 이 사람 처리 중 에러가 나도
        path = make_document(person)
        send_mail(person, path, server)
        results.append((person["name"], "성공"))
        print(f"[성공] {person['name']}")
    except Exception as e:  # 죽지 말고, 실패로 기록만 하고 다음 사람으로
        results.append((person["name"], f"실패: {e}"))
        print(f"[실패] {person['name']} - {e}")
    time.sleep(2)  # 한 통 보내고 2초 쉬기 (뒤에서 설명해요)

# 실행 결과 예시:
# [성공] 홍길동
# [성공] 김영희
# [실패] 이철수 - 잘못된 이메일 주소
# [성공] 박민지

 

이렇게 감싸두면 이철수 씨한테서 에러가 나도 프로그램은 안 죽어요. "이철수 실패"라고 적어두고 박민지 씨한테 넘어가요. 그리고 마지막에 results를 보면 누가 안 갔는지 한눈에 보이죠. 이게 있고 없고가 하늘과 땅 차이예요.


진짜 발송 전에, 나한테 먼저 보내보세요

여기가 오늘 제일 중요한 부분이에요. 밑줄 세 번 치고 싶은 곳.

메일은 Ctrl+Z가 없어요. 한번 나가면 되돌릴 방법이 없거든요. 100명한테 오타 난 안내문을 보내고 나서 "아 잘못 보냈다" 해봤자 이미 늦었어요.

그래서 진짜 전체 발송 전에, 명단 전체가 아니라 나한테만 1명분을 먼저 보내보는 리허설이 꼭 필요해요. 이걸 스위치 하나로 만들어 둘게요.

TEST_MODE = True         # True면 리허설(나에게만), False면 진짜 전체 발송
MY_EMAIL = "myid@gmail.com"

# send_mail 함수 안의 To 부분만 이렇게 바꿔요:
msg["To"] = MY_EMAIL if TEST_MODE else person["email"]
# TEST_MODE가 True인 동안은 누구 차례든 전부 '내 주소'로 옵니다

 

TEST_MODE = True로 두고 한 번 돌려보세요. 그러면 명단이 홍길동이든 김영희든, 메일은 전부 내 받은편지함으로 와요. 제목 맞나, 금액 맞게 들어갔나, 첨부 잘 붙었나 내 눈으로 확인하는 거죠.

다 확인하고 "이제 진짜 보내도 되겠다" 싶을 때 TEST_MODE = False로 바꾸고 실행. 이 습관 하나가 여러분을 대형 사고에서 구해줘요. 저는 이거 없이 발송했다가 식겁한 적 있어서, 진심으로 권해요.


전체 조립 코드 (main)

이제 부품을 다 모아 main 하나로 지휘해볼게요. 위에서 본 조각들이 그대로 들어가요.

import smtplib
import time
from email.message import EmailMessage
from openpyxl import load_workbook
from docx import Document

# --- 설정 (여기만 내 값으로 바꾸면 돼요) ---
SENDER = "myid@gmail.com"        # 보내는 사람
APP_PASSWORD = "앱비밀번호16자리"  # 13에서 만든 Gmail 앱 비밀번호
TEST_MODE = True                 # True면 나에게만! (리허설)
MY_EMAIL = "myid@gmail.com"

def read_recipients(path):
    wb = load_workbook(path)
    ws = wb.active
    people = []
    for row in ws.iter_rows(min_row=2, values_only=True):  # 헤더 건너뜀
        name, email, amount = row[0], row[1], row[2]
        if not email:            # 빈 줄 거르기
            continue
        people.append({"name": name, "email": email, "amount": amount})
    return people

def make_document(person):
    doc = Document()
    doc.add_heading("정산 안내문", level=1)
    doc.add_paragraph(f"{person['name']}님, 이번 달 정산 금액은 {person['amount']}원입니다.")
    path = f"output/안내문_{person['name']}.docx"  # 이름으로 파일명 구분
    doc.save(path)
    return path

def send_mail(person, file_path, server):
    msg = EmailMessage()
    msg["Subject"] = f"{person['name']}님 정산 안내문"
    msg["From"] = SENDER
    msg["To"] = MY_EMAIL if TEST_MODE else person["email"]  # 테스트면 나에게
    msg.set_content(f"{person['name']}님, 첨부된 안내문을 확인해 주세요.")
    with open(file_path, "rb") as f:
        msg.add_attachment(f.read(), maintype="application",
                           subtype="octet-stream",
                           filename=file_path.split("/")[-1])
    server.send_message(msg)

def main():
    people = read_recipients("명단.xlsx")          # 1) 명단 읽기
    results = []
    server = smtplib.SMTP_SSL("smtp.gmail.com", 465)  # 2) 메일 서버 접속
    server.login(SENDER, APP_PASSWORD)
    for person in people:                          # 3) 한 명씩 순회
        try:
            path = make_document(person)           #    문서 만들고
            send_mail(person, path, server)        #    보내고
            results.append((person["name"], "성공"))
            print(f"[성공] {person['name']}")
        except Exception as e:                     #    실패해도 계속
            results.append((person["name"], f"실패: {e}"))
            print(f"[실패] {person['name']} - {e}")
        time.sleep(2)                              #    과속 방지
    server.quit()
    # 4) 마지막 요약
    ok = sum(1 for _, s in results if s == "성공")
    print(f"총 {len(results)}명 중 {ok}명 성공")

main()

# 실행 결과:
# [성공] 홍길동
# [성공] 김영희
# [성공] 박민지
# 총 3명 중 3명 성공

 

한번 쭉 보면, 어려운 새 문법이 하나도 없죠? 전부 지금까지 배운 조각들이에요. 그걸 함수로 나눠 담고 try/exceptTEST_MODE로 안전장치만 둘렀을 뿐이에요.


대량으로 보낼 땐 이것만 조심하세요

시스템은 완성됐지만, 진짜 100명, 200명한테 쏠 때 알아둘 게 몇 가지 있어요.

1인 1메일 원칙. 아까 To에 한 명만 넣으라고 밑줄 그었죠? 이게 왜 중요하냐면요. 만약 여러 명을 한 메일 ToCC에 몰아넣으면, 받는 사람들끼리 서로의 이메일 주소가 다 보여요. 거래처끼리, 수강생끼리 이메일이 노출되는 개인정보 사고예요. 실무에서 은근히 자주 터지는 사고니까 꼭 한 명씩 따로 보내세요.

Gmail 하루 한도. 무료 Gmail은 하루에 약 500통까지만 보낼 수 있어요. 그 이상은 나눠서 며칠에 걸쳐 보내야 해요.

너무 빨리 보내지 마세요. 코드에 time.sleep(2) 넣은 거 기억나시죠? 메일을 쉬지 않고 초당 여러 통씩 쏘면 스팸으로 분류되거나, 심하면 계정이 잠깐 잠겨요. 한 통 보내고 2초쯤 쉬어주는 게 오히려 안전하고 빠른 길이에요.


직접 해보세요

머리로만 보면 안 남아요. 손으로 해봐야 진짜 여러분 거예요.

과제 1. 엑셀에 나 자신을 포함한 3명 명단을 만들고(이메일은 셋 다 내 주소로 해도 돼요), TEST_MODE = True로 전체를 돌려보세요. 세 통이 다 내 받은편지함에 오는지, 첨부 문서 안에 이름이 제대로 바뀌어 들어갔는지 확인해보세요.

힌트: 이메일을 셋 다 내 주소로 해두면 실제로 남한테 안 가면서 결과를 눈으로 볼 수 있어요.

 

과제 2. 명단 중 한 명의 이메일을 일부러 이상하게(예: @ 빼기) 만들어서 돌려보세요. 그 사람만 [실패]로 찍히고, 나머지는 [성공]으로 끝까지 가는지 확인해보세요.

힌트: try/except가 잘 작동하면 프로그램이 안 죽고 마지막 요약까지 나와요.


마무리

여기까지 왔으면, 사실 여러분은 문서랑 메일 파트를 거의 다 마스터한 거예요. 엑셀에서 데이터 꺼내고, 사람마다 문서 찍어내고, 각자에게 자동으로 보내고, 실패까지 챙기는 시스템을 직접 만들었잖아요. 이거 실무에서 바로 쓰는 진짜 자동화예요.

그런데 지금까지 우리가 다룬 데이터는 전부 "내 컴퓨터 안에, 엑셀에 이미 있던 것"이었어요. 이제 방향을 한번 바꿔볼 때가 됐어요.

다음 편(16)에서는 웹에 있는 데이터를 파이썬이 직접 긁어오는 크롤링을 시작해요. 인터넷 페이지에서 원하는 정보만 쏙쏙 뽑아오는 거죠. 지금까지 "가진 데이터를 다루는" 쪽이었다면, 이제 "데이터를 구해오는" 쪽으로 넘어가는 거예요. 재밌어질 거예요.


 


제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀

 

 
728x90