์ƒ์„ธ ์ปจํ…์ธ 

๋ณธ๋ฌธ ์ œ๋ชฉ

Netflix ํด๋ก  ์ฝ”๋”ฉ URLSession ๋ฆฌํŒฉํ† ๋งํ•ด๋ณด๊ธฐ/ Endpint

๐ŸŽ iOS/Network

by AHN.Jihyeon 2024. 8. 10. 17:58

๋ณธ๋ฌธ

URL

 

URL์€ ์›๊ฒฉ ์„œ๋ฒ„์˜ ํ•ญ๋ชฉ์ด๋‚˜ ๋กœ์ปฌ ํŒŒ์ผ์˜ ๊ฒฝ๋กœ์™€ ๊ฐ™์€ ํŠน์ • ๋ฆฌ์†Œ์Šค์˜ ์œ„์น˜๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.

์ฃผ๋กœ Network ์š”์ฒญ, FileManager ์—์„œ ์‚ฌ์šฉ.

let urlString = URL(string: "https://api.themoviedb.org/3/movie/top_rated?api_key=123456780eb5a8248e85f2742a7868a8")
url.absoluteURL    // https://api.themoviedb.org/3/movie/top_rated?api_key=123456780eb5a8248e85f2742a7868a8
url.scheme         // "https"
url.host           // "api.themoviedb.org"
url.path           // "/3/movie/top_rated"
url.query          // "api_key=123456780eb5a8248e85f2742a7868a8"

 

 

URLComponents

URL๊ณผ ๊ฐ™์ด ์ฝ๊ธฐ ๋ถ€๋ถ„.

let urlComponents = URLComponents(string: "https://api.themoviedb.org/3/movie/top_rated?api_key=123456780eb5a8248e85f2742a7868a8")
urlComponents!.scheme     // scheme: "https"
urlComponents!.host       // host: "api.themoviedb.org"
urlComponents!.path       // path: "/3/movie/top_rated"
urlComponents!.query      // "api_key=426472920eb5a8248e85f2742a7868a8"
urlComponents!.queryItems // ["api_key=426472920eb5a8248e85f2742a7868a8"]

 

 

์ด๋Ÿฐ์‹์œผ๋กœ ์“ฐ๊ธฐ๊ฐ€ ๊ฐ€๋Šฅํ•ด URL๋ณด๋‹ค ์œ ๋™์ ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

let urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "api.themoviedb.org"
urlComponents.path = "/3/movie/top_rated"
urlComponents.query = "api_key=426472920eb5a8248e85f2742a7868a8"
//์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ ์•„์ดํ…œ์ด ์žˆ์„ ์‹œ queryItems์œผ๋กœ ๋ฐฐ์—ด๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์Œ.
urlComponents.queryItems = [URLQueryItem(name: "api_key", value: "426472920eb5a8248e85f2742a7868a8")]

urlComponents.url // "https://api.themoviedb.org/3/movie/top_rated?api_key=123456780eb5a8248e85f2742a7868a8"

 

 

URLComponents๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ URL์„ ์ƒ์„ฑ ์‹œ, ๋‹ค์–‘ํ•œ URL ๊ตฌ์„ฑ ์š”์†Œ

 

1. scheme: URL์˜ ์Šคํ‚ด์„ ์„ค์ •. ์˜ˆ๋ฅผ ๋“ค์–ด, "https", "http", "ftp" ๋“ฑ

2. host: URL์˜ ํ˜ธ์ŠคํŠธ ์ด๋ฆ„์„ ์„ค์ •

3. port: URL์˜ ํฌํŠธ๋ฅผ ์„ค์ •. ์ƒ๋žตํ•  ๊ฒฝ์šฐ ๊ธฐ๋ณธ ํฌํŠธ๋ฅผ ์‚ฌ์šฉ

4. path: URL์˜ ๊ฒฝ๋กœ๋ฅผ ์„ค์ •

5. query: URL์˜ ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด์„ ์ง์ ‘ ์„ค์ •. ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด์€ key=value ์Œ์œผ๋กœ ๊ตฌ์„ฑ

components.query = "key1=value1&key2=value2"

6. queryItems: URL์˜ ์ฟผ๋ฆฌ ํ•ญ๋ชฉ์„ ์„ค์ •. URLQueryItem ๊ฐ์ฒด์˜ ๋ฐฐ์—ด๋กœ ์„ค์ •๋˜๋ฉฐ, ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ž๋™์œผ๋กœ ์ธ์ฝ”๋”ฉ

๋ฐฐ์—ด์˜ ํ˜•ํƒœ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด ์—ฌ๋Ÿฌ ์•„์ดํ…œ ์ถ”๊ฐ€ํ•ด์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ.

