10 ByteCoin

프로젝트로 배운 것들

UIPickerView, JSON Parsing, Protocol, delegate,extension

UIPickerView

이번 프로젝트를 진행하면서 처음 배운 기능이다. Picker란 위 GIF에서도 볼 수 있듯이 하단부의 선택 부분을 의미한다. 그리고 iOS 에서는 UIPickerView로 Picker를 컨트롤 할 수 있었다.

class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        currencyPicker.dataSource = self // 중요
        currencyPicker.delegate = self   // 중요
        coinManager.delegate = self
    }
    
    // 구성요소 수 세팅
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        // 이건 아마도 카테고리 같다.
        // 우린 그냥 각 화폐단위를 보여줄꺼니까 카테고리는 한개
        return 1
    }
    
    // 얘는 반환을 Int 로 하고
    // Picker에 보여지는 개수를 출력하는 듯 하다.
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        // 아마도 코인 개수를 의미하는 듯 하다.
        return coinManager.currencyArray.count
    }
    
    // 얘는 반환을 String으로 한다.
    // Picker에 보여지는 화폐단위를 return한다.
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        // 아래 코드는 아마도, 해당 열의 화폐단위를 세팅하는 듯 하다.
        return coinManager.currencyArray[row]
    }
    
    // 선택했을때 해당 인덱스가 반환되는 pickerView
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let selectCurrencyArray = coinManager.currencyArray[row]
        // 선택한 화폐단위의 가치를 가져오는 메서드
        coinManager.getCoinPrice(for: selectCurrencyArray)
    }
}
    

UIPickerViewDataSource, UIPickerViewDelegate

사용
class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate

UIPickerViewDataSource : UIPicker내에 데이터를 변경할 때 사용하는 Protocol
UIPickerViewDelegate : UIPicker를 뷰에서 사용할때 사용하는 Delegate

numberOfComponents

사용
func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

애플 공식문서에 따르면, 구성요소를 정의하는 곳이라고 한다. 구성요소라 함은, 아마도 내 생각엔 카테고리를 지칭하는 듯 하다. 보통 Table View에서도 이런 구성요소를 지정하는 메서드를 본적이 있다. 그때도 카테고리처럼 나눠지는 것을 볼 수 있었는데, 이것 또한 그런 듯 하다.

위 코드는 각 나라의 화폐단위를 적은 Picker를 사용해야 하니, 카테고리는 딱히 필요 없으니 1개로 설정해놓았다.

pickerView(_ pickerView:component:) -> Int

정의
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        // 아마도 코인 개수를 의미하는 듯 하다.
        return coinManager.currencyArray.count
    }

위 메서드는 반환타입이 존재한다. Int 로 값을 반환한다.
위 메서드는 PickerView에서 사용할 인덱스들의 개수를 정의하는 메서드인듯 하다.
아까 위 메서드는 카테고리를 지정했다면, 이건 카테고리 안에 들어있는 인덱스들의 개수 정의다.

pickerView(_ pickerView:row:component:) -> String?

정의
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        // 아래 코드는 아마도, 해당 열의 화폐단위를 세팅하는 듯 하다.
        return coinManager.currencyArray[row]
    }

위 메서드도 반환타입이 존재하는데, 대신 위와 다른 String 타입이다.
아까 위에는 개수를 반환했다면, 이건 각 인덱스에서 표시할 화폐단위들을 반환한다.(USD,HKD 등)

pickerView(_ pickerView:row:component: Int)

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let selectCurrencyArray = coinManager.currencyArray[row]
        // 선택한 화폐단위의 가치를 가져오는 메서드
        coinManager.getCoinPrice(for: selectCurrencyArray)
    }

위 메서드는 PickerView에서 선택됐을 때 그 인덱스를 얻어오는 메서드이다.
하지만 우리는 인덱스 번호를 반환하는게 목적이 아니니, 그 수를 가지고 해당 인덱스에 위치하는 화폐 단위를 가치를 파악할 수 있는 메서드인 getCoinPrice에 전달하는 역할을 해줬다.

JSON Parsing

