개발/Swift

Swift에서 guard let vs 중첩 if let

까칠코더 2025. 11. 11. 12:46
반응형

Swift에서 guard let vs 중첩 if let 

 

옵셔널 언래핑은 Swift 코드의 안정성과 가독성을 좌우합니다. 같은 언래핑이라도 guard let과 중첩 if let은 의도, 제어 흐름, 유지보수성에서 큰 차이가 납니다. 실무에서 어떤 기준으로 선택해야 하는지 예제와 함께 정리했습니다.

 

1. 핵심 결론

항목 guard let 중첩 if let
목적 조기 종료(Early Exit) 로 정상 경로를 평탄화 단계별로 조건을 통과하며 중첩 증가
스코프 바인딩한 상수/변수가 이후 블록 전체에서 유효 바인딩 값이 if 블록 내부로 제한
가독성 Happy Path가 왼쪽(바깥)으로 흐름 들여쓰기(Nesting) 증가로 가독성 저하
실패 처리 else 블록 한 곳에서 명확히 처리 각 단계마다 실패 분기 작성 필요
실무 권장 네트워킹, 입력 검증, 컨트롤러 흐름 등 대부분 상황에서 권장 매우 간단한 1단계 체크에서만 고려

요약: 여러 옵셔널을 연속으로 확인하거나 실패 시 즉시 반환/중단해야 하는 경우 guard let이 표준 선택입니다.

 

2. 기본 형태 비교


guard let 예제

func handle(_ input: String?) {
    guard let text = input, !text.isEmpty else {
        print("유효하지 않은 입력")
        return
    }
    print(text.uppercased()) // text는 여기서 non-optional
}

중첩 if let 예제

func handle(_ input: String?) {
    if let text = input {
        if !text.isEmpty {
            print(text.uppercased())
        } else {
            print("유효하지 않은 입력")
        }
    } else {
        print("유효하지 않은 입력")
    }
}

두 코드는 동치지만, guard가 들여쓰기 없이 정상 경로를 강조합니다.

 

3. 다중 바인딩과 조건 결합


여러 값을 한 번에 바인딩

guard
    let user = currentUser,
    let token = user.sessionToken,
    token.isValid
else {
    logout()
    return
}

print("Hello, \(user.name)")

중첩 if let로 동일 로직을 작성하면

if let user = currentUser {
    if let token = user.sessionToken {
        if token.isValid {
            print("Hello, \(user.name)")
        } else {
            logout(); return
        }
    } else {
        logout(); return
    }
} else {
    logout(); return
}

실패 경로가 늘어날수록 분기 중복 가독성 저하가 커집니다.

 

4. guard의 스코프 이점

guard let으로 바인딩한 상수는 함수의 이후 코드 전체에서 non‑optional로 사용됩니다. 이는 API 사용 시 강제 언래핑 제거 옵셔널 체이닝 남용 회피에 도움됩니다.

guard let url = URL(string: urlString) else { return }
// 이후 모든 코드에서 url은 URL (non-optional)

중첩 if let의 바인딩은 해당 블록 안에서만 유효하므로, 블록 외부에서 다시 언래핑해야 하는 불편함이 생길 수 있습니다.

 

5. 실패 처리의 일관성: 한 곳에서 끝내기

실패 시 리턴/throw/continue/break 등의 조기 종료를 한 곳에서 처리하면, 이후의 “정상 흐름”이 깨끗해집니다.

func process(_ data: Data?) throws {
    guard let data else { throw MyError.missingData }
    // 정상 코드
}

중첩 if에서는 여러 위치에 실패 처리 코드가 흩어져 유지보수성이 떨어집니다.

 

6. guard where 스타일

guard의 조건 절에 추가 조건을 함께 적을 수 있습니다.

guard let age = Int(input), (0..<130).contains(age) else {
    throw ValidationError.invalidAge
}

옵셔널 언래핑과 유효성 검증을 한 번의 가드로 처리합니다.

 

