문제의 시작
- 내가 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에서 하면 더욱 좋겠지만 크게 효용이 없으니 다음 기회에…🫥