Swift에서 URLSession을 이용해서 서버와 소통
- URLSessionConfiguration
- .default
- 디스크를 이용한 정보 저장을 하는 configuration
- 그냥 브라우저 띄울때
- .ephemeral
- default랑 비슷한데, 몇가지 정보들을 저장하지 않음(쿠키, 인증서 등)
- 브라우저 시크릿 모드라고 생각하면 쉬움
- .background
- 네트워크를 통해 파일 다운로드 받을때
- 앱이 백그라운드에서 돌때도, 다운로드 시켜줄 수 있음(게임 업데이트 할떄 등?)
- .default
- URLSession
- configuration을 보고 세션을 생성
- URLSessionTask
- dataTask
- uploadTask
- downloadTask
import Foundation
// Configuration -> URLsession -> URLSessionTask
// Configuration
let configuration = URLSessionConfiguration.default
// URLSession
let session = URLSession(configuration: configuration)
let url = URL(string: "https://api.github.com/users/alstjr7437")!
// URLSessionTask
let task = session.dataTask(with: url) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
print("--> response\(response)")
return
}
guard let data = data else { return }
let result = String(data: data, encoding: .utf8)
print(result)
}
task.resume()
위와 같이 하여서 Github api를 사용하면
아래와 같이 JSON 형태로 데이터를 받아옴
Optional("{\"login\":\"alstjr7437\",\"id\":94051599,\"node_id\":\"U_kgDOBZsdDw\", ~~~})
실패시(404에러 등등 여러가지 Http 응답 코드 반환)
--> responseOptional(<NSHTTPURLResponse: 0x6000002223c0> { URL: https://api.github.com/users/alstjr7437sad } { Status Code: 404, Headers {
- swift 객체를 외부 데이터 형태(주로 JSON)로 인코딩, 디코딩 가능하게 해주는 타입
- 네트워크 응답형태로 JSON이 거의 표준으로 사용
- Codable을 이용하면, JSON과 swift 객체간 전환이 매우 쉬움
아까 URLSession에서 실습한 코드의 결과는 JSON 형태였는데 그걸 Codable을 이용해서 swift 객체에 맞게 변경
우선 가지고 올 구조체 선언
// JSON -> App model
struct GithubProfile: Codable {
let login: String
let avatarUrl: String
let htmlUrl: String
let followers: Int
let following: Int
// json key값을 아래 CodingKeys로
// 언더바가 있으면 아래와 같이 넣어주기
enum CodingKeys: String, CodingKey {
case login
case avatarUrl = "avatar_url"
case htmlUrl = "html_url"
case followers
case following
}
}
avatar_url은 api에서 가져온 스네이크 케이스 하지만 swift에서는 카멜 케이스를 쓰기 때문에 위와 같이 CodingKeys를 수정해줬음.
이제 URLSession에서 실습한 것과 같이 진행하지만 do부분을 이용해 JSONDecoder해서
profile이라는 구조체에 넣어주기
그리고 성공시 profile 출력
실패시 error 출력
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration)
let url = URL(string: "https://api.github.com/users/alstjr7437")!
let task = session.dataTask(with: url) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
print("--> response\(response)")
return
}
guard let data = data else { return }
// data -> GithubProfiledo
do {
let decoder = JSONDecoder()
let profile = try decoder.decode(GithubProfile.self, from: data)
print("profile: \(profile)")
} catch let error as NSError{
print("[error] : \(error)")
}
}
task.resume()
성공시 JSON이 decode되어서
profile로 만들어지게 됨
아래와 같이 login등등이 들어간 부분 확인 가능
profile: GithubProfile(login: "alstjr7437", avatarUrl: "https://avatars.githubusercontent.com/u/94051599?v=4", htmlUrl: "https://github.com/alstjr7437", followers: 29, following: 27)
Codable은 위와 같고 URL 부분을 class와 함수로 만들어 줌
@escaping 관련 공부 블로그
final class NetworkService {
let session: URLSession
init(configuration: URLSessionConfiguration){
session = URLSession(configuration: configuration)
}
// Result는 enum 타입 성공 케이스, 실패 케이스 있음
func fetchProfile(userName: String, completion: @escaping (Result<GithubProfile, Error>)-> Void){
let url = URL(string: "https://api.github.com/users/\(userName)")!
let task = session.dataTask(with: url) { data, response, error in
// 1. error 출력 부분
if let error = error{
completion(.failure(NetworkError.transportError(error)))
return
}
// 2. 200대가 아니면 에러 출력 부분
if let httpResponse = response as? HTTPURLResponse, !(200..<300).contains(httpResponse.statusCode) {
completion(.failure(NetworkError.responseError(statusCode: httpResponse.statusCode)))
return
}
// 3. data 받았는데 데이터 없으면
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
// data -> GithubProfile
do {
let decoder = JSONDecoder()
let profile = try decoder.decode(GithubProfile.self, from: data)
completion(.success(profile))
} catch let error as NSError{
// 4. Decoding 중 에러
completion(.failure(NetworkError.decodingError(error)))
}
}
task.resume()
}
}
// NetworkService 실행 부분
let networkService = NetworkService(configuration: .default)
networkService.fetchProfile(userName: "alstjr7437") { result in
switch result {
case .success(let profile):
print("Profile: \(profile)")
case .failure(let error):
print("Error: \(error)")
}
}
- 정상 결과
Profile: GithubProfile(login: "alstjr7437", avatarUrl: "https://avatars.githubusercontent.com/u/94051599?v=4", htmlUrl: "https://github.com/alstjr7437", followers: 29, following: 27)
- 2번에서 404로 들어와서 에러 출력
userName: "alstjr7437dsad"
위와 같이 바꿔서
> Error: responseError(statusCode: 404)
- 4번에서 Decoding을 하려는데 일치하는 부분이 없을때 에러
case avatarUrl = "avatar_urldsd"
위와 같이 바꿔서
> Error: decodingError(Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "avatar_urldsd", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"avatar_urldsd\", intValue: nil) (\"avatar_urldsd\").", underlyingError: nil)))
이렇게 위와 같이 함수로 바꿔서 훨씬 에러코드도 깔끔해지고 보기 좋아짐
그리고 userName도 함수로 바로 넘겨줘서 해당 부분을 쓰는걸 확인 가능
- URLSessionDataTask에 대해서 publisher 제공
- 따라서, 비동기 응답에 대한 작업이 더 수월함
아까와 똑같이 Codable은 똑같음
하지만 Error부분을 enum을 통해 선언
enum NetworkError: Error {
case invalidRequest
case responseError(statusCode: Int)
}
그리고 NetworkService를 만듬
final class NetworkService {
let session: URLSession
init(configuration: URLSessionConfiguration){
session = URLSession(configuration: configuration)
}
// Result는 enum 타입 성공 케이스, 실패 케이스 있음
func fetchProfile(userName: String) -> AnyPublisher<GithubProfile, Error> {
let url = URL(string: "https://api.github.com/users/\(userName)")!
return session
// dataTask 퍼블리셔 만들기
.dataTaskPublisher(for: url)
// 서버에서 받은 Data, response 확인
.tryMap { result -> Data in
guard let httpResponse = result.response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) else {
let response = result.response as? HTTPURLResponse
let statusCode = response?.statusCode ?? -1
throw NetworkError.responseError(statusCode: statusCode)
}
return result.data
}
// 받은 Data 디코딩하기
// Operator 중에 decode해주는게 있음
.decode(type: GithubProfile.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
위에 만들어진 Class를 이용해서 Subscription 만들기
let subscription = networkService
.fetchProfile(userName: "alstjr7437")
.receive(on: RunLoop.main)
.print()
.sink { error in
print("error: \(error)")
} receiveValue: { profile in
print("profile: \(profile)")
}
receive subscription: (ReceiveOn)
request unlimited
receive value: (GithubProfile(login: "alstjr7437", avatarUrl: "https://avatars.githubusercontent.com/u/94051599?v=4", htmlUrl: "https://github.com/alstjr7437", followers: 29, following: 27))
profile: GithubProfile(login: "alstjr7437", avatarUrl: "https://avatars.githubusercontent.com/u/94051599?v=4", htmlUrl: "https://github.com/alstjr7437", followers: 29, following: 27)
receive finished
receive subscription: (ReceiveOn)
request unlimited
receive error: (responseError(statusCode: 404))
error: failure(__lldb_expr_158.NetworkError.responseError(statusCode: 404))
receive subscription: (ReceiveOn)
request unlimited
receive error: (keyNotFound(CodingKeys(stringValue: "avatar_urlds", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"avatar_urlds\", intValue: nil) (\"avatar_urlds\").", underlyingError: nil)))
error: failure(Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "avatar_urlds", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"avatar_urlds\", intValue: nil) (\"avatar_urlds\").", underlyingError: nil)))
retry 넣어서 실패해도 3번 시도 해봐라
let subscription = networkService
.fetchProfile(userName: "alstjr7437f")
.receive(on: RunLoop.main)
.print()
.retry(3)
.sink { error in
print("completion: \(error)")
} receiveValue: { profile in
print("profile: \(profile)")
}
receive subscription: (ReceiveOn)
request unlimited
receive error: (responseError(statusCode: 404))
receive subscription: (ReceiveOn)
request unlimited
receive error: (responseError(statusCode: 404))
receive subscription: (ReceiveOn)
request unlimited
receive error: (responseError(statusCode: 404))
receive subscription: (ReceiveOn)
request unlimited
receive error: (responseError(statusCode: 404))
completion: failure(__lldb_expr_174.NetworkError.responseError(statusCode: 404))