저는 디버깅에 너무 많은 시간을 씁니다. 누구나 그렇죠. 하지만 Xcode에는 대부분의 개발자가 거의 건드리지 않는 놀랍도록 강력한 도구들이 있습니다. 이것은 제가 수년에 걸쳐 구축한 디버깅 워크플로우—문제가 발생했을 때 실제로 시간을 절약하는 것들입니다.
브레이크포인트는 생각보다 강력합니다#
대부분의 사람들은 거터를 클릭해서 브레이크포인트를 설정하고 그게 전부입니다. 하지만 우클릭하면 디버깅 방식을 바꿀 수 있는 많은 옵션을 발견할 수 있습니다.
조건부 브레이크포인트#
1000개의 항목을 처리하는 루프가 있는데 347번째 항목만 문제를 일으킨다고 해봅시다. 346번이나 브레이크포인트에 걸리고 싶지 않겠죠.
for i in 0..<users.count {
processUser(users[i]) // 여기에 브레이크포인트 설정, 조건: i == 347
}브레이크포인트를 우클릭하고 조건 i == 347을 추가하면 Xcode는 그것이 참일 때만 멈춥니다. userId == "abc123" 또는 error != nil 같은 것도 똑같이 작동합니다.
심볼릭 브레이크포인트#
메서드가 어디서 호출되는지 모를 때 유용합니다. 브레이크포인트 네비게이터(Cmd+8)로 가서 + 버튼을 클릭하고 “Symbolic Breakpoint"를 선택합니다.
모든 뷰 컨트롤러가 로드될 때마다 보고 싶으신가요? 심볼을 viewDidLoad로 설정하세요. Xcode는 모든 곳에서 일시 정지합니다. 너무 시끄럽나요? 좁혀보세요: MyViewController.viewDidLoad.
제가 항상 갖고 있는 가장 유용한 것들:
UIViewAlertForUnsatisfiableConstraints- Auto Layout 충돌을 즉시 포착objc_exception_throw- 크래시 전에 Objective-C 예외에서 중단malloc_error_break- 메모리 할당 문제 발견
예외 브레이크포인트#
새 프로젝트에서 가장 먼저 해야 할 일일 겁니다. 예외 브레이크포인트를 추가하면(브레이크포인트 네비게이터에서 + → Exception Breakpoint) Xcode는 앱이 이미 크래시한 후가 아니라 무언가가 throw될 때 바로 일시 정지합니다.
콘솔에서 신비한 크래시로만 보이는 많은 nil unwrapping 문제를 이걸로 잡았습니다.
브레이크포인트 액션#
여기서부터 재미있어집니다. 브레이크포인트는 실행을 멈출 필요가 없습니다—단지 로그를 남기거나 소리를 재생할 수 있습니다.
브레이크포인트를 편집하고 “Add Action"을 클릭하면:
- 멈추지 않고 변수 출력:
"User count: @(users.count)@" - LLDB 명령 자동 실행
- 셸 스크립트 실행
- 소리 재생(드문 코드 경로에 사용합니다)
“Automatically continue after evaluating actions"를 체크하면 브레이크포인트가 흐름을 방해하지 않는 로거가 됩니다.
LLDB: 실제로 사용할 콘솔 명령어#
브레이크포인트에서 멈췄을 때, 하단의 콘솔은 크래시 로그만 보기 위한 게 아닙니다. LLDB이고, 엄청나게 유용합니다.
기본#
(lldb) po user
▿ User
- id: "123"
- name: "John Doe"
- email: "john@example.com"po는 객체를 읽기 쉬운 형식으로 출력합니다. 제가 쓰는 것의 90%입니다.
더 자세한 정보가 필요하면 p를 시도해보세요:
(lldb) p user.name
(String) $R0 = "John Doe"모든 로컬 변수를 한 번에:
(lldb) frame variable런타임에 변경하기#
진짜 파워 무브입니다. 재컴파일 없이 변수를 수정할 수 있습니다:
(lldb) expr user.name = "Jane Doe"
(lldb) expr index = 0버그를 발견하고 리빌드 없이 수정을 테스트하고 싶으신가요? 변수를 변경하고 계속하면 됩니다. 엣지 케이스를 추적할 때 다양한 값을 테스트하는 데 써왔습니다.
메서드를 호출할 수도 있습니다:
(lldb) po self.refreshUI()
(lldb) expr navigationController?.popViewController(animated: true)네비게이션#
(lldb) bt # 콜 스택 표시
(lldb) frame select 3 # 다른 스택 프레임으로 이동
(lldb) continue # 실행 계속 (또는 단순히 'c')
(lldb) n # 스텝 오버 (다음 줄)
(lldb) s # 함수로 스텝 인워치포인트#
변수가 언제 변경되는지 알고 싶으신가요? 워치포인트를 설정하세요:
(lldb) watchpoint set variable user.isLoggedIn코드 어디에서든 그 값이 변경될 때마다 실행이 일시 정지됩니다. 예상치 못한 상태 변경을 추적하는 데 최고입니다.
Console.app: 숨겨진 디버깅 도구#
Xcode의 콘솔은 괜찮지만, 모든 걸 봐야 할 때—시스템 로그, 크래시 리포트, 전부—Console.app을 여세요.
OSLog를 사용하고 있다면(그래야 합니다), Console.app은 그 로그들을 검색 가능하고 필터링 가능하게 만듭니다:
import os.log
let logger = Logger(subsystem: "com.example.app", category: "networking")
logger.info("Starting API request")
logger.debug("Request URL: \(url.absoluteString)")
logger.error("Failed to decode: \(error.localizedDescription)")Console.app에서:
- 시뮬레이터 또는 연결된 기기 선택
- 서브시스템으로 필터링:
subsystem:com.example.app - 레벨로 필터링:
subsystem:com.example.app AND level:error
자주 쓰는 디버깅 세션을 위해 필터 술어를 저장할 수 있습니다. 네트워크 요청용, 데이터베이스 작업용, 오류와 경고만 표시하는 것이 있습니다.
print()보다 OSLog를 사용하는 이유#
OSLog는 print() 문을 여기저기 흩뿌리는 것보다 훨씬 낫습니다:
- 구조화되어 있습니다—레벨과 카테고리로 필터링 가능
- 빠릅니다—로깅이 최적화되어 앱을 느리게 하지 않음
- 프로덕션에서 민감한 데이터를 자동으로 편집
- 앱이 크래시한 후에도 로그가 지속됩니다
빠른 디버깅에는 print()를. 나중에 보고 싶은 것에는 OSLog를 사용하세요.
고급 Console.app 필터링#
Console.app은 강력한 술어 쿼리를 지원합니다. 정기적으로 쓰는 것들:
# 앱의 모든 오류와 결함
subsystem:com.example.app AND (level:error OR level:fault)
# 실패한 네트워크 요청
subsystem:com.example.app AND category:networking AND eventMessage CONTAINS "failed"
# 지난 5분간의 모든 것
subsystem:com.example.app AND timestamp >= now(-5m)로그를 내보내서 팀과 공유하거나 버그 리포트에 첨부할 수도 있습니다. Xcode 콘솔에서 복사 붙여넣기하는 것보다 훨씬 낫습니다.
뷰 디버깅#
UI가 망가졌는데 이유를 모를 때, 뷰 디버거는 생명의 은인입니다.
앱을 실행하고, 망가진 화면으로 이동한 다음, Xcode 디버그 바에서 “Debug View Hierarchy” 버튼을 클릭(또는 Cmd+Shift+D). 계층의 모든 뷰에 대한 3D 분해도가 나타납니다.
회전시키면 보입니다:
- 다른 뷰 뒤에 숨겨진 채 렌더링되는 뷰
- 화면 밖에 있는 뷰
- 크기가 0인 뷰
- 선택한 뷰의 완전한 제약 체인
탭 이벤트를 차단하는 보이지 않는 버튼, 0x0으로 렌더링되는 레이블, 실수로 10,000픽셀 너비가 된 이미지를 찾는 데 썼습니다.
Auto Layout 디버깅#
뷰 계층의 보라색 경고 아이콘은 제약 충돌을 의미합니다. 클릭하면 Xcode가 정확히 어떤 제약이 충돌하는지 보여줍니다.
프로 팁: 제약에 식별자를 부여하세요:
heightConstraint.identifier = "ProfileImageHeight"제약이 깨지면 메모리 주소 대신 "ProfileImageHeight"가 오류에 표시됩니다. 디버깅이 훨씬 쉬워집니다.
LLDB에서 제약 트리를 출력할 수도 있습니다:
(lldb) po view.hasAmbiguousLayout
(lldb) po view._autolayoutTrace()시간을 절약하는 시뮬레이터 기능#
느린 애니메이션#
Debug → Slow Animations는 모든 것을 1/10 속도로 느리게 합니다. 전환 중에 무슨 일이 일어나는지 보거나 애니메이션이 왜 이상하게 보이는지 이해하는 데 완벽합니다.
메모리 경고 시뮬레이션#
Debug → Simulate Memory Warning. 앱이 낮은 메모리를 어떻게 처리하는지 테스트합니다. 이 방법으로 많은 이미지 캐싱 버그를 찾았습니다—메모리 경고를 트리거할 때까지는 잘 작동하다가 갑자기 모든 이미지가 사라지는 것들.
Network Link Conditioner#
Xcode → Open Developer Tools → Network Link Conditioner
3G, LTE, 높은 패킷 손실 또는 완전한 오프라인 모드를 시뮬레이션합니다. API 요청이 WiFi에서는 잘 작동하지만 셀룰러에서 타임아웃되는 경험이 있다면, 이것이 이유입니다.
500ms 지연과 10% 패킷 손실의 “Bad Network” 프로필을 유지합니다. 빠른 사무실 WiFi에서는 절대 볼 수 없는 타임아웃 버그와 로딩 상태 문제를 잡습니다.
상태 표시줄 오버라이드#
시뮬레이터의 상태 표시줄을 우클릭하면 시간, 배터리 수준, 신호 강도, 통신사를 오버라이드할 수 있습니다. 일관된 스크린샷이나 다양한 배터리 수준에서 UI 확인에 유용합니다.
위치 시뮬레이션#
Debug → Location으로 책상을 떠나지 않고 다양한 위치를 시뮬레이션할 수 있습니다. 커스텀 위치, 도시 산책, 고속도로 주행—모두 가능. 위치 기반 기능 테스트나 특정 지역에서만 발생하는 문제 디버깅에 매우 유용합니다.
메모리 디버깅#
Debug Memory Graph#
Debug Memory Graph 버튼을 클릭(Cmd+Shift+M)하면 Xcode는 메모리의 모든 객체, 그 관계, 그리고 중요하게도 어떤 것이 누수되고 있는지 보여줍니다.
보라색 느낌표는 누수입니다. 클릭하면 retain cycle을 볼 수 있습니다.
가장 자주 보는 누수:
class ViewController: UIViewController {
var onComplete: (() -> Void)?
func setupHandler() {
onComplete = {
self.dismiss(animated: true) // ❌ self를 강하게 캡처
}
}
}weak self로 수정:
onComplete = { [weak self] in
self?.dismiss(animated: true) // ✓ retain cycle 없음
}delegate도 weak이어야 합니다:
weak var delegate: ManagerDelegate? // 단순한 'var'가 아님Instruments#
진지한 메모리 조사에는 Instruments를 사용하세요(Product → Profile, 또는 Cmd+I).
Allocations 인스트루먼트가 보여주는 것:
- 모든 객체 할당
- 시간에 따른 메모리 증가
- 가장 많은 메모리를 사용하는 클래스
- 할당 위치를 보여주는 스택 트레이스
보통 찾는 것:
- 시간에 따라 선형으로 증가하는 메모리(아마도 누수)
- 예상치 못한 큰 할당(1000개 이미지를 한 번에 모두 로드했나?)
- 할당 해제되어야 하지만 안 된 객체
액션 전후에 세대를 표시(작은 깃발 버튼)해서 해제되어야 할 것이 뭐가 남아있는지 확인하세요.
성능 프로파일링#
Time Profiler#
Product → Profile → Time Profiler. 앱에서 느린 작업을 하면서 기록한 다음 호출 트리를 봅니다.
“Self Weight"로 정렬해서 병목을 찾으세요. 함수가 40%의 self weight를 보이면, 거기를 최적화해야 합니다.
JSON 파싱 함수가 초당 1000번 호출되는 걸 발견한 적이 있습니다. 백그라운드 큐로 옮겼더니 UI 끊김이 멈췄습니다.
Main Thread Checker#
Xcode는 백그라운드 스레드에서의 UI 업데이트를 자동으로 포착합니다. 이게 보이면:
Main Thread Checker: UI API called on a background thread: -[UILabel setText:]이런 걸 했습니다:
URLSession.shared.dataTask(with: url) { data, response, error in
self.label.text = "Loaded" // ❌ 크래시!
}수정:
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
self.label.text = "Loaded" // ✓
}
}런타임 진단#
Edit Scheme → Run → Diagnostics. 수동으로는 절대 찾을 수 없는 버그를 잡는 체크박스가 잔뜩 있습니다.
Address Sanitizer#
메모리 손상 발견:
- Use-after-free 오류
- 버퍼 오버플로
- 메모리 누수
앱이 2-3배 느려지지만, 다른 방법으로는 추적 불가능한 버그를 잡습니다. 가끔만 발생하는 크래시를 추적할 때 켭니다.
Thread Sanitizer#
데이터 레이스와 스레딩 문제를 잡습니다. 이걸 활성화하고 앱을 실행한 다음 정상적으로 사용하세요. race condition이 있다면 Thread Sanitizer가 찾을 겁니다.
Address Sanitizer와 Thread Sanitizer를 동시에 실행할 수 없어서, 이상한 크래시를 디버깅할 때 둘을 번갈아 씁니다.
Zombie Objects#
할당 해제된 객체로 보낸 메시지를 잡습니다. 활성화하면 Xcode가 해제된 메모리에 접근할 때 정확히 알려줍니다:
*** -[MyViewController viewDidLoad]: message sent to deallocated instance 0x600001234000켜둔 채로 두지 마세요—객체가 할당 해제되지 않으므로 메모리 사용량이 늘어납니다. 하지만 특정 use-after-free 버그를 찾는 데는 훌륭합니다.
흔한 디버깅 시나리오#
시작 시 앱 크래시#
먼저 예외 브레이크포인트를 추가하세요. 보통 그걸로 잡힙니다.
안 되면 확인:
- Info.plist의 누락된 키
- 초기화 코드의 강제 언래핑된 옵셔널
- 의존성 주입 실패
UI가 업데이트되지 않음#
매번. 매번. 다음 중 하나입니다:
- 업데이트가 메인 스레드에 있지 않음
- outlet이 연결되지 않음
- 뷰가 실제로 보이지 않음 (LLDB에서
po view.window로 확인—nil이면 뷰가 계층에 없습니다)
테이블/컬렉션 뷰의 경우 reloadData() 호출을 잊었습니다.
메모리 경고로 앱이 크래시#
Instruments Allocations로 뭐가 메모리를 쓰는지 확인하세요. 보통:
- 캐시에서 해제되지 않는 이미지
- 할당 해제되지 않는 뷰 컨트롤러 (retain cycle)
- 영원히 증가하는 배열 또는 딕셔너리
시뮬레이터에서 메모리 경고를 시뮬레이션해서 정리 코드를 테스트하세요.
신비한 크래시#
모든 sanitizer를 활성화하세요. 간헐적이면 아마 스레딩—Thread Sanitizer로 실행. 시스템 프레임워크 안이면 아마 메모리 손상—Address Sanitizer로 실행.
시뮬레이터에서 OTA 업데이트 디버깅#
시뮬레이터는 over-the-air 업데이트 플로우 디버깅에 완벽합니다. 제가 하는 것:
네트워크 트래픽 감시. 앱 서브시스템으로 필터링된 Console.app으로 실시간으로 매니페스트 요청 및 응답을 확인합니다. 어떤 헤더가 전송되고 서버가 뭘 반환하는지 정확히 볼 수 있습니다.
나쁜 네트워크 시뮬레이션. Network Link Conditioner로 업데이트 플로우가 느린 연결이나 패킷 손실을 어떻게 처리하는지 테스트. 앱이 멈추나요? 로딩 인디케이터를 보여주나요? 우아하게 폴백하나요?
번들 로딩 확인. 업데이트 확인 로직 주변에 로그를 추가합니다. 업데이트 확인 시, 업데이트 가능 시, 다운로드 시작 시, 새 번들 로드 시에 로그를 남깁니다.
import os.log
let logger = Logger(subsystem: "com.example.app", category: "updates")
logger.info("Checking for updates...")
let update = try await Updates.checkForUpdateAsync()
logger.info("Update available: \(update.isAvailable)")매니페스트 엔드포인트 테스트. 앱이 보내는 것과 동일한 헤더로 매니페스트 엔드포인트를 수동으로 curl할 수 있습니다:
curl -H "expo-protocol-version: 1" \
-H "expo-platform: ios" \
-H "expo-runtime-version: 1.0.0" \
https://your-server.com/api/expo-updates/manifest/클라이언트 측을 디버깅하기 전에 서버가 올바른 데이터를 반환하는지 확인할 수 있습니다.
서드파티 도구#
Charles Proxy / Proxyman#
모든 네트워크 트래픽 확인. 요청과 응답 수정. 오류 조건 테스트. API 디버깅에 필수.
Proxyman은 Charles보다 UI가 낫고 macOS에서 더 네이티브하게 느껴집니다. 작년에 바꾼 이후로 돌아보지 않았습니다.
Reveal#
Xcode의 뷰 디버거와 비슷하지만 더 강력합니다. Xcode에 연결 없이도 물리 기기에서 작동. 테스터 기기의 레이아웃 문제 디버깅에 훌륭합니다.
실제 기기에서 디버깅#
무선 디버깅#
USB로 기기를 한 번 연결하고, Window → Devices and Simulators에서 “Connect via network"를 체크한 다음, 플러그를 뽑으세요. 같은 WiFi에 있는 한 기기가 Xcode에 남아 있습니다.
Device Console#
Window → Devices and Simulators → Open Console. 모든 걸 보여줍니다: 시스템 로그, 앱 로그, 크래시 리포트. Xcode 콘솔에서 보는 것보다 훨씬 상세합니다.
테스터가 “앱이 크래시했다"고 하면, 항상 기기를 연결해서 콘솔 로그를 가져오라고 합니다. 대부분 모든 걸 설명하는 시스템 레벨 오류가 있습니다.
실제로 디버깅할 때 하는 것#
- 예외 브레이크포인트 추가(없다면)
- 버그 재현
- 브레이크포인트 설정(깨질 것 같은 곳 근처)
- **LLDB에서
po**로 값 확인 - 변수 수정해서 재컴파일 없이 수정 테스트
- 뷰 계층 확인(UI 관련인 경우)
- Instruments로 프로파일(성능 관련인 경우)
- sanitizer 켜기(메모리나 스레딩 관련인 경우)
핵심은 증상에서 역으로 추적하는 것. 크래시? 예외 브레이크포인트. 느림? Time Profiler. 메모리? Instruments. 이상한 UI? 뷰 디버거.
더 일찍 알았으면 좋았을 팁#
assertion 사용. 로직 오류가 버그가 되기 전에 잡습니다:
assert(users.count > 0, "Users array should never be empty here")디버깅 전에 커밋. 이론을 테스트하기 위해 변경을 시작하면 되돌리고 싶어질 겁니다.
print 문 삭제. 또는 적어도 #if DEBUG로 감싸세요. 오래된 디버그 로그는 새 문제 디버깅을 어렵게 합니다.
LLDB 배우기. 앱을 20번 리빌드하는 것보다 빠릅니다.
시뮬레이터는 진짜가 아닙니다. 기기에서만 나타나는 버그는 항상 스레딩 또는 메모리 문제입니다. 또는 시뮬레이터가 실제 iPhone 칩이 아닌 Mac의 CPU를 사용하기 때문에 성능 관련입니다.
열어두는 도구#
- Xcode(당연히)
- 앱 서브시스템으로 필터링된 Console.app
- 네트워크 디버깅용 Proxyman
- 심각할 때 Instruments
디버깅은 모든 도구를 아는 게 아닙니다—특정 증상을 볼 때 어떤 도구를 집을지 아는 겁니다. 뷰 디버거는 메모리 누수에 도움이 안 되고, Instruments는 Auto Layout 충돌에 도움이 안 됩니다.
디버깅을 많이 할수록 패턴을 빨리 인식하게 됩니다. “아, 이건 확실히 retain cycle이다.” “이건 메인 스레드 위반처럼 보인다.” “이건 아마 제약 우선순위 문제다.”
그 직감을 쌓으면 디버깅이 훨씬 덜 짜증납니다.

