VBA Application.OnTime 예약·취소: 10초 테스트로 중복 실행 막기

VBA Application.OnTime 예약·취소는 Excel 세션에서 인수가 없는 프로시저를 미래 시각에 한 번 실행하거나, 저장해 둔 같은 시각과 프로시저 이름으로 예약을 해제하는 기능입니다. 이 글은 매일 메일이 자동 발송된다고 약속하지 않습니다. 예약등록_10초 뒤 실행 로그 1행이 생기는지, 예약등록_30초를 즉시 취소한 뒤 로그가 늘지 않는지를 검산합니다.

Excel 세션의 10초 뒤 한 번 예약과 5초 허용 창 및 기대 로그를 설명한 도해
Excel 세션의 10초 뒤 한 번 예약과 5초 허용 창 및 기대 로그를 설명한 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

입력과 출력

첫 시험의 입력은 지연 시간 10초와 작업명 10초 테스트입니다. 출력은 실행로그 시트 A2:C2 한 행입니다. A2에는 실제 실행 시각, B2에는 10초 테스트, C2에는 실행 번호 1이 들어갑니다. 시각은 실행 환경에 따라 달라질 수 있으므로 검산의 고정값은 작업명과 실행 번호입니다.

주제 개요와 전체 흐름

두 번째 시험은 30초 예약 후 즉시 취소합니다. 취소 시점의 실행로그 마지막 행과 35초 뒤 마지막 행이 같아야 합니다. 로그가 늘지 않는 것이 합격값입니다.

OnTime이 보장하는 범위

기존 예약 취소 성공을 확인한 뒤 같은 시각·프로시저 기록으로 한 건만 등록하는 설명 도해
기존 예약 취소 성공을 확인한 뒤 같은 시각·프로시저 기록으로 한 건만 등록하는 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

Microsoft의 Excel Application.OnTime 문서는 EarliestTime, Procedure, LatestTime, Schedule 인수로 미래 실행을 예약한다고 설명합니다. Schedule=False로 취소하려면 이전 예약과 같은 Procedure와 EarliestTime을 사용해야 합니다. 시각은 가장 가까운 초로 반올림됩니다.

단계별 실행 절차

LatestTime은 Excel이 예약 시각에 준비 상태가 아닐 때 기다릴 마지막 시각입니다. 이 예제는 예약 시각 뒤 5초까지 준비되지 않으면 실행하지 않도록 설정합니다. LatestTime을 생략하면 Excel은 실행할 수 있을 때까지 기다릴 수 있으므로, “정확히 10초”가 아니라 허용된 시간 창 안의 1회 실행으로 이해해야 합니다.

예약 프로시저는 인수를 받을 수 없고 사용자 정의 클래스나 UserForm에 선언할 수 없습니다. 따라서 OnTime이 직접 호출하는 예약실행은 표준 모듈의 Public Sub이며 인수가 없습니다.

본문에 포함된 VBA 블록은 이해를 돕기 위한 발췌본입니다. 실습을 완료하려면 ZIP 파일에 포함된 표준 모듈과 ThisWorkbook 이벤트 소스 전체를 각각의 코드 창에 복사하여 넣어야 합니다. 본문의 코드 조각을 단순히 이어 붙여서 완성된 모듈로 사용하지 않도록 주의해 주세요.

1. 예약 시각과 프로시저 문자열을 저장합니다

Option Explicit

' 전역 변수 선언
Public NextRun As Date
Public ScheduledProcedure As String
Public IsScheduled As Boolean

' --- 공개 매크로 ---

점검 체크리스트

취소에 같은 값을 다시 써야 하므로 계산한 시각과 프로시저 문자열을 전역 변수에 보관합니다. 프로시저 문자열에는 현재 통합 문서 이름을 포함합니다.

NextRun = Now + TimeSerial(0, 0, delaySeconds)
ScheduledProcedure = "'" & ThisWorkbook.Name & "'!예약실행"

