나의 발자취
KakaoAPI를 활용한 BookSearch 앱 만들기 (1) 본문
Kakao API를 통해, 책 검색 API를 사용해 검색한 키워드대로 책 목록을 가져오고, 목록을 클릭하면 상세 화면으로 넘어가 책에 대한 상세 내용을 웹뷰로 띄워줄것이다.
func search()
먼저 Kakao API로 책을 검색할 수 있는 함수를 만들어줄 것이다. (API 활용부분은 넘어가기로 함.)
func search() 안에 url, request를 구현해준다.
func search(){
var endPoint = "https://dapi.kakao.com/v3/search/book?sort=recency&query=love"
guard let url = URL(string: endPoint) else { return }
var request = URLRequest(url: url)
Header 교체
카카오 API key를 넣어주기 위해 헤더에 값을 넣어줄 것이다.
request.httpMethod = "GET" // optional이라 기본값이 있어서, 안넣어도 됨.
request.addValue(apiKey, forHTTPHeaderField: "Authorization")
세션 연결
let session = URLSession.shared
세션 연결 코드를 보면, URLSession.shared이라고 된 걸 볼 수 있다. property를 호출하고 있는건데, 이것은 Singleton 객체 라는 것을 알면 된다. 충돌을 방지하기 위해, 세션은 하나만 열려야 하므로 싱글톤 패턴을 사용했다.
Task 생성 & 실행
세션을 통해 request를 보내는 task 변수를 생성해줄 것이다.
let task = session.dataTask(with: request)
그리고 task를 생성만 하고선 실행을 해주어야지만 동작한다!! 그러므로 아래쪽에 아래와 같이 코드를 작성해준다.
Singleton Pattern
여기서 잠깐, dataTask()를 보자.⭐️
dataTask()를 보면, 받는 인자는 두개인것을 알 수 있고, completionHandler 파라미터의 경우 함수를 받는 것을 알 수 있다. 이 함수는 세 개의 인자값을 가지는데, Data?, URLResponse?, (any Error)? 을 받아서 return은 void값을 하는 함수라는 것을 알 수 있다. 그래서 completionHandler를 클로저로 해주어야하는 것이다.
클로저이기때문에 엔터를 쳐서 클로저로 만들어준다!
지금 task 변수가 하는 일을 짚어보자.
let task = session.dataTask(with: request)
- task를 받아서 request를 받는다. 그다음 뒤의 클로저가 호출된다.
{ data, response, error in ~~~~~
let task = session.dataTask(with: request) { data, response, error in
if let error {
print("error 발생")
return
}
JSON Serialization (데이터 직렬화), 예외 처리
root 변수를 보면, 우리가 지금 받는 데이터는 지금 아래와 같이 크게 documents, meta 데이터로 구분할 수 있다.
documents - Array, meta - Object (Dictionary) 타입인 걸 알 수 있으니 [String:Any]로 받는다.
- 여기서 'root'로 변수명을 설정한 이유는, 시멘틱하게 지어서 그렇다.
- 또한 우리가 받는 타입이 Object(Diction) type 이므로 .jsonObject 를 사용했지만 Array인 경우 .jsonArray를 사용한다.
guard let data else { return }
// Data 직렬화
do {
guard let root = try JSONSerialization.jsonObject(with: data) as? [String:Any],
let documents = root["documents"] as? [[String:Any]]
else { return }
} catch {
print("JSON Parsing Error Occured")
}
* 한줄 위로: Cmd+Opt+{
코드 간결성을 위해서 guard let 두개였는데 각각으로 분리해서 빼줬다.
guard let data else { return }
// Data 직렬화
do {
// root 객체 - Object(Diction) type 이므로 .jsonObject (Array인 경우 .jsonArray)
guard let root = try JSONSerialization.jsonObject(with: data) as? [String:Any]
else { return }
let documents = root["documents"] as? [[String:Any]]
} catch {
print("JSON Parsing Error Occured")
}
그리고 'document' 변수를 클래스 전역변수로 사용할 수 있도록 클래스 제일 위 선언부에 선언해주었다.
var documents: [[String:Any]]?
클로저 안에서 메서드에 접근하려면, 앞에 'self' 키워드를 붙여주어야한다.!! 클로저가 호출되는 시점에서 컨텍스트를 다 가지고 간다.
self.documents = root["documents"] as? [[String:Any]]
do {
// root 객체 - Object(Diction) type 이므로 .jsonObject (Array인 경우 .jsonArray)
guard let root = try JSONSerialization.jsonObject(with: data) as? [String:Any]
else { return }
self.documents = root["documents"] as? [[String:Any]]
} catch {
print("JSON Parsing Error Occured")
}
func 인자값 넣기
func search () 에서
func searchWithQuery(_ query: String?, page: Int){ 로 바꾸었다.
그럼 함수 호출할때도 이런식 >> searchWithQuery("사랑", page: 2) 으로 호출을 해주어야할 것이다.
URL 내 한글문자 인코딩하여 처리하기
let strURL = endPoint.addingPercentEncoding(withAllowedCharacters: urlQueryAllowed)
옵셔널이므로, 이 또한 언래핑을 해주어야해서 guard-let이 두 번 들어가야 하는 상황이다. 따라서 마침 url의 guard-let 처리도 아래에 있으므로 합쳐서 하나로 만들어준다.
<Before>
let strURL = endPoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
guard let url = URL(string: endPoint) else { return }
<After>
guard let strURL = endPoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: endPoint) else { return }
SearchBarDelegate 메서드 구현하기
스토리보드에 이렇게 구현을 해주었는데, search bar를 보면 Delegate는 이미 생성된것을 볼 수 있어서 메서드 정의만 해줄것이다.
extension BookTableViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
page = 1
searchWithQuery(searchBar.text, page: page)
}
}
searchBar.text 가 옵셔널이라, 위쪽 searchWithQuery 함수에서 옵셔널을 처리해주기로 한다.
guard let query else { return }
UITableViewController: numberOfSections, numberOfRowsInSection 구현
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
그리고
documents.count를 쓰면 옵셔널이라 에러가 나서, nil-coalescing operator를 써서 optional 바인딩을 해준다.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return documents?.count ?? 0
cellForRowAt
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "book", for: indexPath)
guard let book = documents?[indexPath.row] else { return cell }
let imageView = cell.viewWithTag(1) as? UIImageView
let lblTitle = cell.viewWithTag(2) as? UILabel
lblTitle?.text = book["title"] as? String
let lblAuthors = cell.viewWithTag(3) as? UILabel
let authors = book["authors"] as? [String]
lblAuthors?.text = authors?.joined(separator: ", ")
let lblPublisher = cell.viewWithTag(4) as? UILabel
lblPublisher?.text = book["publisher"] as? String
let lblPrice = cell.viewWithTag(5) as? UILabel
lblPrice?.text = "\((book["price"] as? Int) ?? 0)원"
return cell
}
그리고 실행해보면, 시뮬레이터에 나오지 않는다.
그 이유는? GCD를 알아야한다.
다음 편에 계속 --
'앱 개발 > iOS' 카테고리의 다른 글
API 결과값 안에 tag가 있을 때 - Regex (0) | 2024.09.10 |
---|---|
KakaoAPI를 활용한 BookSearch 앱 만들기 (2) (0) | 2024.09.09 |
WebView: alert 만들기 (파일명: WebNativeCom) (9) | 2024.09.05 |
Xcode 주석 종류 (0) | 2024.09.05 |
iOS WebKit: 웹뷰 frame 크기 조절, 버튼 넣기 (1) | 2024.09.05 |