매크로로 파일을 저장할 때 C:\보고서\2021\08\ 처럼 날짜별 폴더에 넣고 싶은 경우가 많습니다. 그런데 VBA의 MkDir 은 상위 폴더가 이미 있어야만 동작합니다. C:\보고서 가 없는 상태에서 MkDir "C:\보고서\2021\08" 을 하면 "경로를 찾을 수 없습니다" 오류가 나죠. 그래서 경로를 한 단계씩 확인하면서 없는 폴더만 만들어 주는 함수를 하나 두고 씁니다.
Public Function FolderCheck(fPath$) As String
Dim Folders As Variant, Path$, i%
Folders = Split(fPath, "\")
Path = Folders(0)
For i = 1 To UBound(Folders)
If Len(Folders(i)) Then
Path = Path + "\"
Path = Path + Folders(i)
If Len(Dir(Path + "\", vbDirectory)) = 0 Then MkDir Path
End If
Next
FolderCheck = Path + "\"
End Function
어떻게 동작하나
Split(fPath, "\") 로 경로를 조각냅니다. C:\보고서\2021\08\ 이라면 C:, 보고서, 2021, 08, 그리고 마지막 \ 때문에 생긴 빈 문자열까지 다섯 조각입니다. 첫 조각(드라이브)을 시작점으로 두고, 나머지를 하나씩 붙여 가면서 그 경로에 폴더가 있는지 Dir(..., vbDirectory) 로 확인합니다. 없으면 그 자리에서 MkDir 합니다. 앞 단계 폴더가 먼저 만들어지니 다음 단계 MkDir 이 실패할 일이 없습니다.
If Len(Folders(i)) Then 은 빈 조각을 건너뛰는 조건입니다. 경로 끝에 \ 가 있거나 중간에 \\ 처럼 두 번 들어간 경우에도 엉뚱한 폴더를 만들지 않게 해 줍니다. 그래서 끝에 \ 를 붙여 넘기든 안 붙이든 같은 결과가 나옵니다.
반환값은 항상 끝에 \ 가 붙은 경로입니다. 이게 은근히 편해서, 아래처럼 파일 이름을 바로 이어 붙일 수 있습니다.
Dim savePath As String
savePath = FolderCheck("C:\보고서\" & Format(Date, "yyyy\\mm"))
ActiveWorkbook.SaveCopyAs savePath & "일일보고.xlsx"
이미 폴더가 다 있는 경우에는 아무것도 만들지 않고 경로만 돌려주니, 저장할 때마다 습관처럼 한 번 거쳐 가게 해 두면 "폴더가 없어서 저장 실패" 오류를 볼 일이 없습니다.
주의할 경우
넘기는 값은 폴더 경로여야 합니다. C:\보고서\일일보고.xlsx 처럼 파일 이름까지 넣으면 일일보고.xlsx 라는 이름의 폴더가 만들어집니다.
네트워크 공유 경로(\\서버\공유\폴더)는 맞지 않습니다. 앞의 \\ 때문에 첫 두 조각이 빈 문자열이 되어 경로가 \서버\공유... 로 조립되고, 현재 드라이브 루트에 폴더가 만들어지는 식으로 엇나갑니다. 공유 폴더에 쓸 거라면 앞부분 \\서버\공유 를 시작점으로 따로 잡도록 고치거나, 네트워크 드라이브 문자(Z: 등)로 연결해서 쓰는 편이 간단합니다.
권한이 없는 위치거나 폴더 이름에 쓸 수 없는 문자(: * ? " < > |)가 들어가면 MkDir 에서 오류가 나는데, 이 함수에는 오류 처리가 없어서 호출한 쪽에서 멈춥니다. 사용자가 입력한 이름으로 폴더를 만든다면 호출부에서 On Error 처리를 해 두세요.
공유 폴더 경로까지 받으려면?
위에서 말한 \\서버\공유 문제를 고친 형태입니다. 경로가 \\ 로 시작하면 서버 이름과 공유 이름까지를 시작점으로 묶어 두고, 그 아래부터 폴더를 만듭니다. 공유 자체는 MkDir 로 만들 수 있는 게 아니라서 이렇게 건너뛰어야 합니다.
Public Function FolderCheckUNC(ByVal fPath As String) As String
Dim Folders As Variant, Path As String, i As Integer, startIdx As Integer
Folders = Split(fPath, "\")
If Left$(fPath, 2) = "\\" Then
Path = "\\" & Folders(2) & "\" & Folders(3)
startIdx = 4
Else
Path = Folders(0)
startIdx = 1
End If
For i = startIdx To UBound(Folders)
If Len(Folders(i)) Then
Path = Path & "\" & Folders(i)
If Len(Dir(Path & "\", vbDirectory)) = 0 Then MkDir Path
End If
Next
FolderCheckUNC = Path & "\"
End Function
\\서버\공유\보고서 를 \ 로 자르면 빈 문자열 두 개, 서버, 공유, 보고서 순서가 되니 인덱스 2와 3이 서버와 공유 이름입니다. 경로에 공유 이름까지 없는 \\서버 만 넘기면 Folders(3) 에서 첨자 오류가 나니 그런 값은 넘기지 마세요. 원래 함수와 달리 인수에 ByVal 을 붙였는데, 호출하는 쪽 변수가 String 이 아닐 때 "ByRef 인수 형식이 일치하지 않습니다" 오류가 나는 걸 피하려는 것입니다.
FileSystemObject 를 쓰면 한 번에 되나?
Scripting.FileSystemObject 의 CreateFolder 도 MkDir 과 마찬가지로 바로 위 폴더가 있어야 만들 수 있습니다. 그래서 FSO 로 바꿔도 단계별로 확인하는 구조는 똑같이 필요합니다. 차이라면 FolderExists 로 존재 여부를 조금 더 읽기 쉽게 확인할 수 있다는 정도입니다. Dir 은 내부적으로 검색 상태를 하나만 가지고 있어서, 다른 곳에서 Dir 로 파일 목록을 돌리는 중에 이 함수를 부르면 그쪽 반복이 엉킵니다. 파일 목록 루프 안에서 폴더를 만들어야 한다면 이 부분만 FolderExists 로 바꿔 두는 게 안전합니다.