본문 바로가기
감정일기(가칭)

🪵 개발/배포 환경을 구분한 로깅 설계

by 밤새는 탐험가89 2025. 10. 17.
728x90
SMALL

— #if DEBUG와 LogManager로 안전하게 로그 관리하기

앱을 만들다 보면 에러나 상태를 확인하기 위해 print()를 많이 쓰게 되죠.
하지만 프로젝트 규모가 조금만 커져도 이런 고민이 생깁니다 👇

 

콘솔에 로그가 너무 많이 섞여서 보기 힘듦

Core Data, FileManager, API 요청 로그가 한데 모여서 구분 어려움

릴리즈(Release) 빌드에서도 로그가 남아 보안상 위험

디버깅은 쉬운데, 나중엔 유지보수성이 떨어짐

 

그래서 저는 개발 중에는 로그를 보고,

배포 후에는 아예 로그가 남지 않도록 #if DEBUG를 활용했고,
거기에 LogManager 유틸 클래스를 함께 설계했습니다.


💡 왜 유틸 클래스(LogManager)로 만들었나?

처음엔 그냥 #if DEBUG print()로도 충분해 보였어요.
그런데 개발이 진행될수록 일관된 로깅 방식이 필요해졌습니다.
아래는 제가 LogManager를 만들기로 한 이유예요 👇

 

🧩 1️⃣ 로그 포맷을 통일하고 싶었다

여러 파일에서 print가 제각각이면 로그를 읽기가 너무 힘들어요.
하나의 진입점(LogManager.print())을 두면
모든 로그 형식을 한 곳에서 제어할 수 있습니다.

 

⚙️ 2️⃣ 함수명, 라인 번호를 자동으로 찍고 싶었다

에러가 어디서 났는지 찾기 위해 매번 print에 함수명을 적을 수는 없잖아요?
#function과 #line을 유틸 내부에서 자동으로 찍게 만들면,
로그만 봐도 어떤 메서드에서 발생했는지 바로 알 수 있습니다.

 

🚀 3️⃣ 나중에 확장하기 쉽게 만들고 싶었다

나중에 OSLog나 Firebase Crashlytics로 옮길 때,
LogManager 내부만 바꾸면 호출부는 그대로 유지됩니다.
즉, 로깅 계층을 한 번만 만들어두면 평생 재사용 가능!

 

🧱 4️⃣ 배포 환경에서는 완전히 차단하고 싶었다

#if DEBUG를 LogManager 내부에 넣어두면
Release 빌드에선 로그 코드 자체가 컴파일되지 않습니다.
즉, 사용자에게 로그가 찍히지도 않고, 성능 저하도 없어요 ✅


🧭 LogManager 구현 코드

Helper/LogManager/LogManager.swift 파일에 아래 코드를 그대로 넣으면 됩니다 👇

import Foundation

enum LogType: String {
    case info    = "ℹ️"
    case success = "✅"
    case warning = "⚠️"
    case error   = "❌"
}

final class LogManager {
    static func print(
        _ type: LogType,
        _ message: String,
        function: String = #function,
        line: Int = #line
    ) {
        #if DEBUG
        Swift.print("\(type.rawValue) [\(function):\(line)] - \(message)")
        #endif
    }
}

🪄 사용 예시

LogManager.print(.info, "Core Data fetch 시작")
LogManager.print(.success, "이미지 저장 성공: \(path)")
LogManager.print(.warning, "API 응답 지연 발생")
LogManager.print(.error, "파일 삭제 실패: \(error.localizedDescription)")

 

콘솔 출력 예시는 이렇게 보입니다 👇

✅ [saveImage(_:diaryID:index:):42] - 이미지 저장 성공: image_0.jpg
❌ [deleteImage(diaryID:index:):75] - 파일 삭제 실패: The file doesn’t exist.

🧐 Swift.print()는 왜 쓰나요?

여기서 Swift.print() 는 단순한 print가 아니라,
Swift 표준 라이브러리의 print 함수를 직접 호출하겠다는 의미예요.

이게 왜 중요하냐면 👇

 

혹시 프로젝트 어딘가에서 print라는 함수를 오버라이드했거나,
같은 이름으로 확장했을 경우 충돌을 방지해줍니다.

즉, Swift.print()를 쓰면 “원래의 print 함수”를 확실히 호출할 수 있죠.

 

💬 정리하면,

Swift.print()는 ‘안전하게 콘솔에 찍는다’는 의미의 가장 확실한 선택이에요.


🧩 FileManager 코드에 적용하기

이제 우리가 만든 DiaryImageFileManager에 이렇게 적용할 수 있습니다 👇

do {
    try data.write(to: fileURL)
    LogManager.print(.success, "이미지 저장 성공: \(fileURL.lastPathComponent)")
} catch {
    LogManager.print(.error, "이미지 저장 실패: \(error.localizedDescription)")
}

이렇게 하면 개발 중에는 로그가 깔끔하게 출력되고,
배포 버전에서는 아무 로그도 찍히지 않습니다 🙌


✨ 한 줄 요약


“로그는 흩뿌리는 게 아니라 모아야 합니다.”
개발 단계에서는 명확하게 보고,
배포 후에는 깔끔하게 사라지는 —
그 중간 다리를 LogManager가 책임집니다 🪄

728x90
LIST