VBA SaveAs 거래처별 저장: 두 합계와 실패 뒤 남은 파일까지 확인하기

거래처별 파일 두 개가 생겨도 합계 수식이 깨졌거나 원본의 관리 시트가 섞였다면 배포할 수 없습니다. 이번 작업은 Customer_A와 Customer_B를 각각 한 시트짜리 XLSX로 만들고 금액·수식·제외 시트를 함께 확인하는 것입니다. 중간에 실패하면 그 실행 폴더를 따로 남깁니다.

Windows 데스크톱 Excel의 로컬 사본을 위한 코드입니다. 본문과 첨부에는 가상 값만 있습니다. Python으로 입력·이름 충돌·합계를 검산했으며 Windows Excel의 Copy나 SaveAs는 실행하지 않았습니다.

Customer_A·Customer_B를 각각 한 시트 XLSX로 분리하고 B4 두 합계를 확인하는 설명 도해
Customer_A·Customer_B를 각각 한 시트 XLSX로 분리하고 B4 두 합계를 확인하는 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

입력 두 장과 제외할 시트

practice-75.xlsx에는 Customer_A, Customer_B, Summary, 검산이 있습니다. 각 거래처 시트의 A1:B4는 다음과 같습니다.

시트 2행 품목·금액 3행 품목·금액 B4 수식 기대값
Customer_A 노트·1200 펜·450 =SUM(B2:B3) 1650
Customer_B 파일·450 클립·80 =SUM(B2:B3) 530

A1은 품목, B1은 금액, A4는 합계입니다. Summary의 “내보내기 제외” 기록과 검산 시트는 Customer_ 접두사가 없어 저장 대상이 아닙니다. 접두사 비교는 코드에 쓴 대소문자를 그대로 따르므로 customer_A를 같은 대상으로 가정하지 않습니다.

합계는 각 시트 안에서 계산합니다. 다른 시트·통합 문서에 연결되는 수식은 이 독립 파일 예제의 범위를 벗어납니다. 자료에는 실제 거래처나 개인정보가 없으며, 실제 업무 파일의 숨긴 행·메모·개체까지 안전하다는 보장은 없습니다.

새 문서를 변수로 잡는 이유

Before와 After를 생략한 Worksheet.Copy는 복사한 시트를 가진 새 통합 문서를 만듭니다. 복사 직후 ActiveWorkbook을 outBook에 잡고 원본과 다른 개체인지 확인합니다. 이후 저장과 닫기는 outBook으로 수행합니다. 실행 중 다른 자동화나 사용자의 창 전환이 개입하지 않는 실습 환경을 사용하세요.

SaveAs의 FileFormat은 저장할 파일 형식을 지정합니다. 여기서는 확장자 .xlsx와 xlOpenXMLWorkbook을 함께 지정합니다. 복사본에 Excel/OLE 링크, 연결, VBA 프로젝트가 있으면 저장을 중단합니다. 이 검사만으로 모든 외부 의존성과 민감정보가 사라진다고 주장하지 않습니다.

대상 검사·별도 실행 폴더·새 통합 문서·SaveAs와 닫기 순서를 설명한 도해
대상 검사·별도 실행 폴더·새 통합 문서·SaveAs와 닫기 순서를 설명한 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

파일명과 저장 경로의 적용 한계

저장 전 대상 전체의 이름을 정리하고 소문자 Collection 키로 충돌을 확인합니다. 예를 들어 유효한 시트 이름 Customer_A|B와 Customer_A"B는 파일 이름 정제 후 둘 다 Customer_A_B.xlsx가 됩니다. 두 파일 중 하나를 덮어쓰는 대신 이 단계에서 중단해야 합니다.

출력 폴더는 로컬 원본 옆 customer_export_날짜_시각_00 형식입니다. 같은 시각에 이미 있으면 01부터 99까지 찾습니다. 이것은 동시에 실행하는 여러 프로세스의 원자적 잠금이 아닙니다. 경합·권한 오류는 중단 사유입니다. Len(target)>200이면 중단하는 것은 이 예제의 보수적 정책입니다. 모든 환경의 허용 길이를 측정해 정한 값은 아닙니다.

