VBA For 루프로 1,000행 매출 등급 분류하기

1000행을 분류할 때는 마지막 행을 놓치지 않는 것과 잘못된 금액을 정상 등급으로 만들지 않는 것을 함께 확인해야 합니다. 이 글에서는 10개 금액을 반복해 예상 등급 개수를 손으로도 계산할 수 있게 만든 뒤, 오류 입력을 별도 시트에 모읍니다. 속도 경쟁이나 실제 매출 분석이 아니라 행 범위와 입력 계약을 점검하는 실습입니다.

10행에서 1000행의 기대값을 먼저 계산하기

practice-61.xlsx의 Sample에는 95000,82000,75000,68000,92000,88000,71000,65000,99000,45000이 있습니다. 가상 등급 기준은 90000 이상 A, 80000 이상 B, 70000 이상 C, 나머지 비음수 정수 D입니다. 10행에서는 A 3개, B 2개, C 2개, D 3개입니다.

Scale1000은 이 순서를 정확히 100번 반복합니다. ID R0001부터 R1000까지 1000개이며 A 300, B 200, C 200, D 300이 독립 기대값입니다. 마지막 셀은 B1001=45000이고 마지막 등급은 D입니다. 제목의 1000행은 이 입력 크기이며 처리 시간을 측정했다는 뜻이 아닙니다.

시트 읽는 입력 매크로 출력 바꾸지 않을 기대값
Sample A2:B11 C:D E:F
Scale1000 A2:B1001 C:D E:F
Boundary A2:B15 C:D E:F

G열 수식은 Check 상태가 DONE일 때만 실제 출력과 기대값을 비교합니다. 배포 파일은 C:D가 비어 있고 Check는 NOT RUN입니다. 기대값을 출력 칸에 미리 넣지 않았습니다.

마지막 행과 배열 행 번호를 나눠 읽기

For…Next에서 Step을 생략하면 증가량은 1입니다. 아래 모듈은 시트의 2행부터 마지막 입력 행까지 A:B를 배열로 읽은 후 For i = 1 To rowCount로 배열을 순회합니다. 시트의 2행은 배열의 1행이고, 시트 1001행은 배열 1000행입니다. 1000을 시트의 끝 행 번호로 쓰면 마지막 자료 한 건을 빠뜨립니다.

Range.End는 지정한 방향으로 영역 끝의 셀을 나타내는 Range를 반환합니다. 여기서는 A열과 B열의 마지막 값을 각각 조사해 더 큰 행을 사용합니다. ID가 빈 마지막 행에 금액만 남아 있어도 검사에 포함시키기 위해서입니다. E:F의 기대값이나 G열 수식을 마지막 입력으로 세지 않습니다. 데이터는 2~1001행에만 두고 그 아래 입력은 오류로 처리합니다.

전체 모듈과 실행 순서

XLSX를 새 XLSM 사본으로 저장하고 GradeRows.bas를 VBA 프로젝트에 가져옵니다. BAS 첫 줄은 가져오기용 메타데이터입니다. 회사의 매크로 정책이 허용하는 연습 환경에서 컴파일을 먼저 확인합니다.

  1. Sample을 선택해 입력 10개와 E:F를 확인하고 GradeSample을 실행합니다. Check의 Sample 행에 처리 10, 유효 10, 거부 0이 나타나고 G열이 모두 일치하는지 비교합니다.

  2. Scale1000을 선택하고 GradeThousand를 실행합니다. Check에서 처리 1000·유효 1000을 확인한 뒤 등급 개수 300/200/200/300과 마지막 R1000의 D를 대조합니다. 첫 화면 몇 행이 맞는 것으로 전체 성공을 판단하지 않습니다.

  3. Boundary에서 GradeBoundary를 실행합니다. 처리 14·유효 7·거부 7과 각 행의 사유를 확인합니다. DONE은 반복 처리가 끝났다는 상태이며 모든 입력이 유효하다는 뜻은 아닙니다.

Attribute VB_Name = "GradeRows"
Option Explicit

Public Sub GradeSample()
    ProcessGrades "Sample", 2
End Sub

Public Sub GradeThousand()
    ProcessGrades "Scale1000", 3
End Sub

Public Sub GradeBoundary()
    ProcessGrades "Boundary", 4
End Sub