JSON 파싱은 ViewController 에서 진행하는게 아닌 다른 스위프트 파일에서 진행한다.
CoinManager로 들어가보자.

protocol CoinManagerDelegate {
    func didFailWithError(error: Error)
    func didSelectedCoin(coin: CoinModel)
    
}

struct CoinManager {
    let baseURL = "https://rest.coinapi.io/v1/exchangerate/BTC"
    let apiKey = "YOUR_API_KEYS"
    
    let currencyArray = ["AUD", "BRL","CAD","CNY","EUR","GBP","HKD","IDR",
    			 "ILS","INR","JPY","MXN","NOK","NZD","PLN","RON",
                 	 "RUB","SEK","SGD","USD","ZAR"]
    
    var delegate: CoinManagerDelegate?
    
    func getCoinPrice(for currency: String) {
        let urlString = "\(baseURL)/\(currency)?apikey=\(apiKey)"
        performRequest(with: urlString)
    }

위 코드를 보면 아까 우리가 사용했던 getCoinPrice가 보이게 된다. 이코드는 아까도 말했듯이 선택한 화폐단위로 현재 비트코인 값을 구해오는 메서드라고 했다.
따라서 위에 미리 정해진 API URL를 조합하여 원하는 화폐단위의 정보를 받아올 수 있는 링크를 만들고 난 뒤에 performRequest에게 그 url을 전송해주는 역할을 한다.

performRequest

func performRequest(with urlString: String) {
    if let url = URL(string: urlString) {
        // 2. URL 세션 만들기
        // URLSession메서드는 우리가 크롬상에서 보던 JSON 과 같은 형식으로 만들어주는 역할을 한다.
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: url) { (data, response, error ) in
            if error != nil {
                self.delegate?.didFailWithError(error: error!)
                return
            }
            if let safeData = data {
                // 내 입맛대로 땡겨온 데이터를
                if let coin = self.parseJSON(safeData) {
                    // ViewController에서 사용해야하니 얘한테 옮김
                    self.delegate?.didSelectedCoin(coin: coin)
                }
            }
        }
        task.resume()
    }
}

JSON 파싱의 핵심이라고 할 수 있는 performRequest를 보자.
먼저 위에서 만들어진 url을 if let 구문으로 넘어가게 만들고, 세션을 만든다.
이때 URLSession을 사용하면 우리가 크롬상에서 보는 JSON과 같은 형식으로 만들어준다.
그리고 난 뒤에 세션으로 만든 JSON Data를 task 상수에 저장하려고 dataTask를 해준다. 이때 클로저를 사용하여 해당 정보의 문제가 생기거나, 혹은 더 다양한 기능을 사용하고 싶을때 정의한다.

위 코드에서는 data,response,error 3개의 핸들러를 가지고 진행했고, 클로저 내부에 들어가게 될경우

if error != nil {
    self.delegate?.didFailWithError(error: error!)
    return
}

먼저 에러가 발생하지 않을 경우에는 nil을 반환할 것이다. 그러나, 에러가 발생하여 error 핸들러에 일정 값이 들어갔을 경우, 위 코드가 실행되도록 한 것이다.

if let safeData = data {
    // 내 입맛대로 땡겨온 데이터를
    if let coin = self.parseJSON(safeData) {
    // ViewController에서 사용해야하니 얘한테 옮김
    self.delegate?.didSelectedCoin(coin: coin)
    }
}

에러가 발생하지 않았을경우에 실행되는 코드이다. 먼저 data 핸들러는 옵셔널로 설정된다. 따라서 if let 구문으로 언래핑을 해주고 난 뒤, 파싱을 이어나간다. 언래핑을 한 데이터를 parseJSON 메서드로 옮겨주고(이 메서드는 바로 아래서설명하겠다), 받아온 데이터를 다시 ViewController로 옮긴다.

task.resume()