전체 저장 모듈

첫 Attribute VB_Name 줄은 BAS 가져오기용 메타데이터입니다. BAS를 가져올 때는 그대로 두고, 코드 창에 직접 붙여넣을 때는 이 첫 줄만 제외합니다.

Attribute VB_Name = "CustomerExport"
Option Explicit

Public Sub ExportCustomerSheets()
    Const PREFIX As String = "Customer_"
    Dim src As Workbook, outBook As Workbook, ws As Worksheet
    Dim targets As Collection, names As Collection
    Dim oldScreen As Boolean, oldEvents As Boolean, oldAlerts As Boolean
    Dim folder As String, target As String, fileName As String
    Dim links As Variant, saved As Long, failure As String

    oldScreen = Application.ScreenUpdating
    oldEvents = Application.EnableEvents
    oldAlerts = Application.DisplayAlerts
    On Error GoTo Failed
    Set src = ThisWorkbook

    If InStr(1, Application.OperatingSystem, "Mac", vbTextCompare) > 0 Then
        Err.Raise vbObjectError + 701, , "This example requires Windows desktop Excel."
    End If
    If Len(src.Path) = 0 Or InStr(src.Path, "://") > 0 Then
        Err.Raise vbObjectError + 702, , "Save a local copy of the source workbook first."
    End If

    Set targets = New Collection
    Set names = New Collection
    For Each ws In src.Worksheets
        If Left$(ws.Name, Len(PREFIX)) = PREFIX Then
            If ws.Visible <> xlSheetVisible Then
                Err.Raise vbObjectError + 703, , "Review hidden target sheet: " & ws.Name
            End If
            fileName = SafeFileName(ws.Name) & ".xlsx"
            names.Add fileName, LCase$(fileName)
            targets.Add ws
        End If
    Next ws
    If targets.Count = 0 Then
        Err.Raise vbObjectError + 704, , "No visible Customer_ sheets were found."
    End If

    folder = NewRunFolder(src.Path)
    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.DisplayAlerts = True

    For Each ws In targets
        target = folder & "\" & SafeFileName(ws.Name) & ".xlsx"
        If Len(target) > 200 Then
            Err.Raise vbObjectError + 705, , "Example path limit exceeded; use a shorter parent folder."
        End If
        If Len(Dir$(target)) > 0 Then
            Err.Raise vbObjectError + 706, , "Refusing to replace: " & target
        End If

        ws.Copy
        Set outBook = ActiveWorkbook
        If outBook Is src Then
            Set outBook = Nothing
            Err.Raise vbObjectError + 707, , "A separate output workbook was not created."
        End If

        links = outBook.LinkSources(Type:=xlExcelLinks)
        If Not IsEmpty(links) Then
            Err.Raise vbObjectError + 708, , "External workbook links in: " & ws.Name
        End If
        links = outBook.LinkSources(Type:=xlOLELinks)
        If Not IsEmpty(links) Then
            Err.Raise vbObjectError + 709, , "OLE links in: " & ws.Name
        End If
        If outBook.Connections.Count > 0 Or outBook.HasVBProject Then
            Err.Raise vbObjectError + 710, , "Review connections or copied VBA project in: " & ws.Name
        End If

        outBook.SaveAs Filename:=target, FileFormat:=xlOpenXMLWorkbook, AddToMru:=False
        outBook.Close SaveChanges:=False
        Set outBook = Nothing
        saved = saved + 1
    Next ws
    GoTo CleanUp

Failed:
    failure = CStr(Err.Number) & ": " & Err.Description
    Resume CleanUp

CleanUp:
    On Error Resume Next
    If Not outBook Is Nothing Then outBook.Close SaveChanges:=False
    Application.EnableEvents = oldEvents
    Application.DisplayAlerts = oldAlerts
    Application.ScreenUpdating = oldScreen
    On Error GoTo 0
    If Len(failure) > 0 Then
        MsgBox "Stopped. Saved: " & saved & vbCrLf & failure & vbCrLf & folder, vbExclamation
    Else
        MsgBox "Saved: " & saved & vbCrLf & folder, vbInformation
    End If
