Skip to content

Latest commit

ย 

History

History
489 lines (361 loc) ยท 18.7 KB

ServerCommunication.md

File metadata and controls

489 lines (361 loc) ยท 18.7 KB

๊ธฐ๋ณธ ๊ณผ์ œ : ์„œ๋ฒ„ํ†ต์‹  URLSession, Alamofire, Moya ๋น„๊ตํ•˜๊ธฐ

iOS์—์„œ๋Š” ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๊ธฐ ์œ„ํ•ด ๊ธฐ๋ณธ์ ์œผ๋กœ Foundation์˜ URLSession์ด๋ผ๋Š” API๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค. URLSession์€ ๋กœ์šฐ๋ ˆ๋ฒจ์˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ณ , ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ ์‚ฌ์šฉ์ด ๋ณต์žกํ•˜๊ณ  ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์ด ์ข‹์ง€ ์•Š๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

๋”ฐ๋ผ์„œ URLSession์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ๋‹จ๊ณ„ ์ถ”์ƒํ™”์‹œํ‚จ ๋ฐฉ์‹์œผ๋กœ ํ•œ๋„คํŠธ์›Œํ‚น ์ž‘์—…์„ ๋‹จ์ˆœํ™”ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ Alamofire ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋ณดํŽธ์ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. ํ•˜์ง€๋งŒ Alamofire๋Š” ์œ ์ง€ ๋ณด์ˆ˜์™€ ์œ ๋‹› ํ…Œ์ŠคํŠธ๊ฐ€ ํž˜๋“ค๋‹ค๋Š” ๋‹จ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

์ด๋•Œ URLSession์„ ์ถ”์ƒํ™”ํ•œ Alamofire๋ฅผ ๋‹ค์‹œ ์ถ”์ƒํ™”ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ NetWork Layer๋ฅผ ํ…œํ”Œ๋ฆฟํ™” ํ•ด์„œ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’ํžˆ๊ณ , ํ…Œ์ŠคํŠธ๊ฐ€ ์šฉ์ดํ•˜๋ฉฐ ๊ฐœ๋ฐœ์ž๊ฐ€ request,response์—๋งŒ ์‹ ๊ฒฝ์“ฐ๋„๋ก ํ•ด์ค€ ๊ฒƒ์ด ๋ฐ”๋กœ Moya๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

iOS ์„œ๋ฒ„ํ†ต์‹ ์— ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” URLSession, Alamofire, Moya์— ๋Œ€ํ•ด ๊ฐ„๋‹จํžˆ ๋น„๊ตํ•ด๋ณด๋„๋ก ํ•˜์ž.

์ฐธ๊ณ ๐Ÿ’ก ์ถ”์ƒํ™”๋ž€?

: ๊ฐ์ฒด๋“ค์˜ ๊ณตํ†ต๋œ ๋ถ€๋ถ„๋งŒ ๋”ฐ๋กœ ๋ฝ‘์•„, ์žฌ์‚ฌ์šฉ์„ ํ•˜๊ธฐ ์‰ฝ๋„๋ก ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๋œปํ•œ๋‹ค. (์‰ฝ๊ฒŒ ๋งํ•ด ์ผ๋ฐ˜ํ™”์‹œํ‚ค๋Š” ๊ฒƒ)

์ถ”์ƒํ™”์˜ ์žฅ์  : ๋ชจ๋ธ๋ง, ์ฝ”๋“œ์˜ ์žฌ์‚ฌ์šฉ์„ฑ, ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ, ์ผ๊ด€๋œ ๋ฐฉํ–ฅ์„ฑ ๋“ฑ์ด ๊ณตํ†ต์ ์œผ๋กœ ์–ธ๊ธ‰์ด ๋œ๋‹ค.

๋ชจ๋ธ๋ง์„ ํ†ตํ•ด ์ฝ”๋“œ๋ฅผ ์ถ”์ƒํ™”ํ•˜๋ฉด ์ฝ”๋“œ์˜ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๊ณ , ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์—ฌ ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ํ›จ์”ฌ ๋” ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด ์ฃผ๊ธฐ๋„ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ถ”์ƒํ™”๋œ ์ฝ”๋“œ๋Š” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ผ๊ด€์„ฑ์„ ๊ฐ€์ง€๊ฒŒ ๋œ๋‹ค.

์ž ๊น! HTTP, REST, ๊ทธ๋ฆฌ๊ณ  JSON์— ๋Œ€ํ•ด ๊ฐ„๋ฝํžˆ ์•Œ๊ณ  ๋„˜์–ด๊ฐ€์ž

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๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์•ฑ์— ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์„ ์‰ฝ๊ฒŒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.


1. URLSession

  • 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"),
...
]


2. Alamofire

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)
            }
        }


3. Moya

Moya๋Š” URLSession์„ ์ถ”์ƒํ™”ํ•œ Alamofire๋ฅผ, ๋‹ค์‹œ ์ถ”์ƒํ™”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ Network Layer๋ฅผ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ด๊ณ  ๊ฐœ๋ฐœ์ž๊ฐ€ request, response์—๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

Moya ๊ณต์‹๋ฌธ์„œ์— ์žˆ๋Š” ๋Œ€๋กœ ์ˆœ์„œ๋ฅผ ์ •๋ฆฌํ•ด๋ณด๋„๋ก ํ•˜์ž.

1. Service.swift ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

๊ฐ 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"]
        }
    }
}

3. ์ œ๋„ค๋ฆญ ํƒ€์ž…์œผ๋กœ Service๋ฅผ ๊ฐ€์ง„ MoyaProvider ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

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๋ž€?

: ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ๋„คํŠธ์›Œํฌ ์ž‘์—…์„ ์ฝ˜์†”์— ๊ธฐ๋กํ•ด์ฃผ๋Š” ๊ฒƒ์„ ๋œปํ•œ๋‹ค.

4. ์„œ๋ฒ„ ํ†ต์‹ ์„ ์ง„ํ–‰ํ•˜๋Š” ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ , ์„œ๋ฒ„ ํ†ต์‹ ์„ ํ•œ๋‹ค.

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 ๊นƒํ—ˆ๋ธŒ ๊ณต์‹๋ฌธ์„œ

Moya ๊นƒํ—ˆ๋ธŒ ๊ณต์‹๋ฌธ์„œ

[iOS - swift] 1. Alamofire ์‚ฌ์šฉ ๋ฐฉ๋ฒ• - Network Layer ๊ตฌํ˜„ (Moya ํ”„๋ ˆ์ž„์›Œํฌ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•)

[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๊ฐ€ ๋ฌด์—‡์ธ์ง€, ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

iOS URLSession ์ดํ•ดํ•˜๊ธฐ

[iOS - swift] URLSession ๋„คํŠธ์›Œํฌ ํ†ต์‹  ๊ธฐ๋ณธ (URLSessionConfiguration, URLSession, URLComponents, URLSessionTask)

[iOS] Moya๊ฐ€ ๋ชจ์•ผ? - Moya๋กœ Get ํ†ต์‹ ํ•˜๊ธฐ

[iOS] Moya , Alamofire , URLSession ๋น„๊ต