반응형
iOS 개발자가 많이 하는 실수 - 네트워크 에러 및 HTTP 상태 코드(4xx/5xx)를 무시하는 실수
OS 네트워크 코드를 처음 작성할 때 가장 많이 하는 실수 중 하나는:
- error만 대충 체크하거나
- 아예 체크도 안 하고
- HTTP 상태 코드(4xx, 5xx)를 완전히 무시하는 것
입니다.
1. 문제 패턴: 에러/상태코드 무시하고 data만 사용하는 코드
대표적인 코드:
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
let user = try? JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.updateUI(with: user)
}
}.resume()
여기서 흔한 문제점:
- error를 전혀 보지 않는다.
- response가 HTTP 200인지, 404인지, 500인지 확인하지 않는다.
- 서버가 에러 HTML, 에러 JSON을 내려줘도 그대로 decode를 시도한다.
결과:
- 서버측 에러가 나도 앱은 “정상 응답”인 것처럼 행동
- 사용자에게 “알 수 없는 오류” UI만 표시하거나
- 조용히 실패해서 아무것도 안 보이는 상태가 된다.
2. URLSession 콜백에서 확인해야 할 3가지
URLSession의 기본 콜백 시그니처:
(data: Data?, response: URLResponse?, error: Error?)
실무에서는 이 세 가지를 모두 확인해야 한다.
- transport 레벨 에러 (error)
- 네트워크 연결 실패, 타임아웃, DNS 문제 등
- HTTP 레벨 상태 코드 ((response as? HTTPURLResponse)?.statusCode)
- 200, 204, 400, 401, 404, 500 등
- 응답 데이터 (data)
- JSON, 빈 바디, 에러 응답 등
3. 올바른 기본 처리 흐름 예시
URLSession.shared.dataTask(with: url) { data, response, error in
// 1) Transport 에러 체크 (네트워크 자체의 문제)
if let error = error {
print("Network error:", error)
// 사용자에게 네트워크 오류 안내
return
}
// 2) HTTP 상태 코드 체크
guard let httpResponse = response as? HTTPURLResponse else {
print("Invalid response")
return
}
guard (200..<300).contains(httpResponse.statusCode) else {
print("Server error status:", httpResponse.statusCode)
// 상태코드별 에러 처리 (401, 403, 500 등)
return
}
// 3) 실제 데이터 체크
guard let data = data else {
print("Empty data")
return
}
// 4) 데이터 파싱/디코드
do {
let user = try JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.updateUI(with: user)
}
} catch {
print("Decode error:", error)
}
}.resume()
이 패턴만 잘 지켜도 네트워크 관련 버그의 상당 부분을 줄일 수 있습니다.
4. Transport 에러 vs HTTP 에러의 차이
4-1. Transport 에러 (error)
- DNS 실패
- 연결 타임아웃
- 인터넷 연결 끊김
- TLS/SSL 문제 등
이 경우는 네트워크 자체가 실패한 것이므로
사용자에게 “네트워크 오류”를 보여주는 것이 적절합니다.
4-2. HTTP 에러 (4xx, 5xx 등)
- 4xx: 클라이언트 요청이 잘못되었거나 권한 문제
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found 등
- 5xx: 서버 내부 문제
- 500 Internal Server Error
- 502 Bad Gateway
- 503 Service Unavailable 등
이 경우:
- 네트워크는 정상적으로 연결되었지만,
- 서버가 요청을 처리하지 못한 것이므로
- 로그인 문제, 권한 문제, 서버 점검 등의 UI를 보여주는 것이 맞습니다.
5. 상태 코드별 처리 전략 (예시)
실무에서는 보통 다음처럼 상태코드를 그룹으로 나누어 처리합니다.
switch httpResponse.statusCode {
case 200..<300:
// 성공
case 400:
// 잘못된 요청 (클라이언트 버그 가능성)
case 401:
// 인증 필요 → 로그인 화면으로 유도
case 403:
// 권한 없음 → 접근 불가 안내
case 404:
// 리소스 없음 → "삭제되었거나 존재하지 않음" 안내
case 500..<600:
// 서버 측 문제 → "잠시 후 다시 시도해 주세요" 안내
default:
// 정의되지 않은 기타 상태
break
}
이 로직을 서비스 공통 레이어에 모아두면,
각 화면에서는 성공 케이스만 신경 쓰면 됩니다.
6. Result 타입 또는 에러 타입 설계
실무에서는 다음과 같이 네트워크 에러를 하나의 enum으로 정의하고
모든 네트워크 로직이 이 타입을 사용하도록 통일하는 것이 좋습니다.
enum NetworkError: Error {
case transportError(Error)
case serverError(statusCode: Int, data: Data?)
case decodingError(Error)
case invalidResponse
case emptyData
}
사용 예:
func requestUser(completion: @escaping (Result<User, NetworkError>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(.transportError(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(.invalidResponse))
return
}
guard (200..<300).contains(httpResponse.statusCode) else {
completion(.failure(.serverError(statusCode: httpResponse.statusCode, data: data)))
return
}
guard let data = data else {
completion(.failure(.emptyData))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(.decodingError(error)))
}
}.resume()
}
이렇게 해두면, UI 쪽에서는:
requestUser { result in
switch result {
case .success(let user):
// UI 업데이트
case .failure(let error):
// 에러 타입에 따라 다른 메시지 표시
}
}
처럼 명확하게 처리할 수 있습니다.
7. async/await 환경에서의 에러 처리
Swift Concurrency를 사용할 경우:
func fetchUser() async throws -> User {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw NetworkError.serverError(statusCode: httpResponse.statusCode, data: data)
}
do {
return try JSONDecoder().decode(User.self, from: data)
} catch {
throw NetworkError.decodingError(error)
}
}
사용:
Task {
do {
let user = try await fetchUser()
await MainActor.run {
self.updateUI(with: user)
}
} catch {
// NetworkError로 캐스팅해서 타입별 처리 가능
print("Error:", error)
}
}
8. 실무용 체크리스트
네트워크 코드를 작성할 때 다음을 항상 확인합니다.
- error를 체크하는가?
- response를 HTTPURLResponse로 캐스팅하여 statusCode를 확인하는가?
- 200..<300 범위만 성공으로 취급하는가?
- 4xx / 5xx에 대해 적절한 메시지/로직을 구현했는가?
- 디코딩 실패(DecodingError)를 구분해 로그를 남기는가?
- 공통 네트워크 레이어에서 에러 처리 로직을 재사용 가능한 형태로 빼두었는가?
9. 요약
- error == nil이라고 해서 요청이 성공한 것은 아니다.
- HTTP 상태 코드가 2xx인지 반드시 확인해야 한다.
- 4xx, 5xx를 무시하면 서버 장애, 인증 문제, 권한 문제를 모두 “그냥 nil”로 처리하게 된다.
- 실무에서는:
- Transport 에러, HTTP 에러, 디코딩 에러를 구분하고
- 각각 다른 UI/동작으로 대응하는 것이 중요하다.
핵심 문장:
네트워크 코드에서는 data만 보지 말고, error와 statusCode를 함께 보라.
반응형

