서버에서 실시간 데이터를 받아와야 한다
// ❌ 콜백 방식 — 중첩이 깊어진다 (Callback Hell)
func fetchUser(completion: @escaping (User?) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else { completion(nil); return }
completion(try? JSONDecoder().decode(User.self, from: data))
}.resume()
}
// ✅ async/await 방식 — 위에서 아래로 읽힌다
func fetchUser() async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// await: "여기서 기다려, 결과 오면 계속 진행해"
// async: "이 함수는 비동기 문맥에서만 호출 가능"
// throws: "에러가 발생할 수 있다, try로 처리해야 한다"
// 호출 방법
Task {
do {
let user = try await fetchUser()
print(user.name)
} catch {
print("에러: \(error)")
}
}
await를 만나면 현재 스레드가 멈추는가, 아니면 다른 작업을 처리하러 가는가?async throws 함수를 호출할 때 try await 순서가 왜 이 순서인가?.task { }와 onAppear { Task { } }의 차이는?onAppear + Task의 개선된 버전// API 응답 모델
struct Post: Codable, Identifiable {
let id: Int
let title: String
let body: String
let userId: Int
}
// 네트워크 함수
func fetchPosts() async throws -> [Post] {
guard let url = URL(
string: "https://jsonplaceholder.typicode.com/posts"
) else {
throw URLError(.badURL)
}
// data(from:)은 iOS 15+에서 async/await 지원
let (data, response) = try await URLSession.shared
.data(from: url)
// HTTP 상태 코드 확인
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let decoder = JSONDecoder()
// API 키가 snake_case면 자동 변환
// decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode([Post].self, from: data)
}
// POST 요청 (데이터 전송)
func createPost(_ post: Post) async throws -> Post {
guard let url = URL(
string: "https://jsonplaceholder.typicode.com/posts"
) else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json",
forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(post)
let (data, _) = try await URLSession.shared
.data(for: request)
return try JSONDecoder().decode(Post.self, from: data)
}
URLSession.shared와 직접 만든 URLSession의 차이는? 언제 직접 만들어야 하는가?.convertFromSnakeCase: user_name → userName 자동 변환. CodingKeys 없이도 매핑 가능// enum으로 상태 표현 enum LoadingState{ case idle case loading case success(T) case failure(Error) } struct PostListView: View { @State private var state: LoadingState<[Post]> = .idle var body: some View { Group { switch state { case .idle: Color.clear.onAppear { load() } case .loading: ProgressView("불러오는 중...") case .success(let posts): List(posts) { post in VStack(alignment: .leading, spacing: 4) { Text(post.title) .font(.headline) Text(post.body) .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) } } case .failure(let error): VStack(spacing: 16) { Image(systemName: "wifi.slash") .font(.system(size: 40)) .foregroundStyle(.secondary) Text(error.localizedDescription) .multilineTextAlignment(.center) Button("다시 시도") { load() } .buttonStyle(.borderedProminent) } .padding() } } .navigationTitle("포스트") } func load() { state = .loading Task { do { let posts = try await fetchPosts() state = .success(posts) } catch { state = .failure(error) } } } }
LoadingState를 enum으로 표현하면 @State var isLoading: Bool + @State var error: Error?로 나누는 것보다 어떤 점이 좋은가?.task { }로 데이터를 불러올 때 View가 사라지면 무슨 일이 일어나는가?async let)async let a = fetchA(); async let b = fetchB(); let (ra, rb) = try await (a, b) — 두 요청을 동시에 실행ProgressView("텍스트") 스피너, ProgressView(value: 0.5) 프로그레스 바// ViewModel에 @MainActor를 붙이면
// 모든 프로퍼티 변경이 메인 스레드에서 실행됨
@MainActor
class NewsViewModel: ObservableObject {
@Published var articles: [Article] = []
@Published var isLoading = false
@Published var errorMessage: String?
func fetchArticles() async {
isLoading = true
errorMessage = nil
do {
// 네트워크는 백그라운드에서 실행
articles = try await NewsService.fetch()
} catch {
// @MainActor이므로 UI 업데이트가 안전
errorMessage = error.localizedDescription
}
isLoading = false
}
}
struct NewsView: View {
@StateObject private var vm = NewsViewModel()
var body: some View {
NavigationStack {
Group {
if vm.isLoading {
ProgressView()
} else {
List(vm.articles) { article in
Text(article.title)
}
}
}
.navigationTitle("뉴스")
}
.task { await vm.fetchArticles() }
}
}
@MainActor 없이 백그라운드 스레드에서 @Published를 바꾸면 어떤 경고/오류가 발생하는가?@StateObject와 @ObservedObject 중 ViewModel을 생성하는 View에서는 어느 것을 써야 하는가?// 커스텀 에러 타입
enum NetworkError: LocalizedError {
case invalidURL
case badResponse(statusCode: Int)
case decodingFailed
case noInternet
var errorDescription: String? {
switch self {
case .invalidURL:
return "잘못된 URL입니다."
case .badResponse(let code):
return "서버 오류 (\(code)). 잠시 후 다시 시도해주세요."
case .decodingFailed:
return "데이터를 읽을 수 없습니다."
case .noInternet:
return "인터넷 연결을 확인해주세요."
}
}
}
// 에러 타입 활용
func fetchData(from urlString: String) async throws -> Data {
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
let (data, response) = try await URLSession.shared
.data(from: url)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.badResponse(statusCode: 0)
}
guard (200...299).contains(http.statusCode) else {
throw NetworkError.badResponse(
statusCode: http.statusCode)
}
return data
}
// 에러 분기 처리
do {
let data = try await fetchData(from: "https://...")
let posts = try JSONDecoder().decode([Post].self, from: data)
} catch let error as NetworkError {
print(error.errorDescription ?? "")
} catch is DecodingError {
print("JSON 파싱 실패")
} catch {
print("알 수 없는 오류: \(error)")
}
LocalizedError 프로토콜을 채택하면 어떤 이점이 있는가?catch let error as NetworkError와 catch의 실행 순서는? 더 구체적인 catch를 먼저 써야 하는가?errorDescription을 구현하면 error.localizedDescription이 사용자 친화적 메시지를 반환catch error는 모든 에러를 처리하는 폴백// 재사용 가능한 제네릭 네트워크 함수
struct APIClient {
static let shared = APIClient()
private let baseURL = "https://jsonplaceholder.typicode.com"
private let decoder: JSONDecoder = {
let d = JSONDecoder()
d.keyDecodingStrategy = .convertFromSnakeCase
return d
}()
func fetch(_ path: String) async throws -> T {
guard let url = URL(string: baseURL + path) else {
throw NetworkError.invalidURL
}
let (data, response) = try await URLSession.shared
.data(from: url)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw NetworkError.badResponse(statusCode: 0)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decodingFailed
}
}
}
// 사용 — 타입 추론으로 깔끔하게
let posts: [Post] = try await APIClient.shared.fetch("/posts")
let user: User = try await APIClient.shared.fetch("/users/1")
fetch<T: Decodable>에서 T는 호출 시점에 어떻게 결정되는가?APIClient.shared의 장점과 테스트에서의 단점은?