엑셀 셀 값으로 파일 이름을 만들어 PDF나 시트를 저장하는 매크로는 흔한데, 꼭 한 번은 "파일 이름이 잘못되었습니다" 오류에 걸립니다. 거래처명에 / 가 들어 있거나, 웹에서 긁어 온 게시글 제목에 ? 나 : 가 섞여 있는 경우죠. 윈도 파일 이름에는 \ / : * ? " < > | 아홉 문자를 쓸 수 없어서, 저장하기 전에 이걸 다른 문자로 바꿔 줘야 합니다.

Function changeFileName(txt)
    s = txt
    s = Replace(s, "\", "_")
    s = Replace(s, "/", "_")
    s = Replace(s, ":", "_")
    s = Replace(s, "*", "_")
    s = Replace(s, "?", "_")
    s = Replace(s, """", "_")
    s = Replace(s, "<", "_")
    s = Replace(s, ">", "_")
    s = Replace(s, "|", "_")
    changeFileName = s
End Function

Replace 를 금지 문자 수만큼 이어서 부르는 단순한 구조입니다. 큰따옴표는 VBA 문자열 안에서 """" 처럼 두 번 겹쳐 써야 따옴표 한 글자가 된다는 점만 눈여겨보면 됩니다. 역슬래시는 VBA에서 특별한 의미가 없어서 "\" 그대로 씁니다.

광고

쓰는 방법

경로 전체가 아니라 파일 이름 부분에만 적용해야 합니다. 경로까지 넣으면 폴더 구분자인 \ 와 드라이브 뒤의 : 까지 밑줄로 바뀌어 엉뚱한 이름이 됩니다.

Dim fName As String
fName = changeFileName(Range("A2").Value)
ThisWorkbook.SaveCopyAs ThisWorkbook.Path & "\" & fName & ".xlsx"

이 함수로 다 해결되지 않는 경우

윈도에는 문자 말고도 걸리는 규칙이 몇 가지 더 있습니다. 이름 끝이 공백이나 마침표면 윈도가 그걸 잘라 버려서 의도와 다른 이름으로 저장되거나 나중에 파일을 다루기가 까다로워집니다. CON, PRN, AUX, NUL, COM1, LPT1 같은 이름은 장치 이름으로 예약돼 있어서 확장자를 붙여도 파일로 만들 수 없고요. 거래처명이나 제목처럼 사람이 입력한 값이라면 Trim 으로 앞뒤 공백을 지우고, 끝의 마침표를 제거하는 처리를 함께 넣어 두면 오류가 거의 사라집니다.

웹에서 가져온 텍스트에는 줄바꿈이나 탭 같은 제어 문자가 들어 있는 경우도 많습니다. 이것도 파일 이름에 쓸 수 없으니 vbCr, vbLf, vbTab 도 같은 방식으로 바꿔 주는 게 좋습니다. 그리고 경로 전체 길이가 260자 근처를 넘으면 이름이 올바라도 저장이 실패할 수 있어서, 제목이 긴 데이터를 다룬다면 Left(fName, 100) 처럼 적당히 잘라 쓰는 걸 권합니다.

치환할 문자가 많아지면 금지 문자를 배열에 담아 For Each 로 돌리는 형태로 바꾸면 줄 수가 줄고 관리하기도 편합니다. VB.NET이라면 IO.Path.GetInvalidFileNameChars() 가 금지 문자 목록을 돌려주니 그걸 써도 됩니다.

위 내용을 한 함수로 묶은 버전

금지 문자 치환, 줄바꿈·탭 제거, 길이 제한, 끝의 공백·마침표 제거, 예약 이름 회피를 한곳에 모았습니다. 길이를 먼저 자르고 나서 끝부분을 다듬는 순서가 중요합니다. 반대로 하면 잘린 자리에 공백이나 마침표가 새로 남을 수 있거든요.

Function SafeFileName(ByVal txt As String, Optional ByVal maxLen As Long = 100) As String
    Dim bad As Variant, ch As Variant, s As String
    bad = Array("\", "/", ":", "*", "?", """", "<", ">", "|", vbCr, vbLf, vbTab)
    s = txt
    For Each ch In bad
        s = Replace(s, ch, "_")
    Next ch
    If Len(s) > maxLen Then s = Left$(s, maxLen)
    s = LTrim$(s)
    Do While Right$(s, 1) = "." Or Right$(s, 1) = " "
        s = Left$(s, Len(s) - 1)
    Loop
    Select Case UCase$(s)
        Case "CON", "PRN", "AUX", "NUL"
            s = "_" & s
        Case Else
            If UCase$(s) Like "COM#" Or UCase$(s) Like "LPT#" Then s = "_" & s
    End Select
    If Len(s) = 0 Then s = "_"
    SafeFileName = s
End Function

SafeFileName("2021 견적서: A/S 건. ") 은 2021 견적서_ A_S 건 이 됩니다. 마지막 줄의 빈 문자열 처리도 빼지 마세요. 셀이 비어 있으면 이름 없이 .xlsx 만 남은 파일을 만들려고 하게 됩니다. 반복문으로 여러 행을 저장하는 중이라면 빈 행마다 같은 이름을 덮어쓰거나 오류로 멈추니, 빈 값은 함수 안에서든 호출부에서든 한 번 걸러 주는 게 좋습니다.

날짜나 숫자 셀을 이름으로 쓸 때

Range("A2").Value 가 날짜라면 문자열로 바뀌면서 제어판의 날짜 형식을 따릅니다. 한국어 윈도 기본값이면 2021-07-01 처럼 나오지만, 형식이 2021/07/01 로 설정된 PC에서는 / 가 밑줄로 바뀌어 2021_07_01 이 됩니다. PC마다 파일 이름이 달라지는 걸 막으려면 Format(Range("A2").Value, "yyyymmdd") 처럼 형식을 직접 지정해서 넘기는 게 확실합니다. 셀에 보이는 그대로를 쓰고 싶다면 .Value 대신 .Text 를 쓰는 방법도 있는데, 열 너비가 좁아 ### 으로 보이는 셀은 그 ### 이 그대로 들어온다는 함정이 있습니다.