components.queryItems = [URLQueryItem(name: "key1", value: "value1"),
                          URLQueryItem(name: "key2", value: "value2")]

7. fragment: URL์˜ ํ”„๋ž˜๊ทธ๋จผํŠธ๋ฅผ ์„ค์ •. ์ด๋Š” URL์˜ ํ•ด์‹œ ๋ถ€๋ถ„์— ํ•ด๋‹นํ•˜๋ฉฐ, ํŽ˜์ด์ง€ ๋‚ด ์œ„์น˜๋ฅผ ์ง€์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ

components.fragment = "section1"

 

 


๊ธฐ์กด์— ์‚ฌ์šฉํ–ˆ๋˜ URLSession 

//๋ฐฉ๋ฒ•1
func fetch<T: Decodable>(urlString: String, type: T) -> T {
    let session = URLSession(configuration: .default)
    let url = URL(string: urlString)
    let request = URLRequest(url: url)
    
    session.dataTask(with: request) { data, response, error in 
        // do something
    }
}    

//๋ฐฉ๋ฒ•2
func fetch<T: Decodable>(url: URL) -> Single<T> {
    return Single.create { observer in
        let session = URLSession(configuration: .default)
        session.dataTask(with: URLRequest(url: url)) { data, response, error in
        
        }.resume()
    }
}


//๋ฐฉ๋ฒ•2 ์‚ฌ์šฉ
let urlString = "https://api.themoviedb.org/3/movie/popular?api_key=\(apiKey)")
guard let url = urlString else { return }
NetworkManager.shared.fetch(url: url)
... ์ƒ๋žต

 

์—ฌ๊ธฐ์„œ URL์—์„œ ์›์‹œ๊ฐ’์„ ์ง์ ‘ ์ž…๋ ฅํ•˜๊ณ  ๊ด€๋ฆฌํ•  ๋•Œ ์˜คํƒ€ ๋“ฑ๊ณผ ๊ฐ™์ด ์‹ค์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. 

๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ•ด์•ผํ•˜๋Š” ์ธ์Šคํ„ด์Šค๋Š” url, urlString์„ ์ƒ์„ฑ, ๊ด€๋ฆฌํ•˜๋Š” ์ฑ…์ž„์ด ์ถ”๊ฐ€์ ์œผ๋กœ ์ƒ๊ธด๋‹ค. 

-> ์—”๋“œํฌ์ธํŠธ๊ฐ€ ๋งŽ์•„์ง„๋‹ค๋ฉด, ์ธ์Šคํ„ด์Šค๋กœ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•ด๋ณผ ๊ฒƒ.

 

 

 


Endpoint(์—”๋“œํฌ์ธํŠธ)

URLSession์—์„œ endPoint๋Š” ํด๋ผ์ด์–ธํŠธ(์‚ฌ์šฉ ์ค‘์ธ ์•ฑ)๊ฐ€ ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ ๊ทธ ์š”์ฒญ์ด ์–ด๋””๋กœ ๊ฐ€์•ผ ํ•˜๋Š”์ง€ ์•Œ๋ ค์ฃผ๋Š” ์„œ๋ฒ„์˜ ํŠน์ • ์œ„์น˜๋‚˜ ๊ฒฝ๋กœ

 

์˜ˆ๋ฅผ ๋“ค์–ด์–ด๋ณด์ž๋ฉด,

์—ฌ๊ธฐ์„œ https://api.example.com/v1/users๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ์ „์ฒด URL์ด ๋œ๋‹ค.

 

 

์ฟผ๋ฆฌ(Query)

์š”์ฒญํ•˜๋Š” ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด ์ถ”๊ฐ€์ ์ธ ์กฐ๊ฑด์ด๋‚˜ ํ•„ํ„ฐ๋ฅผ ์ง€์ •ํ•˜๊ธฐ ์œ„ํ•ด URL์— ๋ถ™๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋งํ•œ๋‹ค.

์ฃผ๋กœ ? ๋’ค์— ์œ„์น˜ํ•˜๋ฉฐ, ์—ฌ๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด &๋กœ ๊ตฌ๋ถ„๋œ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด, ํŠน์ • ์‚ฌ์šฉ์ž ID์— ํ•ด๋‹นํ•˜๋Š” ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ๋‹ค๋ฉด,

  • Base URL: https://api.example.com
  • ์—”๋“œํฌ์ธํŠธ: /v1/users
  • ์ฟผ๋ฆฌ: ?id=123&name=John

์ „์ฒด URL์€ https://api.example.com/v1/users?id=123&name=John์ด ๋œ๋‹ค.