End Sub

Private Function SafeFileName(ByVal value As String) As String
    Dim item As Variant, i As Long
    value = Trim$(value)
    For Each item In Array("\", "/", ":", "*", "?", Chr$(34), "<", ">", "|")
        value = Replace(value, CStr(item), "_")
    Next item
    For i = 0 To 31
        value = Replace(value, Chr$(i), "_")
    Next i
    Do While Len(value) > 0
        If Right$(value, 1) <> "." And Right$(value, 1) <> " " Then Exit Do
        value = Left$(value, Len(value) - 1)
    Loop
    If Len(value) = 0 Then Err.Raise vbObjectError + 711, , "Empty output file name."
    SafeFileName = value
End Function

Private Function NewRunFolder(ByVal parent As String) As String
    Dim stem As String, candidate As String, attempt As Long
    stem = parent & "\customer_export_" & Format$(Now, "yyyymmdd_hhnnss")
    For attempt = 0 To 99
        candidate = stem & "_" & Format$(attempt, "00")
        If Len(Dir$(candidate, vbDirectory)) = 0 Then
            MkDir candidate
            NewRunFolder = candidate
            Exit Function
        End If
    Next attempt
    Err.Raise vbObjectError + 712, , "No unused run folder was available."
End Function

거래처 두 파일을 점검하는 순서

  1. practice-75.xlsx를 짧은 로컬 경로의 Master.xlsm 사본으로 저장합니다.

  2. Customer_A B4=1650, Customer_B B4=530과 각 SUM 수식을 확인합니다.

  3. ExportCustomerSheets.bas를 해당 프로젝트로 가져오고 컴파일을 확인합니다.

  4. 다른 Excel 자동화를 중단한 실습 환경에서 ExportCustomerSheets를 실행합니다.

  5. 새 출력 폴더의 두 파일을 열어 한 시트, B4 값·수식, Summary 제외를 확인합니다.

  6. 실패하면 저장 개수와 실제 폴더 내용을 기록하고 부분 결과를 배포하지 않습니다.

검증 결과와 실제 파일 대조

예상 파일은 Customer_A.xlsx, Customer_B.xlsx 두 개입니다. 각 파일을 열어 시트가 하나이고 B4 값이 각각 1650·530인지, 수식 입력줄이 SUM(B2:B3)인지 확인합니다. 숫자만 복사해 만든 파일도 합계가 같을 수 있으므로 수식 확인을 생략하지 않습니다.

원본을 닫고 출력 파일을 다시 열어 외부 링크 없이 값이 유지되는지도 확인하세요. Python은 XLSX 입력 수식과 계산 캐시를 독립 산술로 대조했습니다. 실제 Excel 출력 파일을 만들거나 열어서 검증한 것은 아닙니다. 검산!D2:D4는 실제 관찰값을 적기 위해 비워 두었고, D5에는 이미 저장된 파일은 실패 시 자동 삭제하지 않는다는 안내가 있습니다.

서로 다른 두 시트 이름이 같은 파일명으로 정제되는 충돌과 저장 대상을 구분한 설명 도해
서로 다른 두 시트 이름이 같은 파일명으로 정제되는 충돌과 저장 대상을 구분한 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

저장 완료 뒤 받는 사람과 전체 내용을 대조합니다

Saved: 2와 두 합계가 맞아도 받는 사람이 맞다는 뜻은 아닙니다. 가령 Customer_A.xlsx와 Customer_B.xlsx의 수신자를 서로 바꿔 연결하면 파일 수·합계·수식 검사는 모두 통과한 채 잘못 전달할 수 있습니다. 이 매크로는 수신자나 보고 대상 기간을 검사하지 않습니다.

  1. 실제 배포에는 거래처 코드 또는 승인된 명칭, 확정된 수신자, 이번 실행 폴더의 전체 파일 경로, 보고 대상 기간, 검토 상태를 한 줄씩 기록한 확인표를 따로 둡니다. 파일명만 보고 이메일 주소를 추정하지 않습니다.
  2. 확인표의 경로로 파일을 다시 열어 해당 거래처의 내용만 있는지 봅니다. A1:B4는 예제의 합계 검산 범위이지 내보내기 범위가 아닙니다. Worksheet.Copy는 시트 자체를 복사하므로 그 밖의 셀·숨긴 행과 열·메모·개체도 검토 대상입니다.
  3. 배포용 사본에서 Microsoft 문서 검사 안내의 파일 → 정보 → 문제 확인 → 문서 검사를 실행해 결과를 확인합니다. 검사 결과만으로 모든 숨은 내용이 안전하다고 승인하지 않습니다. 문서 속성·머리글·바닥글과 직접 확인할 개체도 검토하세요.
  4. 숨긴 내용을 제거하면 수식 결과가 바뀌거나 되돌릴 수 없을 수 있습니다. 원본에 일괄 제거를 적용하지 말고 배포를 보류한 뒤 허용된 내용으로 새 사본을 준비합니다. 수정한 파일을 다시 열어 B4의 값·수식과 거래처 내용을 재검산하고 확인표도 갱신합니다.

이전 실행과 새 실행에는 같은 파일명이 있을 수 있습니다. 실패 폴더에서 남은 파일과 새 폴더의 파일을 섞지 말고, 재실행 뒤에는 새 경로의 결과를 다시 승인하세요. 이 확인표 작성과 수신자 대조는 배포 전 검토 절차이며, 본문 매크로에 이메일 전송 기능을 추가한 것이 아닙니다.

한 파일 뒤에 실패하면 이미 저장한 파일을 보존합니다

이 코드는 전체 작업을 하나의 트랜잭션으로 되돌리지 않습니다. A를 저장한 뒤 B에서 실패하면 A는 실행 폴더에 남을 수 있습니다. 메시지의 Saved 수는 저장·닫기까지 끝난 개수이며, 실패 지점에 따라 미완성 파일이 별도로 남았는지 폴더도 확인해야 합니다.

숨겨진 Customer_ 시트가 있으면 자동으로 공개하지 않고 멈춥니다. 입력 범위와 숨김 이유를 확인하고, 내보낼 수 있는 새 사본으로 정리한 뒤 재실행합니다. 외부 링크나 VBA 때문에 멈춘 경우에는 경고를 무시하거나 연결 오류를 0으로 대체하지 말고 의존성을 검토합니다.

실패한 outBook만 닫기를 시도하고 시작 당시 화면 갱신·이벤트·경고 설정을 복원합니다. 복구 단계는 오류를 무시하는 best-effort 처리이므로 앱 강제 종료와 모든 닫기 실패까지 보장하지 않습니다. 일부 파일이 남은 폴더를 완료 묶음으로 배포하지 말고, 새 실행 폴더의 결과를 다시 검산하세요. 원본 자동 저장과 이메일 전송은 하지 않습니다.

각 출력 파일의 시트·B4 값·SUM 수식·외부 링크를 확인한 뒤 배포 검토 여부를 나눈 설명 도해
각 출력 파일의 시트·B4 값·SUM 수식·외부 링크를 확인한 뒤 배포 검토 여부를 나눈 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

기존 설명용 그림입니다. 이번 VBA 실행을 증명하는 화면은 아닙니다.

A 저장 후 B 오류가 나면 부분 결과와 실행 폴더를 보존하고 재검산하는 설명 도해
A 저장 후 B 오류가 나면 부분 결과와 실행 폴더를 보존하고 재검산하는 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

기존 설명용 그림입니다. 이번 VBA 실행을 증명하는 화면은 아닙니다.

공식 자료

Worksheet.Copy method (Excel)

Workbook.SaveAs method (Excel)

출처 원문 수집일: S1 2026-09-24; S2 2026-09-24.

작성자 소개