iOS์์๋ ์๋ฒ์ ํต์ ํ๊ธฐ ์ํด ๊ธฐ๋ณธ์ ์ผ๋ก Foundation์ URLSession
์ด๋ผ๋ API๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค. URLSession
์ ๋ก์ฐ๋ ๋ฒจ์ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๊ณ , ๋ค๋ฅธ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ ํ์๊ฐ ์๋ค๋ ์ฅ์ ์ด ์์ง๋ง ์ฌ์ฉ์ด ๋ณต์กํ๊ณ ์ฝ๋์ ๊ฐ๋
์ฑ์ด ์ข์ง ์๋ค๋ ๋จ์ ์ด ์๋ค.
๋ฐ๋ผ์ URLSession
์ ๊ธฐ๋ฐ์ผ๋ก ํ ๋จ๊ณ ์ถ์ํ์ํจ ๋ฐฉ์์ผ๋ก ํ๋คํธ์ํน ์์
์ ๋จ์ํํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Alamofire
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ณดํธ์ ์ผ๋ก ์ฌ์ฉํ๋ค. ํ์ง๋ง Alamofire
๋ ์ ์ง ๋ณด์์ ์ ๋ ํ
์คํธ๊ฐ ํ๋ค๋ค๋ ๋จ์ ์ ๊ฐ์ง๊ณ ์๋ค.
์ด๋ URLSession
์ ์ถ์ํํ Alamofire
๋ฅผ ๋ค์ ์ถ์ํํ ํ๋ ์์ํฌ๋ก NetWork Layer๋ฅผ ํ
ํ๋ฆฟํ ํด์ ์ฌ์ฌ์ฉ์ฑ์ ๋ํ๊ณ , ํ
์คํธ๊ฐ ์ฉ์ดํ๋ฉฐ ๊ฐ๋ฐ์๊ฐ request,response์๋ง ์ ๊ฒฝ์ฐ๋๋ก ํด์ค ๊ฒ์ด ๋ฐ๋ก Moya
๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
iOS ์๋ฒํต์ ์ ์์ฃผ ์ฌ์ฉ๋๋ URLSession
, Alamofire
, Moya
์ ๋ํด ๊ฐ๋จํ ๋น๊ตํด๋ณด๋๋ก ํ์.
์ฐธ๊ณ ๐ก ์ถ์ํ๋?
: ๊ฐ์ฒด๋ค์ ๊ณตํต๋ ๋ถ๋ถ๋ง ๋ฐ๋ก ๋ฝ์, ์ฌ์ฌ์ฉ์ ํ๊ธฐ ์ฝ๋๋ก ๊ตฌํํ๋ ๊ฒ์ ๋ปํ๋ค. (์ฝ๊ฒ ๋งํด ์ผ๋ฐํ์ํค๋ ๊ฒ)
์ถ์ํ์ ์ฅ์ : ๋ชจ๋ธ๋ง, ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ, ์ฝ๋์ ๊ฐ๋ ์ฑ, ์ผ๊ด๋ ๋ฐฉํฅ์ฑ ๋ฑ์ด ๊ณตํต์ ์ผ๋ก ์ธ๊ธ์ด ๋๋ค.
๋ชจ๋ธ๋ง์ ํตํด ์ฝ๋๋ฅผ ์ถ์ํํ๋ฉด ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ์ ๋์ผ ์ ์๊ณ , ์ฝ๋์ ๊ฐ๋ ์ฑ์ ๋์ฌ ์ฝ๋๋ฅผ ์ดํดํ๊ธฐ ํจ์ฌ ๋ ์ฝ๊ฒ ๋ง๋ค์ด ์ฃผ๊ธฐ๋ ํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ถ์ํ๋ ์ฝ๋๋ ์์ฐ์ค๋ฝ๊ฒ ์ผ๊ด์ฑ์ ๊ฐ์ง๊ฒ ๋๋ค.
HTTP๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ ๋ ์ฌ์ฉํ๋ Application Protocol์ด๋ค. HTTP๋ ์๋์ ๊ฐ์ด ๋ค์ํ request method๋ฅผ ์ ์ํ์ฌ ๋ฐ๋์งํ ๋์๋ค์ ๊ฐ๋ฆฌํฌ ์ ์๊ฒ ํ๋ค.
- GET: ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋๋ค. (์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ์๋ ์๋ค.)
- HEAD: GET๊ณผ ๋น์ทํ์ง๋ง, ์ง์ง ๋ฐ์ดํฐ๊ฐ ์๋ header๋ง ์ ๋ฌํ๋ค.
- POST: ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์ ์ ์กํ๋ค. (ex. form์ ์ฑ์ฐ๊ฑฐ๋ submit ๋ฒํผ์ ๋๋ฅผ ๋ ๋ฑ)
- PUT: ๋ฐ์ดํฐ๋ฅผ ํน์ ํ ์ฅ์์ ์ ์กํ๋ค. (ex. user profile ์ ๋ฐ์ดํธ ๋ฑ)
- DELETE: ํน์ ์ฅ์์ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ค.
JSON์ JavaScript Object Notation์ ์ฝ์๋ก, ์์คํ ๊ฐ ๋ฐ์ดํฐ ์ ๋ฌ์ ์์ด ์ง๊ด์ ์ด๊ณ ์ฌ๋์ด ์ฝ์ ์ ์๋ ๋ฉ์ปค๋์ฆ์ ์ ๊ณตํ๋ค. JSON์ string, boolean, array, object/dictionary, number, null๊ณผ ๊ฐ์ด ํ์ ๋ ์์ ๋ฐ์ดํฐ ํ์ ๋ง ๊ฐ์ง ์ ์๋ค.
Swift4 ์ ์๋ JSON์์ data object๋ก, ๋ ๊ทธ ๋ฐ๋๋ก ๋ณํํ๊ธฐ ์ํด JSONSerialization ํด๋์ค๋ฅผ ์ฌ์ฉํด์ผ ํ๋๋ฐ, ์์ฆ์ Codable ํ๋กํ ์ฝ์ ์ฑํํด JSON๊ณผ data model ์ฌ์ด ์๋ํ ๋ณํ์ ์ด์ฉํ๋ค.
REST๋ REpresentational State Transfer์ ์ฝ์๋ก, ์ง์์ ์ธ ์น API๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๊ท์น์ ์งํฉ์ด๋ค. REST๋ request ์ฌ์ด์ ์ํ๋ฅผ ์ง์ํ์ง ์๊ฑฐ๋, cacheable request๋ฅผ ๋ง๋ค๊ณ , ๋์ผํ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๋ค. ์ด๋ฅผ ํตํด request๊ฐ ๋ฐ์ดํฐ์ ์ํ๋ฅผ ์ถ์ ํ์ง ์๊ณ ๋ API๋ฅผ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์ฑ์ ํตํฉํ๋ ๊ฒ์ ์ฝ๊ฒํ ์ ์๋๋ก ํ๋ค.
- URLSession์ HTTP/HTTPS๋ฅผ ํตํด ์ฝํ ์ธ ๋ฐ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๊ธฐ ์ํด API๋ฅผ ์ ๊ณตํ๋ ํด๋์ค ๋ฐ ํด๋์ค ๋ชจ์์ด๋ค.
class URLSession : NSObject
An object that coordinates a group of related, network data transfer tasks.
URLSession
: HTTP ์์ฒญ์ ๋ณด๋ด๊ณ ๋ฐ๋ ํต์ฌ ๊ฐ์ฒด์ด๋ค. ์ ๊ณต๋๋ URLSessionConfiguration
์ ํตํด ๋ค์ ์ธ ๊ฐ์ง ์ ํ์ URL์ ์์ฑํ๋ค.
.default
: ๊ธฐ๋ณธ ๋คํธ์ํฌ ํต์.ephemeral
: ์ฟ ํค๋ ์บ์๋ฅผ ์ ์ฅํ์ง ์๊ฒ ํ ๋ ์ฌ์ฉ (private ๋ชจ๋์ ๋น์ทํ๋ค).background
: ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์ ์์ ๋ ์ฌ์ฉ (์ปจํ ์ธ ๋ค์ด๋ก๋ ํน์ ์ ๋ก๋ ๋ฑ)
URLSession
์ฌ๋ฌ๊ฐ๋ก URLSessionTask
๋ฅผ ๋ง๋ค ์ ์๋ค. ์ด URLSessionTask
๋ก ์ค์ ํต์ ์ ํ๊ฒ ๋๋ค. URLSessionTask
๋ ์ธ ๊ฐ์ง ์ ํ์ผ๋ก ๋ถ๋ฅํ ์ ์๋ค.
URLSessionDataTask
: ๊ฐ๋จํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ๋ ์ฌ์ฉ (๋ฐฑ๊ทธ๋ผ์ด๋์์ ์งํ์ ์ ๋จ)URLSessionUploadTask
: ๋ฐ์ดํฐ๋ฅผ ์ ๋ก๋ํ ๋ ์ฌ์ฉURLSessionDownloadTask
: ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋๋ฅด ํ ๋ ์ฌ์ฉ
URLSession Delegate
์ ํตํด์ ๋คํธ์ํฌ ์ค๊ฐ๊ณผ์ ์ ํ์ธํ ์ ์๋ค. (ํ์๋ ์๋)
URLSession ์ค์ต
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
var urlComponents = URLComponents(string: "https://itunes.apple.com/search?media=music&entity=song&term=IU")!
let requestURL = urlComponents.url!
์์ ์ฝ๋๋ฅผ ํตํด URLConfiguration์ ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ์ด๋ฅผ ํตํด URLSession์ ์์ฑํ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์์ requestURL์๋ ์๋์ ๊ฐ์ด 50๊ฑด์ IU๋์ ๊ณก์ ๋ํ ์ ๋ณด๊ฐ ๋์์๋ค.
์ฌ๊ธฐ์ ์ํ๋ ์ ๋ณด๋ง์ ๋ฝ์๋ด๊ธฐ ์ํด Codable ํ๋กํ ์ฝ์ ์ฑํํ ๊ตฌ์กฐ์ฒด๋ฅผ ์์ฑํ๋ค.
struct Response: Codable {
let resultCount: Int
let tracks: [Track]
enum CodingKeys: String, CodingKey {
case resultCount
case tracks = "results"
}
}
struct Track: Codable {
let title: String
let artistName: String
enum CodingKeys: String, CodingKey {
case title = "trackName"
case artistName
}
๊ทธ๋ฆฌ๊ณ DataTask๋ฅผ ์์ฑํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.
// data task ์์ฑ
let dataTask = session.dataTask(with: requestURL) { (data, response, error) in
guard error == nil else {
return
}
// HTTP ์๋ต ์ฌ๋ถ ํ์ธ
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else {
return
}
// HTTP ์๋ต ์ฑ๊ณต ๋ฒ์
let successRange = 200..<300
guard successRange.contains(statusCode) else {
return
}
// ๋คํธ์ํฌ๋ฅผ ํตํด ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ resultData์ ์ ์ฅ
guard let resultData = data else { return }
// ๋ฐ์ดํฐ ํ์ฑ ๋ฐ ๊ฒฐ๊ณผ ์ถ๋ ฅ
do {
let decoder = JSONDecoder()
let response = try decoder.decode(Response.self, from: resultData)
let tracks = response.tracks
print("--> tracks: \(tracks)")
} catch let error {
print("---> error: \(error.localizedDescription)")
}
}
dataTask.resume()
์์ ์ฝ๋๋ฅผ ์คํํ๋ฉด ์๋์ ๊ฐ์ด ๊ณก ์ ๋ชฉ, ๊ฐ์๋ง 50๊ฑด์ด ์ถ๋ ฅ๋๋ค.
--> tracks: [__lldb_expr_31.Track(title: "Love Poem", artistName: "IU"),
...
]
Alamore๋ ๋น๋๊ธฐ๋ก ์ํํ๋ Swift ๊ธฐ๋ฐ์ HTTP ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
Alamofire๋ URLSession ๊ธฐ๋ฐ์ด๋ฉฐ,URLSession ๋ฐ URLSessionTask ๊ฐ์ ํด๋์ค๋ฅผ ์ฌ์ฉํ๊ธฐ ์ฝ๊ฒ ๊ตฌํ๋์ด ์๋ค.
- Alamofire๋ CocoaPods๋ฅผ ์ฌ์ฉํด ์ฝ๊ฒ ์ค์น ๊ฐ๋ฅํ๋ค.
pod 'Alamofire', '~> 5.2' // 5.2 version
cf. ์ค์นํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํ ๋ : import Alamofire
์ ๊ณต๋๋ ๋ํ ๊ธฐ๋ฅ์ผ๋ก๋ ์๋์ ๊ฐ๋ค.
AF.upload
: ๋ฉํฐํํธ, ์คํธ๋ฆผ, ํ์ผ๋ฉ์๋๋ฅผ ํตํด ํ์ผ์ ์ ๋ก๋ํ๋ค.AF.download
: ํ์ผ์ ๋ค์ด๋ก๋ํ๊ฑฐ๋ ์ด๋ฏธ ์งํ ์ค์ธ ๋ค์ด๋ก๋๋ฅผ ์ฌ๊ฐํ๋ค.AF.request
: ํ์ผ ์ ์ก๊ณผ ๋ฌด๊ดํ ๋ค๋ฅธ HTTP๋ฅผ ์์ฒญํ๋ค.
์ธ์คํ๊ทธ๋จ ํด๋ก ๊ณผ์ ํ์๊ฐ์ ๋ถ๋ถ ์์
let dataRequest = AF.request(url,
method: .post,
parameters: body,
encoding: JSONEncoding.default,
headers: header)
dataRequest.responseData { dataResponse in
switch dataResponse.result {
case .success:
guard let statusCode = dataResponse.response?.statusCode else { return }
guard let value = dataResponse.value else { return }
let networkResult = NetworkHelper.parseJSON(by: statusCode, data: value, type: SignUpResponse.self)
completion(networkResult)
case .failure(let err):
print(err)
completion(.networkFail)
}
}
Moya๋ URLSession์ ์ถ์ํํ Alamofire๋ฅผ, ๋ค์ ์ถ์ํํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก Network Layer๋ฅผ ํ ํ๋ฆฟ์ผ๋ก ๋ง๋ค์ด ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๊ณ ๊ฐ๋ฐ์๊ฐ request, response์๋ง ์ง์คํ ์ ์๋๋ก ์ค๊ณํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
Moya ๊ณต์๋ฌธ์์ ์๋ ๋๋ก ์์๋ฅผ ์ ๋ฆฌํด๋ณด๋๋ก ํ์.
๊ฐ case๋ ๊ฐ๋ณ์ ์ธ ๋คํธ์ํฌ๋ฅผ ๋ด๋นํ๊ฒ ๋๋ค. ํ๋ผ๋ฏธํฐ๋ ๊ฐ๋ณ API ๋ฌธ์๋ฅผ ๋ณด๊ณ ํ์ํ ๊ฒฝ์ฐ์ ๋ฐ๋ผ ํน์ ๋ก์ง์ ๋ฐ๋ผ์ ๋ง๋ ๋ค.
enum CardService {
case cardDetailFetch(cardID: String)
case cardCreation(request: CardCreationRequest, image: UIImage)
case cardListEdit(request: CardListEditRequest)
case cardDelete(cardID: String)
}
2. extension์ ํตํด TargetTypeํ๋กํ ์ฝ์ ์ถ๊ฐ๋ก ์ค์ํ๋๋ก ํ๊ณ , ํ์ํ ์์ฑ์ Service.swift ์ ์ถ๊ฐ๋ก ๊ตฌํํ๋ค.
TargetType ํ๋กํ ์ฝ์ ์ฑํํ๋ ์ด์ ๋ ์๋์ ๊ฐ์ด ๋ค์ํ ๋คํธ์ํน ์์ฑ์ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ธ๋ฐ, ์๋์ ๊ฐ์ ๋คํธ์ํน ์์ฑ์ ๊ฐ์ง๋ค.
- baseURL : ์๋ฒ์ base URL
- path : ์๋ฒ์ base URL ๋ค์ ์ถ๊ฐ๋ Path
- method : HTTP Method (GET, POST, PUT, DELETE ๋ฑ...)
- task : request์ ์ฌ์ฉ๋๋ ํ๋ผ๋ฏธํฐ ์ค์
- sampleData : ํ ์คํธ์ฉ Mock Data (ํ ์คํธ๋ฅผ ์ํ ๋ชฉ์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ ๋ ์ฌ์ฉ)
- validationType : ํ์ฉํ response์ ํ์
- headers : HTTP headers
extension CardService: TargetType {
var baseURL: URL { return URL(string: Const.URL.baseURL)! }
var path: String {
switch self {
case .cardDetailFetch(let cardID):
return "/card/\\(cardID)"
case .cardCreation:
return "/card"
case .cardListEdit:
return "/cards"
case .cardDelete(let cardID):
return "/card/\\(cardID)"
}
}
var method: Moya.Method {
switch self {
case .cardDetailFetch:
return .get
case .cardCreation:
return .post
case .cardListEdit:
return .put
case .cardDelete:
return .delete
}
}
var sampleData: Data {
return Data()
}
var task: Task {
switch self {
case .cardDetailFetch, .cardDelete:
return .requestPlain
case .cardCreation(let request, let image):
var multiPartData: [Moya.MultipartFormData] = []
let userIDData = request.userID.data(using: .utf8) ?? Data()
multiPartData.append(MultipartFormData(provider: .data(userIDData), name: "card.userId"))
let defaultImageData = Int(request.frontCard.defaultImage).description.data(using: .utf8) ?? Data()
multiPartData.append(MultipartFormData(provider: .data(defaultImageData), name: "card.defaultImage"))
"card.thirdTMI"))
return .uploadMultipart(multiPartData)
case .cardListFetch(let userID, let isList, let offset):
return .requestParameters(parameters: ["userId": userID,
"list": isList ?? false,
"offset": offset ?? ""
], encoding: URLEncoding.queryString)
case .cardListEdit(let requestModel):
return .requestJSONEncodable(requestModel)
}
}
var headers: [String: String]? {
switch self {
case .cardDetailFetch, .cardDelete:
return .none
case .cardCreation:
return ["Content-Type": "multipart/form-data"]
case .cardListEdit:
return ["Content-Type": "application/json"]
}
}
}
var cardProvider = MoyaProvider<CardService>(plugins: [NetworkLoggerPlugin()])
provider.request(.createUser(firstName: "James", lastName: "Potter")) { result in
// do something with the result (read on for more details)
}
์ฐธ๊ณ ๐ก NetworkLoggerPlugin๋?
: ๋ฐ์ํ๋ ๋ชจ๋ ๋คํธ์ํฌ ์์ ์ ์ฝ์์ ๊ธฐ๋กํด์ฃผ๋ ๊ฒ์ ๋ปํ๋ค.
import Foundation
import Moya
public class CardAPI {
static let shared = CardAPI()
var cardProvider = MoyaProvider<CardService>(plugins: [MoyaLoggerPlugin()])
public init() { }
func cardDetailFetch(cardID: String, completion: @escaping (NetworkResult<Any>) -> Void) {
cardProvider.request(.cardDetailFetch(cardID: cardID)) { (result) in
switch result {
case .success(let response):
let statusCode = response.statusCode
let data = response.data
let networkResult = self.judgeCardDetailFetchStatus(by: statusCode, data)
completion(networkResult)
case .failure(let err):
print(err)
}
}
}
func cardCreation(request: CardCreationRequest, image: UIImage, completion: @escaping (NetworkResult<Any>) -> Void) {
cardProvider.request(.cardCreation(request: request, image: image)) { (result) in
switch result {
case .success(let response):
let statusCode = response.statusCode
let data = response.data
let networkResult = self.judgeCardCreationStatus(by: statusCode, data)
completion(networkResult)
case .failure(let err):
print(err)
completion(.networkFail)
}
}
}
func cardListEdit(request: CardListEditRequest, completion: @escaping (NetworkResult<Any>) -> Void) {
cardProvider.request(.cardListEdit(request: request)) { (result) in
switch result {
case .success(let response):
let statusCode = response.statusCode
let data = response.data
let networkResult = self.judgeStatus(by: statusCode, data)
completion(networkResult)
case .failure(let err):
print(err)
}
}
}
func cardDelete(cardID: String, completion: @escaping (NetworkResult<Any>) -> Void) {
cardProvider.request(.cardDelete(cardID: cardID)) { (result) in
switch result {
case .success(let response):
let statusCode = response.statusCode
let data = response.data
let networkResult = self.judgeStatus(by: statusCode, data)
completion(networkResult)
case .failure(let err):
print(err)
}
}
}
private func judgeCardDetailFetchStatus(by statusCode: Int, _ data: Data) -> NetworkResult<Any> {
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(GenericResponse<Card>.self, from: data)
else {
return .pathErr
}
switch statusCode {
case 200:
return .success(decodedData.data ?? "None-Data")
case 400..<500:
return .requestErr(decodedData.msg)
case 500:
return .serverErr
default:
return .networkFail
}
}
private func judgeCardCreationStatus(by statusCode: Int, _ data: Data) -> NetworkResult<Any> {
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(GenericResponse<Card>.self, from: data)
else {
return .pathErr
}
switch statusCode {
case 201:
return .success(decodedData.data ?? "None-Data")
case 400..<500:
return .requestErr(decodedData.msg)
case 500:
return .serverErr
default:
return .networkFail
}
}
private func judgeStatus(by statusCode: Int, _ data: Data) -> NetworkResult<Any> {
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(GenericResponse<String>.self, from: data)
else { return .pathErr }
switch statusCode {
case 200:
return .success(decodedData.msg)
case 400..<500:
return .requestErr(decodedData.msg)
case 500:
return .serverErr
default:
return .networkFail
}
}
}
URLSession | Apple Developer Documentation
Alamofire ๊นํ๋ธ ๊ณต์๋ฌธ์
[Swift] Alamofire๋ฅผ Moya์ฒ๋ผ ์ฌ์ฉํด๋ณด์! By Router Pattern (1ํธ - Foundation Setting)
[Swift] Alamofire๋ฅผ Moya์ฒ๋ผ ์ฌ์ฉํด๋ณด์! By Router Pattern (2ํธ - Services, Routers ๊ตฌํ)
Alamofire 5 Tutorial for iOS: Getting Started
Fetching Website Data into Memory
Swift, URLSession๊ฐ ๋ฌด์์ธ์ง, ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์์๋ด ๋๋ค.