์—ฌ๊ธฐ์„œ id=123๊ณผ name=John์€ ์„œ๋ฒ„์— ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์š”์ฒญํ•˜๋Š” ์กฐ๊ฑด์ด๋‹ค.

 

 

์ฐจ์ด์  ์š”์•ฝ

์—”๋“œํฌ์ธํŠธ(Endpoint): ์„œ๋ฒ„์˜ ํŠน์ • ๋ฆฌ์†Œ์Šค๋‚˜ ๊ธฐ๋Šฅ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ๊ฒฝ๋กœ.

์˜ˆ์‹œ: /v1/users

 

์ฟผ๋ฆฌ(Query): ์š”์ฒญ์— ์ถ”๊ฐ€ ์กฐ๊ฑด์„ ์ง€์ •ํ•˜์—ฌ ํ•„ํ„ฐ๋งํ•˜๊ฑฐ๋‚˜ ๊ฒ€์ƒ‰ํ•  ๋•Œ ์‚ฌ์šฉ.

์˜ˆ์‹œ: ?id=123&name=John


endpoint๋Š” URL์ด ์•„๋‹Œ, URL์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” Endpoint ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋‹ค.

์‹ค์ œ URL ๊ฐ์ฒด๋Š” creatEndpoint() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ URLRequest๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ •์—์„œ ๋งŒ๋“ค์–ด์ง„๋‹ค.

๋”ฐ๋ผ์„œ, endpoint ์ž์ฒด๋Š” URL์ด ์•„๋‹ˆ๋ฉฐ, endpoint์—์„œ creatEndpoint() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ

URLRequest ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉด, ์ด URLRequest ๊ฐ์ฒด๊ฐ€ URL์„ ํฌํ•จํ•˜๊ฒŒ ๋œ๋‹ค.

import Foundation

enum HTTPMethodType: String {
    case post = "POST"
    case get = "GET"
    case put = "PUT"
    case delete = "DELETE"
}

final class Endpoint{
    let baseURL: String
    let method: HTTPMethodType
    let headerParpmeters: [String: String]
    let path: String
    let queryParameters: [String: Any]
    
    
    init(baseURL: String = "https://api.themoviedb.org",
         method: HTTPMethodType = .get,
         headerParpmeters: [String : String] = [:],
         path: String = "",
         queryParameters: [String : Any] = [:]
    ) {
        self.baseURL = baseURL
        self.method = method
        self.headerParpmeters = headerParpmeters
        self.path = path
        self.queryParameters = queryParameters
    }
    
    
    //URL ์ƒ์„ฑ ๋ฉ”์„œ๋“œ
    func creatURL() -> URL? {
        var urlComponents = URLComponents(string: baseURL.appending(path)) 
        
        var queryItems = [URLQueryItem]() // name, value ์†์„ฑ ์žˆ์Œ
        
        queryParameters.forEach {
            queryItems.append(URLQueryItem(name: $0.key, value: "\($0.value)"))
        }
        
        urlComponents?.queryItems = queryItems
        
        return urlComponents?.url
        
    }
    
    
    //endpoint ์ƒ์„ฑ ๋ฉ”์„œ๋“œ 
    func creatEndpoint() throws -> URLRequest { 
        guard let url = creatURL() else {throw NetworkError.invalidUrl}
        
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        
        var allHeaders: [String: String] = [:]
        headerParpmeters.forEach {
            allHeaders.updateValue($1, forKey: $0)
        }
        return request
    }
    
}
import Foundation
import RxSwift

enum NetworkError: Error {
    case invalidUrl
    case dataFetchFail
    case decodingFail
}

class NetworkManager {
    static let shared = NetworkManager()
    private init(){}