7. try? / try와 결합

guard let payload = try? JSONDecoder().decode(Payload.self, from: data) else {
    logger.error("디코딩 실패")
    return
}
handle(payload)

실패를 nil로 흡수할지(try?) 예외로 올릴지(try + do/catch)는 정책에 따릅니다.

 

8. 비동기 / 탈출 클로저 컨텍스트


비동기 함수

func fetch() async throws -> Model {
    guard let url = URL(string: endpoint) else { throw URLError(.badURL) }
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Model.self, from: data)
}

탈출 클로저 내부

탈출 클로저 안에서도 guard는 조기 종료를 명확히 해줍니다.

service.request { [weak self] result in
    guard let self else { return }
    guard case .success(let data) = result else { return }
    self.render(data)
}

 

9. UI 흐름 제어 (UIKit/SwiftUI)


UIKit

@IBAction func didTapSubmit(_ sender: UIButton) {
    guard let text = textField.text, !text.isEmpty else {
        showAlert("내용을 입력하세요")
        return
    }
    submit(text)
}

SwiftUI

뷰 로직에서도 guard는 액션 핸들러에서 입력 검증/흐름 제어를 단순화합니다.

Button("확인") {
    guard !form.name.isEmpty else {
        toast = "이름은 필수입니다"
        return
    }
    save()
}

 

10. 흔한 안티 패턴과 교정

10.1 deep nesting (피라미드 코드)

if let a = a {
  if let b = b {
    if let c = c {
      // ...
    }
  }
}


교정: guard로 평탄화

10.2 여러 곳에서 return/throw 남발

여러 if 분기로 실패 처리를 나누지 말고, 하나의 guard 블록으로 모으세요.

10.3  옵셔널 강제 언래핑(!) 남용

guard let으로 언래핑 후 사용하면 강제 언래핑이 필요 없습니다.

10.4  불필요한 두 번 언래핑

중첩 if가 블록 범위 때문에 같은 값을 다시 언래핑하게 만드는지 확인하세요. guard로 개선 가능합니다.

 

11. 선택 가이드

상황 권장
다단계 검증/전처리 필요 guard let
실패 시 즉시 반환/중단 guard + return/throw/continue/break
단일 간단 체크, 블록 내부 한 번만 사용 단일 if let 가능
옵셔널 값이 이후 코드 전반에서 필요 guard let으로 스코프 확장

 

12. 예제 모음


12.1 로그인 요구 시나리오

func openProfile() {
    guard let user = Auth.currentUser else {
        router.showLogin()
        return
    }
    router.showProfile(of: user)
}

12.2 파일 로드 + 파싱

func loadConfig(path: String?) -> Config? {
    guard
        let path,                       // 1) nil 아님
        let data = try? Data(contentsOf: URL(fileURLWithPath: path)),  // 2) 파일 읽힘
        let cfg = try? JSONDecoder().decode(Config.self, from: data)   // 3) 파싱
    else { return nil }
    return cfg
}

12.3 테이블셀 구성시 필수 데이터 확인

func configure(_ cell: Cell, with model: Model?) {
    guard let model else { cell.isHidden = true; return }
    cell.title = model.title
}

 

13. 성능 관점 메모

둘 모두 컴파일러 최적화 하에 비용 차이는 미미합니다. 차이는 제어 흐름과 스코프에서 오며, 그 결과로 불필요한 언래핑/체이닝/중첩을 줄여 읽기·유지보수 비용을 낮춥니다.

 

14. 참조 링크

 

15. 결론

  • 실패를 한 곳에서 처리하고 정상 흐름을 평탄화하려면 guard let이 정답입니다.
  • 매우 단순한 1단계 체크만 필요하고 값의 사용 범위가 제한적일 때만 중첩 없이 if let을 선택하세요.
  • 다단계 검증, 네트워크/파싱, UI 입력 처리 등 대부분의 실무 상황에서 guard가 더 읽기 쉽고 안전합니다.
반응형