본문 바로가기
C.W.K.
Stream
Lesson 02 of 06 · published

옵셔널과 데이터를 잃는 조기 반환

~14 min · swift-for-apple, optionals, guard, silent-failure

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"할 수 없다는 게 할 일이 없다는 뜻은 아니야."

옵셔널이 약속하는 것

Optional은 값이 없을 수도 있다고 Swift가 대놓고 말하는 방식이야. if let, guard let, ??, 옵셔널 체이닝이 값이 없는 경우를 다루는 도구고, 컴파일러는 네가 그걸 까먹게 두지 않아. 여기까진 좋은 얘기야.

나쁜 얘기는 Swift에선 값이 없을 때 그냥 아무것도 안 하고 넘어가기가 너무 쉽다는 거야. guard let x else { return }은 컴파일도 되고 깔끔해 보이는데, "이건 할 수 없었다"를 "아무 일도 없었다"로 바꿔버려. 그러면서 로그도 에러도 안 남기고, 사용자한테도 아무 신호를 안 줘.

성공한 것처럼 보였던 공유 확장

가족 iOS 캡처 앱 하나에 공유 확장이 있었어. 아무 앱에서나 메모를 공유하고 게시를 누르면, 메모가 공유 App Group 폴더에 떨어지고 본 앱이 가져가는 구조야. 첫 버전은 컨테이너를 이렇게 읽었어. guard let root = SharedInbox.containerURL() else { return }. 그것도 defer로 시트를 닫는 메서드 안에서 말이야.

서명 안 한 시뮬레이터 빌드엔 App Group 엔타이틀먼트가 없어서 컨테이너 URL이 nil이었어. 시트가 뜨고, 게시를 누르면 닫히고, 아무것도 안 써졌어. 딱 성공처럼 보였지. 캡처 화면에선 이게 제일 나쁜 실패야. 사용자는 며칠 뒤, 저장한 줄 알았던 걸 찾다가 그제야 알게 되거든.

이런 패턴은 어디에나 있어. 설정에 따라 없을 수도 있는 OS 자원(App Group 컨테이너, 키체인 항목, 사진 보관함, 백그라운드 모드)을 guard let … else { return }으로 받으면, 설정이 틀어진 날 데이터를 조용히 잃는 버그를 심어두는 거야. 고친 확장은 빠진 컨테이너 이름을 알려주는 알림을 띄워. 입력한 메모는 "계속 편집" 버튼으로 지키고, 사용자가 직접 버리기를 골랐을 때만 버려.

옵셔널 체이닝도 아무것도 안 할 수 있어

Mac 클라이언트 하나가 모델을 통해 웹 미리보기 창을 조종했어. model.webView?.load(request). 근데 웹 뷰는 SwiftUI 창이 그려질 때만 만들어졌어. 그리고 그 창을 띄우는 조건은 화면 없는 스모크 테스트에선 false였지. webView는 계속 nil이었고, 옵셔널 체이닝 호출은 아무것도 안 한 채 성공했어. 내비게이션 콜백은 한 번도 안 왔고, 테스트는 끝없이 기다렸지. "없다"가 진짜로 "할 일 없다"일 땐 옵셔널 체이닝이 맞아. "위에서 뭔가 고장 났다"는 뜻이면 그렇다고 말해야 해.

솔직한 선택지 셋

  • 호출자가 뭔가 할 수 있으면 던져(throw).
  • 사람이 알아야 하면 드러내. 빠진 게 뭔지 짚어주는 알림이나 상태 줄로.
  • 값이 없는 게 진짜로 할 일이 없다는 뜻일 때만 조용히 반환하고, 왜 그런지 주석을 달아.

Code

조용한 버전과 솔직한 버전·swift
import Foundation

let group = "group.com.example.spark"

// Silent: a missing entitlement becomes "nothing happened".
func saveSharedNoteSilently(_ text: String) {
    guard let root = FileManager.default
        .containerURL(forSecurityApplicationGroupIdentifier: group) else { return }
    try? Data(text.utf8).write(to: root.appendingPathComponent("note.txt"))
}

// Honest: absence of the container is an error that names itself.
enum ShareSaveError: Error, CustomStringConvertible {
    case containerMissing(String)
    var description: String {
        switch self {
        case .containerMissing(let g):
            return "The shared folder \(g) is not reachable. Check the App Group entitlement on both targets."
        }
    }
}

func saveSharedNote(_ text: String) throws {
    guard let root = FileManager.default
        .containerURL(forSecurityApplicationGroupIdentifier: group) else {
        throw ShareSaveError.containerMissing(group)
    }
    try Data(text.utf8).write(to: root.appendingPathComponent("note.txt"), options: .atomic)
}

External links

Exercise

네가 가진 Swift 코드(아니면 좀 자란 Spark 패키지)에서 else { return }try?를 찾아. 찾은 것마다, 값이 없는 상황이 그냥 할 일이 없다는 뜻인지, 호출자가 뭔가 할 수 있는 건지, 사람이 알아야 하는 건지 나눠 봐. 제일 나쁜 곳은 원인이 드러나는 에러를 던지거나 화면에 드러내도록 고쳐. 남은 조용한 반환엔 왜 괜찮은지 한 줄 주석을 달아.
Hint
위험한 곳은 OS 자원 근처에 몰려 있어. 컨테이너 URL, 키체인 조회, 파일 읽기, 권한 상태. 앱을 어떻게 빌드하고 설정했느냐에 따라 값이 빠질 수 있다면, 조용한 반환은 설정 버그를 숨기게 돼.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.