Private Function AmountGrade(ByVal value As Variant, ByRef reason As String) As String
    Dim amount As Double
    reason = "OK"
    If IsError(value) Then
        reason = "CELL_ERROR"
        Exit Function
    End If
    If IsEmpty(value) Then
        reason = "EMPTY"
        Exit Function
    End If
    If VarType(value) = vbString Then
        If Len(value) = 0 Then
            reason = "EMPTY"
        Else
            reason = "TYPE"
        End If
        Exit Function
    End If
    Select Case VarType(value)
        Case vbByte, vbInteger, vbLong, vbSingle, vbDouble, vbCurrency, vbDecimal
        Case Else
            reason = "TYPE"
            Exit Function
    End Select
    amount = CDbl(value)
    If amount < 0 Then
        reason = "NEGATIVE"
        Exit Function
    End If
    If amount <> Fix(amount) Then
        reason = "DECIMAL"
        Exit Function
    End If
    Select Case amount
        Case Is >= 90000: AmountGrade = "A"
        Case Is >= 80000: AmountGrade = "B"
        Case Is >= 70000: AmountGrade = "C"
        Case Else: AmountGrade = "D"
    End Select
End Function

Private Sub ProcessGrades(ByVal sheetName As String, ByVal reportRow As Long)
    Const LAST_ALLOWED As Long = 1001
    Dim ws As Worksheet, check As Worksheet
    Dim lastRow As Long, candidate As Long, colNo As Long
    Dim rowCount As Long, i As Long, validCount As Long
    Dim data As Variant, outputs() As Variant, headers As Variant
    Dim grade As String, reason As String, problem As String
    Dim oldScreen As Boolean, oldEvents As Boolean
    Dim checkReady As Boolean, outputReady As Boolean

    oldScreen = Application.ScreenUpdating
    oldEvents = Application.EnableEvents
    On Error GoTo Failed
    Set check = ThisWorkbook.Worksheets("Check")
    If check.ProtectContents Then
        Err.Raise vbObjectError + 611, , "Use an unprotected practice Check sheet."
    End If
    checkReady = True
    Application.EnableEvents = False
    Application.ScreenUpdating = False
    check.Range("B" & reportRow & ":F" & reportRow).ClearContents
    check.Cells(reportRow, 2).Value2 = "RUNNING"
    Set ws = ThisWorkbook.Worksheets(sheetName)
    If ws.ProtectContents Then
        Err.Raise vbObjectError + 612, , "Use an unprotected practice input sheet."
    End If
    headers = Array("id", "amount", "grade", "reason", "expected_grade", "expected_reason", "match")
    For colNo = 1 To 7
        If IsError(ws.Cells(1, colNo).Value2) Then
            Err.Raise vbObjectError + 613, , "Invalid practice header."
        End If
        If CStr(ws.Cells(1, colNo).Value2) <> headers(colNo - 1) Then
            Err.Raise vbObjectError + 614, , "Practice headers must remain unchanged."
        End If
    Next colNo
    outputReady = True
    ws.Range("C2:D" & LAST_ALLOWED).ClearContents
    ws.Calculate
    lastRow = 1
    For colNo = 1 To 2
        candidate = ws.Cells(ws.Rows.Count, colNo).End(xlUp).Row
        If candidate > lastRow Then lastRow = candidate
    Next colNo
    If lastRow > LAST_ALLOWED Then
        Err.Raise vbObjectError + 615, , "Input is limited to rows 2 through 1001."
    End If
    rowCount = lastRow - 1
    If rowCount = 0 Then GoTo Complete
    data = ws.Range("A2:B" & lastRow).Value2
    ReDim outputs(1 To rowCount, 1 To 2)

    For i = 1 To rowCount
        grade = ""
        reason = "NAME"
        If Not IsError(data(i, 1)) Then
            If VarType(data(i, 1)) = vbString Then
                If Len(Trim$(data(i, 1))) > 0 Then
                    grade = AmountGrade(data(i, 2), reason)
                End If
            End If
        End If
        outputs(i, 1) = grade
        outputs(i, 2) = reason
        If reason = "OK" Then validCount = validCount + 1
    Next i
    ws.Range("C2").Resize(rowCount, 2).Value2 = outputs

Complete:
    check.Cells(reportRow, 2).Value2 = "DONE"
    check.Cells(reportRow, 3).Value = Now
    check.Cells(reportRow, 4).Value2 = rowCount
    check.Cells(reportRow, 5).Value2 = validCount
    check.Cells(reportRow, 6).Value2 = rowCount - validCount
    ws.Calculate
    check.Calculate
    Application.EnableEvents = oldEvents
    Application.ScreenUpdating = oldScreen
    Exit Sub

Failed:
    problem = Err.Description
    On Error Resume Next
    If outputReady Then ws.Range("C2:D" & LAST_ALLOWED).ClearContents
    If checkReady Then
        check.Range("B" & reportRow & ":F" & reportRow).ClearContents
        check.Cells(reportRow, 2).Value2 = "ERROR"
        check.Calculate
    End If
    Application.EnableEvents = oldEvents
    Application.ScreenUpdating = oldScreen
    On Error GoTo 0
    MsgBox "Grade processing failed: " & problem & vbCrLf & _
        "Do not reuse old output or an old timestamp.", vbExclamation