예약 뒤 파일을 다른 이름으로 저장하면 저장한 프로시저 문자열과 현재 이름이 달라질 수 있습니다. 이 예제에서는 예약이 남아 있는 동안 파일 이름을 바꾸지 않습니다.

2. 기존 예약을 취소한 뒤 한 건만 등록합니다

Public Sub 예약등록_10초()
    예약등록 10, "10초 테스트"
End Sub

Public Sub 예약등록_30초()
    예약등록 30, "30초 테스트"
End Sub

Private Sub 예약등록(ByVal delaySeconds As Long, ByVal taskName As String)
    If IsScheduled Then 예약취소_Internal
    If IsScheduled Then Exit Sub

    NextRun = Now + TimeSerial(0, 0, delaySeconds)
    ScheduledProcedure = "'" & ThisWorkbook.Name & "'!예약실행"

    Application.OnTime EarliestTime:=NextRun, _
        Procedure:=ScheduledProcedure, _
        LatestTime:=NextRun + TimeSerial(0, 0, 5), _
        Schedule:=True
    
    IsScheduled = True
    ThisWorkbook.Worksheets("상태").Range("B2").Value = "예약됨: " & taskName
End Sub

Private Sub 상태초기화(ByVal status As String)
    ThisWorkbook.Worksheets("상태").Range("B2").Value = status
End Sub

첫 번째 If는 이전 예약이 있으면 취소를 시도합니다. 취소가 실패하면 IsScheduled가 True로 남으므로 두 번째 If에서 새 예약을 추가하지 않습니다. 기존 예약과 새 예약이 동시에 남는 상황을 막습니다.

예약등록_10초와 예약등록_30초는 이 내부 절차에 각각 10과 30을 넘기는 공개 매크로입니다. OnTime이 호출하는 대상은 인수가 있는 예약등록이 아니라 인수 없는 예약실행입니다.

3. 실행 로그 한 행을 기록합니다

10초 예약의 A2:C2 한 행과 30초 예약 취소 후 35초의 새 로그 0행을 비교한 설명 도해
10초 예약의 A2:C2 한 행과 30초 예약 취소 후 35초의 새 로그 0행을 비교한 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.
Public Sub 예약실행()
    Dim ws As Worksheet
    Dim nextRow As Long
    Dim taskName As String

    On Error GoTo HandleError
    Set ws = ThisWorkbook.Worksheets("실행로그")
    
    taskName = Replace( ThisWorkbook.Worksheets("상태").Range("B2").Value, "예약됨: ", "")
    
    nextRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 1

    ws.Cells(nextRow, "A").Value = Now
    ws.Cells(nextRow, "B").Value = taskName
    ws.Cells(nextRow, "C").Value = nextRow - 1

    IsScheduled = False
    NextRun = 0
    ScheduledProcedure = ""
    상태초기화 "실행 완료"
    Exit Sub

HandleError:
    ' 예약 실행 시 발생한 오류라도 예약 상태는 해제되어야 함
    IsScheduled = False
    NextRun = 0
    ScheduledProcedure = ""
    MsgBox "실행 중 오류 발생: " & Err.Description, vbCritical
End Sub

실행이 끝나면 예약 상태를 비웁니다. 이 코드는 실행 뒤 다음 날 예약을 자동 등록하지 않습니다. 먼저 한 번 실행과 취소가 정확히 동작하는지 확인한 뒤에만 반복 정책을 별도로 설계해야 하기 때문입니다.

4. 같은 시각과 프로시저로 취소합니다

Public Sub 예약취소()
    예약취소_Internal
End Sub

Private Sub 예약취소_Internal()
    If Not IsScheduled Then
        상태초기화 "대기 없음"
        Exit Sub
    End If

    On Error GoTo CancelError
    Application.OnTime EarliestTime:=NextRun, _
        Procedure:=ScheduledProcedure, _
        Schedule:=False
    
    IsScheduled = False
    NextRun = 0
    ScheduledProcedure = ""
    상태초기화 "취소됨"
    Exit Sub