그리고 마지막 줄에 이 코드는 꼭 써줘야 한다. 마치 다 해놓고 마감버튼을 누르지 않는 느낌이랄까..? 라고 생각하면 된다. 그리고 task.start() 가 아니라 왜 resume() 이냐고 생각하는 사람들이 있을 것이다. 나도 정확히는 모르지만, 기억상 중간에 에러를 걸러내는 코드가 있다고 해도, 에러는 언제든 존재할 수 있기 때문에, resume()으로 중간에 생길 에러를 방지하고자, 에러가 생기면 중지될 수 있게 하기 위해서 였던걸로 기억한다. (사실 잘 모른다. 혹시 아시는 분들 있으면 댓글 부탁드린다.)

parseJSON

// 파싱해온 정보 데이터화 시키기
// 원하는 데이터만 골라 먹기 하는 parseJSON 메서드
func parseJSON(_ coinData: Data) -> CoinModel? {
    // JSON을 디코딩할 수 있는 객체
    let decoder = JSONDecoder()
    do {
        // 디코더로 땡겨와서
        let decodedData = try decoder.decode(CoinData.self, from: coinData)
        // 여기서 원하는 정보들 변수로 저장시키고
        let lastPrice = decodedData.rate
        let name = decodedData.asset_id_quote
        // 모델 파일에 원하는것들만 추가시키고 리턴
        let coin = CoinModel(rate: lastPrice, asset_id_quote: name)
        return coin
    } catch {
        delegate?.didFailWithError(error: error)
            return nil
        }
    }

위 코드는 performRequest 에서 파싱 후 언래핑 해온 JSON 데이터를 Swift에서 효율적으로 사용할 수 있게 만드는 코드이다.

parseJSON 의 반환 타입을 보면 CoinModel?으로 보이는데, 이는 CoinModel로 따로 JSON파일 중에서 내가 필요로 하는 것만 모아논 구조체 swift 파일이다.

struct CoinData: Codable {
    let time: String
    let rate: Double
    let asset_id_quote: String
}

struct CoinModel {
    let rate: Double
    let asset_id_quote: String
}

이렇게 두개의 구조체가 존재한다. CoinData는 실제 JSON 파일에서 처음으로 데이터를 가져 오는 것이고(이때 상수의 이름과 타입이 JSON에서 제공하는 것과 동일해야 파싱이 가능하다),
CoinModel의 경우 가져온 데이터 중에서 내가 필요로 하는 것만 따로 빼온것이다.

따라서 JSON 파일을 통해 CoinData로 정의된 것들에 대해 파싱을 받아온 후, CoinModel로 따로 내가 원하는 것들을 또 옮기는 작업을 하는 것이다.

무튼, do - catch 구문을 사용했는데, 먼저 do 구문에 사용한 코드를 보자.

// JSON을 디코딩할 수 있는 객체
let decoder = JSONDecoder()
do {
    // 디코더로 땡겨와서
    let decodedData = try decoder.decode(CoinData.self, from: coinData)
    // 여기서 원하는 정보들 변수로 저장시키고
    let lastPrice = decodedData.rate
    let name = decodedData.asset_id_quote
    // 모델 파일에 원하는것들만 추가시키고 리턴
    let coin = CoinModel(rate: lastPrice, asset_id_quote: name)
    return coin
} 

JSONDecoder()는 JSON을 디코딩할 수 있게 해주는 객체이다.
디코딩이란, 아까 말했던 JSON 파일을 swift에서 사용할 수 있게 해주는 작업이다.

그리고 do 구문을 열고, decoder 상수에 이미 디코딩을 할 수 있게 적어줬고, 이제 decodedDataCoinData로 지정해뒀던 이름들에 해당하는 정보들을 땡겨와준다.

그리고 난 뒤에 가져온 정보들 중에, 내가 사용할 것들만 상수로 저장 시켜주고 난 뒤에, CoinModel을 초기화 해준다.

그리고 CoinModel 반환하면 우리가 원했던 각 나라의 시세로 본 비트코인 시세를 확인할 수 있다.

Protocol

protocol CoinManagerDelegate {
    func didFailWithError(error: Error)
    func didSelectedCoin(coin: CoinModel)
}