End Sub

14개 경계 입력에서 거부를 읽기

VarType은 변수의 하위 형식을 나타내는 정수를 반환합니다. 이 예제는 숫자 형식의 비음수 정수만 금액으로 받으며 숫자처럼 보이는 문자는 변환하지 않습니다.

입력 기대 등급 또는 사유
90000 / 89999 A / B
80000 / 79999 B / C
70000 / 69999 / 0 C / D / D
빈 금액 EMPTY
문자 95000, 문자 95,000원 TYPE
-1 NEGATIVE
70000.5 DECIMAL
=1/0 셀 오류 CELL_ERROR
빈 ID와 숫자 95000 NAME

문자 95000은 Boundary!B10에 문자로 저장돼 있습니다. CSV만 Excel로 열면 숫자로 자동 해석될 수 있어, 형식 거부 실험의 기준 파일은 XLSX입니다. =1/0은 의도적인 오류 입력이며 깨진 첨부가 아닙니다.

각 행의 오류는 빈 등급과 사유로 남기지만, 헤더 변경·보호·범위 초과는 전체 실행 오류입니다. 오류를 고치고 재실행할 때 C:D의 옛 결과는 지우도록 작성했습니다. 쓰기 실패가 나면 옛 시각이나 DONE을 신뢰하지 말고 새 사본에서 확인합니다. 실제 업무의 등급 규칙이나 반올림 정책을 이 가상 기준으로 대체하지 마세요.

검증 결과: 마지막 행과 처리 건수를 따로 대조하기

GradeThousand 실행 후 Scale1000!A1001=R1000, B1001=45000, C1001=D를 확인합니다. Check!D3의 처리 건수는 1000, E3의 유효 건수도 1000이어야 합니다. 마지막 셀만 맞고 중간 행이 빠지는 경우를 잡기 위해 Check의 실제 등급 개수를 독립 기대값 A300/B200/C200/D300과 비교하고 Scale1000!G2:G1001의 불일치가 0인지 함께 확인합니다. 어느 하나라도 다르면 완료 표시만으로 결과를 채택하지 않습니다.

독립 검산의 한계

Python 참조 계산으로 10행·1000행의 등급 분포, 1000번째 입력, 14개 경계 사유와 타입 보존을 확인합니다. 전체 BAS와 입력·테스트를 ZIP에 함께 넣었습니다. Microsoft Excel에서 VBA를 컴파일하거나 실행하지 않았습니다. 따라서 Excel의 실제 배열 쓰기, 오류 메시지, 이벤트 복구는 확인 대상입니다. Python 검산은 VBA 실행 증거가 아닙니다.

입력과 결과 칸을 찾는 기존 그림

그림은 실습 위치를 찾는 보조 자료입니다. 실제 실행 결과는 위 절차로 만든 사본의 값과 상태를 대조합니다.

숫자 비음수 정수의 A·B·C·D 등급 기준과 10행을 100회 반복한 1000행의 기대 개수 300·200·200·300을 설명한 도해
숫자 비음수 정수의 A·B·C·D 등급 기준과 10행을 100회 반복한 1000행의 기대 개수 300·200·200·300을 설명한 도해. 실제 앱 실행 화면이 아닌 설명 도해이며 Microsoft Excel·VBA·Outlook의 실행이나 성공을 확인한 증거가 아닙니다.
시트 2~1001행과 배열 1~1000행의 대응, 마지막 R1000·45000·D를 확인하는 도해
시트 2~1001행과 배열 1~1000행의 대응, 마지막 R1000·45000·D를 확인하는 도해. 실제 앱 실행 화면이 아닌 설명 도해이며 Microsoft Excel·VBA·Outlook의 실행이나 성공을 확인한 증거가 아닙니다.
Scale1000의 처리 1000·유효 1000과 Boundary의 처리14·유효7·거부7을 비교하고 오류 사유와 복구를 설명한 도해
Scale1000의 처리 1000·유효 1000과 Boundary의 처리14·유효7·거부7을 비교하고 오류 사유와 복구를 설명한 도해. 실제 앱 실행 화면이 아닌 설명 도해이며 Microsoft Excel·VBA·Outlook의 실행이나 성공을 확인한 증거가 아닙니다.

공식 자료

For…Next statement (VBA)

VarType function (Visual Basic for Applications)

Range.End property (Excel)

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

이어서 연습할 글

반복 구조와 별도로 등급 구간의 경계값을 다루는 연습은 Select Case의 5단계·7단계 등급 분류에서 이어집니다.

작성자 소개