Swift에서 guard let vs 중첩 if let
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. 참조 링크
- guard 문법: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/controlflow/#Early-Exit
- if 구문: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/controlflow/#Conditional-Statements
- 옵셔널: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Optionals
- 옵셔널 바인딩: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Optional-Binding
15. 결론
- 실패를 한 곳에서 처리하고 정상 흐름을 평탄화하려면 guard let이 정답입니다.
- 매우 단순한 1단계 체크만 필요하고 값의 사용 범위가 제한적일 때만 중첩 없이 if let을 선택하세요.
- 다단계 검증, 네트워크/파싱, UI 입력 처리 등 대부분의 실무 상황에서 guard가 더 읽기 쉽고 안전합니다.