문제의 시작

  • 내가 Xcode를 사용하게 되면서 들이게 된 버릇 중 하나는 파일을 지우지 않는 것이다. 리팩터링을 하는 중이거나 필요없어진 파일을 지우게 되면 프로젝트 파일(.pbxproj)에 반영되고 머지할 때 이따금 컨플릭트가 일어나는데, 파일 내 변경사항에 대한 건 금방 풀 수 있지만 프로젝트 파일을 망치게 되면 매우 귀찮아지기 때문이다.

어우 토나와

대안

  • 결국 나는 파일 내용만 지우는 방법을 택했다. 이렇게 하면 결국 컨플릭나는건 파일 단위로 나고 머지할 때 흐름이 깨지지 않고 해결하면 되었기 때문이다. 하지만 아무래도 가장 큰 단점은 빈 쓰레기 파일이 자꾸 남는다는 것이었다. 그래서 이걸 해결하기 위해 스크립트를 작성했다.

빈 파일 지우기

  • 원래는 이런 스크립트 종류의 작업은 모두 Python으로 했겠지만 모두가 Python을 다루지 않다 보니 Swift로 작성해 프로젝트에 넣기로 결정했다. 프로젝트에 Mac Command Line Tool로 타겟을 만들고 실행시킬 수 있도록 스킴으로 만들어 넣는다(사진은 Xcode지만 사실 설정은 Tuist에서 해 주었다).

  • 중요한 건 인자를 전달하는 것인데, Xcode 내에서 이미 환경변수로 설정된 $(PROJECT_DIR)을 실행인자로 넣어주면 프로젝트의 실행경로를 전달받을 수 있다.

스크립팅

  • 타겟의 main.swift에서 1번 인덱스로 전달받는다. 여타 다른 프로그램들이 그렇듯이 0번 인자에는 프로그램 그 자체가 떨어진다.
  • 프로젝트가 멀티모듈이기 때문에 넘겨받은 프로젝트 루트의 상위 폴더로부터 빈 swift 파일을 찾기 위한 순회를 돌고 파일을 제거한다.
import Foundation
 
let rootURL = URL(filePath: CommandLine.arguments[1].appending("/.."))
let fileManager = FileManager.default
 
func main() {
    do {
        try traverse(dirURL: rootURL)
    } catch {
        print(error.localizedDescription)
    }
}
 
func traverse(dirURL: URL) throws {
    try FileManager
        .default
        .contentsOfDirectory(atPath: dirURL.path())
        .compactMap { name in
            dirURL.appending(path: name)
        }
        .forEach { url in
            var isDirectory: ObjCBool = false
 
            if FileManager.default.fileExists(
                atPath: url.path(),
                isDirectory: &isDirectory
            ) {
                if isDirectory.boolValue {
                    try traverse(dirURL: url)
                } else {
                    try handleFile(fileURL: url)
                }
            }
        }
}
 
func handleFile(fileURL: URL) throws {
    guard fileURL.pathExtension == "swift",
          let data = try? Data(contentsOf: fileURL),
          let content = String(data: data, encoding: .utf8),
          content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    else {
        return
    }
 
    try FileManager.default.removeItem(atPath: fileURL.path())
    print("제거 완료: \(fileURL.path())")
}
 
main()
  • 피쳐가 끝나고 머지 후에 종종 돌려주고 있다. 이런걸 CI에서 하면 더욱 좋겠지만 크게 효용이 없으니 다음 기회에…🫥