왜 엑셀 자동화에서 시작하는가
대부분의 비즈니스 자동화는 거창한 AI 시스템이 아니라 엑셀 파일 하나에서 시작된다. 매출 자료를 모으고, 거래처별 파일을 합치고, 월별 보고서를 만들고, 잘못 입력된 날짜를 고치는 일이 매일 반복된다. 이런 업무는 규칙이 비교적 명확하다. 그래서 처음부터 AI를 붙이기보다 파이썬으로 안정적인 규칙 기반 자동화를 만드는 편이 더 적합하다.
엑셀 자동화의 기본 흐름은 단순하다.
입력 폴더에서 파일을 찾는다.
엑셀 또는 CSV 데이터를 읽는다.
필요한 열만 고르고 이름을 통일한다.
누락값, 날짜, 숫자 형식을 정리한다.
여러 파일을 하나로 합친다.
결과 파일과 로그를 저장한다.
이 흐름은 앞으로 다룰 데이터베이스, API, AI 자동화에서도 계속 반복된다. 입력이 엑셀 파일일 뿐, 자동화의 기본 구조는 같다.
이번 장에서는 앞에서 만든 프로젝트 구조를 그대로 사용한다.
project/
data/
sales/
inventory/
cost/
output/
logs/
src/
config.json원본 파일은 data에 두고, 생성된 결과는 output에 저장한다. 원본과 결과를 섞지 않는 것이 첫 번째 운영 원칙이다.
AI 생성 이미지
엑셀과 CSV 파일 읽기
파이썬에서 표 형태 데이터를 다룰 때 가장 많이 쓰는 도구는 pandas다. 엑셀 서식을 유지하거나 특정 셀에 값을 쓰는 작업에는 openpyxl을 함께 사용한다.
설치는 다음처럼 한다.
pip install pandas openpyxlCSV 파일은 read_csv, 엑셀 파일은 read_excel로 읽는다.
from pathlib import Path
import pandas as pd
DATA_DIR = Path('data/sales')
csv_path = DATA_DIR / 'sales_2026_04.csv'
excel_path = DATA_DIR / 'sales_2026_05.xlsx'
csv_df = pd.read_csv(csv_path, encoding='utf-8-sig')
excel_df = pd.read_excel(excel_path, sheet_name='매출')
print(csv_df.head())
print(excel_df.head())encoding='utf-8-sig'는 한글 CSV 파일을 엑셀에서 저장했을 때 자주 생기는 문자 깨짐을 줄이는 설정이다. 회사마다 파일 생성 방식이 다르기 때문에, CSV를 읽을 때는 인코딩 문제를 먼저 의심해야 한다.
실무에서는 파일마다 열 이름이 조금씩 다르다. 어떤 파일은 거래처명, 어떤 파일은 고객사, 어떤 파일은 client라고 적혀 있을 수 있다. 자동화를 안정적으로 만들려면 열 이름을 먼저 표준화해야 한다.
COLUMN_MAP = {
'거래처명': 'customer',
'고객사': 'customer',
'client': 'customer',
'매출일': 'date',
'일자': 'date',
'금액': 'amount',
'매출액': 'amount'
}
def normalize_columns(df):
df = df.copy()
df.columns = df.columns.astype(str).str.strip()
df = df.rename(columns=COLUMN_MAP)
return df열 이름 앞뒤에 공백이 들어가는 경우도 많기 때문에 str.strip()으로 먼저 정리해 두면 좋다. 이런 매핑은 사소해 보이지만 중요하다. 자동화가 실패하는 이유는 복잡한 알고리즘이 없어서가 아니라, 입력 파일의 이름과 형식이 매번 조금씩 달라지기 때문이다.
여러 엑셀 파일 병합하기
월별 매출 파일이 여러 개 있다고 가정해 보자. 담당자별로 파일을 나누어 저장하거나, 지점별로 같은 양식의 엑셀을 제출하는 경우가 많다. 이때 사람이 직접 복사해 붙여넣으면 누락과 중복이 쉽게 생긴다.
다음 코드는 data/sales 폴더 안의 모든 엑셀 파일을 읽어 하나로 합친다.
from pathlib import Path
import pandas as pd
DATA_DIR = Path('data/sales')
OUTPUT_DIR = Path('output')
OUTPUT_DIR.mkdir(exist_ok=True)
REQUIRED_COLUMNS = ['date', 'customer', 'amount']
COLUMN_MAP = {
'거래처명': 'customer',
'고객사': 'customer',
'client': 'customer',
'매출일': 'date',
'일자': 'date',
'금액': 'amount',
'매출액': 'amount'
}
def read_sales_file(path):
df = pd.read_excel(path)
df.columns = df.columns.astype(str).str.strip()
df = df.rename(columns=COLUMN_MAP)
missing = [col for col in REQUIRED_COLUMNS if col not in df.columns]
if missing:
raise ValueError(f'{path.name} 파일에 필수 열이 없습니다: {missing}')
df = df[REQUIRED_COLUMNS].copy()
df['source_file'] = path.name
return df
def main():
files = sorted(
path for path in DATA_DIR.glob('*.xlsx')
if not path.name.startswith('~$')
)
if not files:
raise FileNotFoundError('처리할 엑셀 파일이 없습니다.')
frames = []
for path in files:
sales = read_sales_file(path)
frames.append(sales)
result = pd.concat(frames, ignore_index=True)
result.to_excel(OUTPUT_DIR / 'sales_merged.xlsx', index=False)
if __name__ == '__main__':
main()여기서 중요한 부분은 source_file 열이다. 병합 후 특정 행에 문제가 생겼을 때 어느 원본 파일에서 왔는지 추적할 수 있다. 자동화 결과만 저장하면 문제를 찾기 어렵다. 운영 가능한 자동화는 항상 원본 추적 정보를 남긴다.
또 하나 눈여겨볼 부분은 ~$로 시작하는 파일을 제외한 것이다. 엑셀 파일을 열어 둔 상태에서는 임시 파일이 생길 수 있는데, 이런 파일까지 읽으려 하면 오류가 날 수 있다. 실제 폴더를 대상으로 자동화를 만들 때는 이런 작은 예외가 의외로 중요하다.
실무 데이터 오류 처리하기
엑셀 자동화에서 가장 많은 시간이 드는 부분은 계산이 아니라 데이터 정리다. 날짜가 2026-05-01, 2026.05.01, 5/1/2026처럼 섞여 있거나, 금액에 쉼표와 원 표시가 들어가기도 한다. 빈칸, 잘못된 거래처명, 음수 금액도 자주 나온다.
자동화 스크립트는 이런 오류를 조용히 무시하면 안 된다. 처리 가능한 오류는 정리하고, 판단이 필요한 오류는 별도 파일로 분리해야 한다.
def clean_sales_data(df):
df = df.copy()
df['date'] = pd.to_datetime(df['date'], errors='coerce')
df['amount'] = (
df['amount']
.astype(str)
.str.replace(',', '', regex=False)
.str.replace('원', '', regex=False)
.str.strip()
)
df['amount'] = pd.to_numeric(df['amount'], errors='coerce')
df['customer'] = df['customer'].fillna('').astype(str).str.strip()
error_mask = (
df['date'].isna() |
df['customer'].eq('') |
df['amount'].isna()
)
valid_df = df[~error_mask].copy()
error_df = df[error_mask].copy()
return valid_df, error_dferrors='coerce'는 변환할 수 없는 값을 강제로 빈값으로 만든다. 예를 들어 날짜로 바꿀 수 없는 값은 NaT, 숫자로 바꿀 수 없는 값은 NaN이 된다. 이렇게 하면 오류가 있는 행을 찾아낼 수 있다.
다음처럼 정상 데이터와 오류 데이터를 따로 저장한다.
valid_df, error_df = clean_sales_data(result)
valid_df.to_excel(OUTPUT_DIR / 'sales_clean.xlsx', index=False)
error_df.to_excel(OUTPUT_DIR / 'sales_error_rows.xlsx', index=False)오류 행을 삭제해 버리면 안 된다. 매출 데이터라면 한 행이 실제 돈과 연결될 수 있다. 자동화가 할 일은 오류를 숨기는 것이 아니라, 사람이 확인해야 할 목록을 정확히 만들어 주는 것이다.
다만 모든 이상값을 무조건 오류로 볼 필요는 없다. 예를 들어 음수 금액은 환불이나 취소 매출일 수도 있다. 이런 값은 회사의 업무 규칙에 따라 정상 데이터로 둘지, 확인 대상 파일로 분리할지 정해야 한다. 자동화 코드는 업무 규칙이 정해진 뒤에 그 규칙을 충실히 반영해야 한다.
파일명 변경과 폴더 분류 자동화
엑셀 데이터만큼 자주 반복되는 일이 파일 정리다. 예를 들어 거래처별 세금계산서, 견적서, 발주서가 한 폴더에 섞여 있을 수 있다. 파일명이 제각각이면 검색도 어렵고, 누락 확인도 어렵다.
파일 정리 자동화는 보통 다음 작업을 포함한다.
파일명에서 날짜, 거래처, 문서 종류 추출
표준 파일명으로 변경
문서 종류별 폴더로 이동
중복 파일 탐지
처리 결과 기록
예를 들어 파일명에 견적서, 발주서, 세금계산서라는 단어가 들어 있다면 폴더를 나눌 수 있다.
from pathlib import Path
import shutil
BASE_DIR = Path('data/documents')
RULES = {
'견적서': 'quotes',
'발주서': 'orders',
'세금계산서': 'tax_invoices'
}
def classify_file(path):
for keyword, folder in RULES.items():
if keyword in path.name:
return folder
return 'unknown'
def organize_files(dry_run=True):
for path in BASE_DIR.glob('*'):
if path.is_dir():
continue
folder_name = classify_file(path)
target_dir = BASE_DIR / folder_name
target_path = target_dir / path.name
if target_path.exists():
print(f'[중복주의] 이미 같은 이름의 파일이 있습니다: {target_path}')
continue
if dry_run:
print(f'[확인] {path.name} -> {target_path}')
else:
target_dir.mkdir(exist_ok=True)
shutil.move(str(path), str(target_path))
if __name__ == '__main__':
organize_files(dry_run=True)이 코드는 실제 파일을 바로 이동하지 않는다. dry_run=True 상태에서는 어떤 파일이 어디로 갈지 출력만 한다. 출력 결과를 확인한 뒤 문제가 없을 때만 organize_files(dry_run=False)로 실행한다.
파일을 삭제하거나 이동하는 자동화는 반드시 확인 단계가 있어야 한다. 자동화는 빠르기 때문에 잘못 실행하면 잘못된 작업도 빠르게 수행한다. 특히 같은 이름의 파일이 이미 있을 때는 덮어쓰지 말고 멈추거나 별도의 이름을 붙이는 정책을 먼저 정해야 한다.
중복 파일 탐지하기
파일명이 달라도 내용이 같은 파일이 있을 수 있다. 단순히 이름만 비교하면 중복을 찾기 어렵다. 이럴 때는 파일 내용을 기준으로 해시값을 만든다. 해시는 파일 내용을 짧은 식별값처럼 바꾼 것이다. 같은 파일은 같은 해시값을 가진다.
from pathlib import Path
import hashlib
from collections import defaultdict
TARGET_DIR = Path('data/documents')
def file_hash(path):
hasher = hashlib.sha256()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
hasher.update(chunk)
return hasher.hexdigest()
def find_duplicates():
hashes = defaultdict(list)
for path in TARGET_DIR.rglob('*'):
if path.is_file():
hashes[file_hash(path)].append(path)
duplicates = {h: paths for h, paths in hashes.items() if len(paths) > 1}
return duplicates
for h, paths in find_duplicates().items():
print('중복 그룹')
for path in paths:
print('-', path)중복 파일 탐지는 삭제까지 자동화하지 않는 편이 안전하다. 특히 계약서, 증빙, 재무 자료는 같은 파일처럼 보여도 보관 위치 자체가 의미를 가질 수 있다. 자동화는 중복 후보 목록을 만들고, 삭제 여부는 사람이 승인하도록 설계한다.
월별 보고서 자동 생성하기
이제 엑셀 파일을 읽고 정리한 데이터를 보고서로 만들어 보자. 예를 들어 월별 매출 보고서에는 다음 항목이 필요할 수 있다.
총매출
거래처 수
거래처별 매출 합계
일자별 매출 추이
오류 데이터 건수
먼저 정리된 매출 데이터에서 요약표를 만든다.
def make_sales_summary(df):
total_amount = df['amount'].sum()
customer_count = df['customer'].nunique()
by_customer = (
df.groupby('customer', as_index=False)['amount']
.sum()
.sort_values('amount', ascending=False)
)
by_date = (
df.groupby('date', as_index=False)['amount']
.sum()
.sort_values('date')
)
return total_amount, customer_count, by_customer, by_date단순한 보고서는 pandas만으로도 충분하다.
total_amount, customer_count, by_customer, by_date = make_sales_summary(valid_df)
summary_df = pd.DataFrame({
'항목': ['총매출', '거래처 수', '오류 데이터 건수'],
'값': [total_amount, customer_count, len(error_df)]
})
with pd.ExcelWriter(OUTPUT_DIR / 'monthly_sales_report.xlsx', engine='openpyxl') as writer:
summary_df.to_excel(writer, sheet_name='요약', index=False)
valid_df.to_excel(writer, sheet_name='정리데이터', index=False)
by_customer.to_excel(writer, sheet_name='거래처별매출', index=False)
by_date.to_excel(writer, sheet_name='일자별매출', index=False)하지만 회사에서 이미 사용하는 보고서 양식이 있다면 openpyxl로 템플릿 파일을 열어 특정 셀에 값을 채우는 방식이 좋다.
from openpyxl import load_workbook
TEMPLATE_PATH = Path('data/templates/monthly_report_template.xlsx')
REPORT_PATH = OUTPUT_DIR / 'monthly_report_2026_05.xlsx'
wb = load_workbook(TEMPLATE_PATH)
ws = wb['요약']
ws['B2'] = '2026-05'
ws['B4'] = total_amount
ws['B5'] = customer_count
ws['B6'] = len(error_df)
wb.save(REPORT_PATH)템플릿 방식의 장점은 현업이 익숙한 양식을 그대로 유지할 수 있다는 점이다. 자동화가 현업의 일하는 방식을 무리하게 바꾸지 않아도 된다. 대신 템플릿의 시트 이름이나 셀 위치가 바뀌면 코드도 함께 수정해야 하므로, 템플릿 파일은 버전 관리하거나 변경 이력을 남기는 것이 좋다.
안정적으로 운영하기 위한 점검 목록
엑셀과 파일 자동화는 작게 시작하기 쉽지만, 운영 단계에서는 주의할 점이 많다. 다음 항목은 스크립트를 실행하기 전마다 확인해야 한다.
원본 파일을 덮어쓰지 않는가?
처리 결과가
output폴더에 따로 저장되는가?오류 행이 별도 파일로 남는가?
어떤 원본 파일에서 온 데이터인지 추적할 수 있는가?
파일 이동, 삭제 작업에 확인 단계가 있는가?
실행 로그가 남는가?
담당자가 바뀌어도 실행 방법을 이해할 수 있는가?
간단한 로그만 추가해도 운영 품질이 크게 좋아진다.
import logging
from pathlib import Path
LOG_DIR = Path('logs')
LOG_DIR.mkdir(exist_ok=True)
logging.basicConfig(
filename=LOG_DIR / 'excel_automation.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('매출 파일 병합 시작')
logging.info(f'처리 파일 수: {len(files)}')
logging.info(f'정상 데이터 수: {len(valid_df)}')
logging.info(f'오류 데이터 수: {len(error_df)}')로그는 문제가 생긴 뒤에 가치를 발휘한다. 언제 실행했는지, 몇 개 파일을 처리했는지, 오류가 몇 건이었는지를 알 수 있으면 원인 파악 시간이 줄어든다. 가능하다면 결과 파일명에도 실행 날짜나 대상 월을 넣어 두면 나중에 다시 찾기 쉽다.
AI보다 먼저 규칙을 정리하라
엑셀과 파일 업무는 AI를 쓰지 않아도 자동화할 수 있는 일이 많다. 열 이름 통일, 파일 병합, 폴더 분류, 보고서 생성은 규칙 기반 자동화가 더 빠르고 저렴하며 예측 가능하다.
반대로 파일명이 너무 제각각이라 문서 종류를 규칙으로 판단하기 어렵거나, 비정형 메모에서 의미를 읽어야 한다면 AI가 도움이 될 수 있다. 예를 들어 김부장 요청건 최종 수정본.xlsx 같은 파일명을 보고 문서 종류와 중요도를 추정하는 작업은 규칙만으로는 한계가 있다. 하지만 그 경우에도 AI가 바로 파일을 이동하거나 삭제하게 만들면 위험하다. AI는 후보를 분류하고, 사람은 승인하는 구조가 안전하다.
이번 장의 핵심은 명확하다. 기업 자동화의 출발점은 엑셀과 파일이다. 먼저 데이터를 읽고, 정리하고, 오류를 분리하고, 결과를 저장하는 흐름을 안정적으로 만들어야 한다. 이 기반이 있어야 이후 데이터베이스, API, AI 자동화도 무리 없이 연결된다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.