"할 수 없다는 게 할 일이 없다는 뜻은 아니야."
옵셔널이 약속하는 것
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).
- 사람이 알아야 하면 드러내. 빠진 게 뭔지 짚어주는 알림이나 상태 줄로.
- 값이 없는 게 진짜로 할 일이 없다는 뜻일 때만 조용히 반환하고, 왜 그런지 주석을 달아.