	//url์—์„œ endpoint๋กœ url ๋ฐ›์•„์˜ค๊ธฐ
    func fetch<T: Decodable>(endpoint: Endpoint) -> Single<T> {
        
        do {
        	//URLRequest ์ƒ์„ฑ
            let request = try endpoint.creatEndpoint() //URLRequest ํƒ€์ž… ๋ฆฌํ„ด 
        
            return Single.create { observer in   
            	//URLSession์„ ํ†ตํ•ด ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
                let session = URLSession(configuration: .default)
                session.dataTask(with: request) { data, response, error in
                    
                    // error ๊ฐ€ ์žˆ๋‹ค๋ฉด Single ์— fail ๋ฐฉ์ถœ.
                    if let error = error {
                        observer(.failure(error))
                        return
                    }
                    
                    // data ๊ฐ€ ์—†๊ฑฐ๋‚˜ http ํ†ต์‹  ์—๋Ÿฌ ์ผ ๋•Œ dataFetchFail ๋ฐฉ์ถœ.
                    guard let data = data,
                          let response = response as? HTTPURLResponse,
                          (200..<300).contains(response.statusCode) else {
                        observer(.failure(NetworkError.dataFetchFail))
                        return
                    }
                    
                    do {
                        // data ๋ฅผ ๋ฐ›๊ณ  json ๋””์ฝ”๋”ฉ ๊ณผ์ •๊นŒ์ง€ ์„ฑ๊ณตํ•œ๋‹ค๋ฉด ๊ฒฐ๊ณผ๋ฅผ success ์™€ ํ•จ๊ป˜ ๋ฐฉ์ถœ.
                        let decodedData = try JSONDecoder().decode(T.self, from: data)
                        observer(.success(decodedData))
                    } catch {
                        // ๋””์ฝ”๋”ฉ ์‹คํŒจํ–ˆ๋‹ค๋ฉด decodingFail ๋ฐฉ์ถœ.
                        observer(.failure(NetworkError.decodingFail))
                    }
                }.resume()  
                return Disposables.create()
            }
        } catch let error {
            return Single.create { observer in
                observer(.failure(error))
                return Disposables.create()
            }
        }
    }
    
    
    // ๊ธฐ์กด ์ฝ”๋“œ
	func fetch<T: Decodable>(url: URL) -> Single<T> {

        return Single.create { observer in
            let session = URLSession(configuration: .default)
            session.dataTask(with: url) { data, response, error in
                
                // error ๊ฐ€ ์žˆ๋‹ค๋ฉด Single ์— fail ๋ฐฉ์ถœ.
                if let error = error {
                    observer(.failure(error))
                    return
                }
                
                // data ๊ฐ€ ์—†๊ฑฐ๋‚˜ http ํ†ต์‹  ์—๋Ÿฌ ์ผ ๋•Œ dataFetchFail ๋ฐฉ์ถœ.
                guard let data = data,
                      let response = response as? HTTPURLResponse,
                      (200..<300).contains(response.statusCode) else {
                    observer(.failure(NetworkError.dataFetchFail))
                    return
                }
                
                do {
                    // data ๋ฅผ ๋ฐ›๊ณ  json ๋””์ฝ”๋”ฉ ๊ณผ์ •๊นŒ์ง€ ์„ฑ๊ณตํ•œ๋‹ค๋ฉด ๊ฒฐ๊ณผ๋ฅผ success ์™€ ํ•จ๊ป˜ ๋ฐฉ์ถœ.
                    let decodedData = try JSONDecoder().decode(T.self, from: data)
                    observer(.success(decodedData))
                } catch {
                    // ๋””์ฝ”๋”ฉ ์‹คํŒจํ–ˆ๋‹ค๋ฉด decodingFail ๋ฐฉ์ถœ.
                    observer(.failure(NetworkError.decodingFail))
                }
            }.resume()
            
            return Disposables.create()
        }
    }
}
//MainViewModel

	func fetchPopularMovie(){
        //๊ธฐ์กด url ์ƒ์„ฑํ–ˆ๋˜ ๋ฐฉ๋ฒ• 
//        guard let url = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=\(apiKey)") else {
//            popularMovieSubject.onError(NetworkError.invalidUrl)
//            return
//        }
        
        //endpoint ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ํ›„ ๋ณ€์ˆ˜ ํ• ๋‹น
        let endpoint = Endpoint(
            path: "/3/movie/popular",
            queryParameters: ["api_key": apiKey]
        )

        
        // ์ด ๋„คํŠธ์›Œํฌ fetch ์˜ ๊ฒฐ๊ณผ๊ฐ€ Single ํƒ€์ž…์œผ๋กœ ์˜ต์ €๋ฒ„๋ธ”์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ๋‹ค
        NetworkManager.shared.fetch(endpoint: endpoint)
            .subscribe(onSuccess: { [weak self] (movieResponse: MovieResponse) in  //๊ตฌ๋… ์‹œ์ž‘.
                //fetch์—์„œ ๊ฐ’์ด ๋ฐฉ์ถœ ๋๋‹ค๋ฉดonSuccess ๋˜๋Š” onFailure ๋กœ์ง ์‹คํ–‰
                //๋””์ฝ”๋”ฉ ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด observer(.success(decodedData))๋กœ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐฉ์ถœ ๋˜๊ณ  movieResponse๋กœ ๊ฐ’์ด ๋“ค์–ด์™€ BehaviorSubject์— ๊ฐ’์„ ๋„ฃ์–ด์ค€๋‹ค.
                self?.popularMovieSubject.onNext(movieResponse.results)
            }, onFailure: { [weak self] error in
                self?.popularMovieSubject.onError(error)
            }).disposed(by: disposeBag)
    }

๊ด€๋ จ๊ธ€ ๋”๋ณด๊ธฐ