CancelError:
    ThisWorkbook.Worksheets("상태").Range("B2").Value = "취소 실패"
    MsgBox "예약을 취소하지 못했습니다. 통합 문서를 닫지 마세요: " & vbCrLf & Err.Description, vbCritical
End Sub

취소 오류를 On Error Resume Next로 숨기지 않습니다. 실패하면 예약 상태를 True로 남기고 새 예약 등록과 통합 문서 닫기를 막을 수 있게 합니다.

완성 예제 1: 10초 뒤 한 번 실행

정상 실행·취소 성공·허용 창 만료의 예약 상태 차이를 나눈 설명 도해
정상 실행·취소 성공·허용 창 만료의 예약 상태 차이를 나눈 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.
  1. 파일 저장: ontime-practice.xlsx 파일을 열고 [파일] > [다른 이름으로 저장]을 선택하여 파일 형식을 Excel 매크로 사용 통합 문서 (.xlsm)로 변경하여 저장합니다.

  2. 모듈 추가: ontime_practice.bas 파일의 내용을 복사하여 VBA 편집기(Alt+F11)의 표준 모듈에 붙여넣습니다.

  3. 이벤트 코드 삽입: ThisWorkbook-events.txt의 내용을 복사하여 VBA 편집기의 ThisWorkbook 개체에 붙여넣습니다.

  4. 실행로그에 머리글만 있는지 확인합니다.

  5. 예약등록_10초를 실행합니다.

  6. 상태 시트가 예약됨: 10초 테스트인지 확인합니다.

  7. 15초 안에 실행로그 A2:C2를 확인합니다.

합격값은 B2가 10초 테스트, C2가 1이고 실행 로그가 정확히 한 행 늘어난 상태입니다. A2 시각이 등록 시각과 정확히 10.000초 차이여야 한다고 판정하지 않습니다. Excel 준비 상태와 초 반올림 때문에 허용 창 안에서 실행될 수 있습니다.

완성 예제 2: 30초 예약을 취소

예약등록_30초를 실행한 직후 예약취소를 실행합니다. 상태 시트 B2가 취소됨인지 확인하고, 실행로그 마지막 행 번호를 적습니다. 35초 뒤 마지막 행 번호가 그대로면 통과입니다.

중복 검사도 합니다. 예약등록_30초를 연속 두 번 실행하면 두 번째 호출은 첫 예약 취소를 확인한 뒤 새 예약 하나를 등록합니다. 이후 한 번만 취소하면 상태가 취소됨이 되고 35초 뒤 로그가 늘지 않아야 합니다.

실패와 복구 및 닫기 중단

예약 취소 실패 시 새 예약을 만들지 않습니다. 같은 Excel 세션에서 보존된 NextRun과 ScheduledProcedure를 그대로 사용하여 예약취소를 다시 시도합니다. 오류 없이 완료되어 상태!B2가 취소됨으로 바뀌고 IsScheduled=False임을 확인한 경우에만 다음 실습을 진행합니다. 허용 창을 넘긴 뒤에도 취소 오류가 계속되면 이 코드로 만료와 다른 취소 오류를 구분할 수 없으므로 아래 중단 안내를 따릅니다.

등록 직후 매크로를 찾을 수 없다는 오류가 나오면 예약실행이 표준 모듈의 Public Sub인지, 인수가 없는지 확인합니다. 실행로그 시트 오류가 나면 시트 탭 이름을 정확히 맞춥니다. 예약 후 파일 이름을 바꿨다면 취소에 필요한 프로시저 문자열이 달라질 수 있으므로, 예약 상태에서 이름 변경을 하지 않는 규칙을 적용합니다.

통합 문서를 닫기 전에 Workbook.BeforeClose 이벤트에서 예약 취소를 호출합니다. 변경된 통합 문서에서는 저장 여부를 묻기 전에 발생하며 Cancel=True로 닫기를 중단할 수 있습니다. 닫기 중단은 변경 내용의 저장을 대신하지 않습니다.

Option Explicit