프로토콜이란, 일종의 규약, 제약 같은 것이다. "이 프로토콜을 추가 하면 최소한 이정도는 있어야해!" 라고 알려주는 느낌이다. 필수적인 요소들을 넣어놓으면 좋을 기능이다.

위 코드를 예시로 들면, 위 코드는 비트코인의 시세를 각 나라의 화폐 단위로 표시하는 앱을 개발하고 있는 프로토콜이다. 그때 필요한 최소 메서드는, 선택된 화폐단위로 계산된 시세, 에러 발생시의 행동 요 두개의 메서드 정도 일 것이다. 따라서 위 코드에서 그렇게 지정 하였고, 메서드 이외에도 상수, 변수, 구조체, 클래스 등 모든게 선언이 가능하다.

그리고 이 프로토콜을 Class나 extension에서 선택했을때, 위 두개는 꼭 구현해야 한다.

extension

// exction 으로 리펙토링
extension ViewController: CoinManagerDelegate {
    // 프로토콜로 추가한 두개의 메서드를 정의
    func didFailWithError(error: Error) {
        print(error)
    }
    
    // 매개변수는 하나면 충분
    func didSelectedCoin(coin: CoinModel) {
        DispatchQueue.main.async {
            // 큐 돌리면서 앱 비동기 방식으로 하나하나~
            self.bitcoinLabel.text = String(format: "%.2f", coin.rate)
            self.currencyLabel.text = coin.asset_id_quote
        }
    }
}

extension이란 본래 있던 것에 대해 추가적으로 기능을 부여하거나, 위와 같이 따로 리팩토링이 가능하게 해줄 수 있다. 이로 인한 이점은 코드가 읽기 편해지고, 기존 기능에서 추가적인 기능을 사용할 수 있게 해준다.

위 코드는 아까 위에서 선언한 프로토콜을 구현한 코드이다.
didFailWithError의 경우 에러발생시 앱이 행해야 할 것을 얘기하고 있는데, 일단 그냥 print() 해줬다.

didSelectedCoin의 경우 JSON Parsing을 통해 받은 데이터들을 앱 label에 옮겨 사용자에게 보여지게 해야 하는 기능을 구현해야 한다.
따라서, DispatchQueue 를 이용해, 하나하나 데이터를 받아와서 과부하가 오지 않게 데이터를 처리한다. 그렇게 label을 업데이트한다.

그리고 익스텐션을 사용할 때의 주의점이 있다.

var coinManager = CoinManager()
override func viewDidLoad() {
        super.viewDidLoad()
        currencyPicker.dataSource = self
        currencyPicker.delegate = self
        coinManager.delegate = self
    }

위 코드는 ViewControllerviewDidLoad() 메서드이다. 여기에 아까 extension으로 선언한 CoinManager의 delegate를 self로 묶어줘야 한다. 그래야지 extension의 모든 코드가 정상 작동한다.
이 코드의 뜻을 내 생각대로 해석해보자면, 일단 ViewController와 연결이 되어있어야 코드가 작동하지 않을까? 애초에 delegate가 대리자 역할을 하는데, 대리자가 의뢰자 없이는 뭔가를 할 수 없듯이 self가 지정되어있어야 코드가 진행 될 것이라고 생각한다.

프로젝트를 하며 느끼고 배운점

API 사용법이 너무 어려워서 강의를 다시 듣고 새로 시작한 챌린지였다. 이번 챌린지를 진행하며 어느정도 API 파싱을 할 수 있게된 듯 하다. 사실 ~Model, ~Data 파일들의 의미를 잘 몰랐었고, 파싱 자체를 너무나도 어렵게 생각했었지만, 진행되는 코드를 천천히 살피고 의미를 애플 공식문서를 찾아보면서 의미를 곱씹어보니, 그렇게 어려운 것은 아니라고 느끼게 되었다. 뭐..다른 API를 사용해보며 익숙해지는 방법밖에 없는 듯 하다.
extension, protocol 에 대한 기초적인 구성방법 또한 얻어가는 프로젝트였다. 이 앱을 시작으로 API가 좀 더 쉬워지는 계기가 되었으면 한다.

좋은 웹페이지 즐겨찾기