Private Sub Workbook_BeforeClose(Cancel As Boolean)
    On Error GoTo StopClose
    If IsScheduled Then 예약취소
    If IsScheduled Then
        Cancel = True
        MsgBox "예약 취소를 확인하지 못해 닫기를 중단했습니다."
    End If
    Exit Sub
StopClose:
    Cancel = True
    MsgBox "예약 상태 확인 중 오류가 발생해 닫기를 중단했습니다."
End Sub

취소 성공을 확인하지 못했는데도 상태 변수를 False로 덮어쓰지 않습니다. 닫기를 중단하고 사용자가 예약 시각과 프로시저 값을 확인할 수 있게 남깁니다.

운영 한계

Workbook_BeforeClose에서 남은 예약의 취소 성공과 닫기 중단을 나눈 설명 도해
Workbook_BeforeClose에서 남은 예약의 취소 성공과 닫기 중단을 나눈 설명 도해. 가상 입력과 기대값을 설명한 그림이며 실제 Excel 실행 화면이나 실행 성공 증거가 아닙니다.

OnTime은 이 Excel 응용 프로그램 세션에서 프로시저 실행을 예약하는 기능입니다. PC 전원 켜기, 사용자 로그인, Excel 시작, 네트워크 복구, 메일 전송 성공을 보장하는 운영 스케줄러가 아닙니다. Excel이 준비 상태가 아니면 LatestTime 범위 안에서 지연되거나 실행되지 않을 수 있습니다.

이 예제는 한 건 예약 상태만 전역 변수로 관리합니다. 여러 작업을 동시에 예약하려면 작업별 시각과 프로시저 문자열을 별도로 저장하고 각각 취소할 수 있어야 합니다. 응용 프로그램이 비정상 종료되면 메모리 변수와 상태 시트가 일치하지 않을 수 있으므로, 이를 완전 무인 운영의 근거로 사용하지 않습니다.

예약 시각부터 LatestTime까지 Excel이 실행 가능한 준비 상태로 돌아오지 않으면 OnTime 콜백이 실행되지 않아 IsScheduled=True가 남을 수 있습니다. 이 값은 코드가 아직 정리되지 않았다는 표시이며, Excel 내부에 실행 가능한 예약이 남았다는 증거는 아닙니다. 이 예제에는 만료 뒤 안전한 복구·닫기 절차가 실제 Excel에서 검증되어 있지 않습니다. 허용 창을 넘긴 뒤 취소 실패 또는 닫기 중단이 반복되면 새 예약과 추가 실습을 중단하고 현재 파일 이름을 유지하여 변경 내용과 실행로그를 정상 저장하세요. 예약 시각·프로시저 문자열·오류 내용을 보존하여 확인해야 합니다. 상태 변수를 임의로 초기화하거나 예약실행을 수동 호출하여 해결하지 않습니다. 따라서 완전 무인 운영이나 항상 안전한 종료를 보장하지 않습니다.

관련 글과 확장 실습

예약이 실행할 본체 매크로의 오류 처리는 VBA 오류 처리 가이드와 연결해 단계별 로그를 추가할 수 있습니다. 실행 결과를 PDF로 만드는 기능은 VBA PDF 내보내기의 성공 조건을 먼저 독립적으로 검증한 뒤 OnTime에서 호출합니다. Excel을 일부러 바쁘게 만들어 LatestTime을 넘기는 실습은 만료 뒤 안전한 복구·닫기 경로를 실제 Excel에서 검증하기 전에는 진행하지 않습니다.

참고한 공식 자료

제공된 ZIP 파일 내의 XLSX, BAS, TXT, README 파일 구성과 상태!B2 셀 및 실행 로그 머리글 배치를 확인했습니다. 실제 Excel 환경에서의 VBA 컴파일이나 예약 실행 결과는 아직 검증되지 않았습니다. 따라서 본문에 제시된 테스트 결과값은 독자가 확인해야 할 기대값이며, 실제 측정된 성공 결과가 아님을 유의하시기 바랍니다.

작성자 소개