[Swift] Youtube API 활용하여 앱 만들기 (2) : Alamofire를 사용하여 API 읽어오기

Oni·2023년 9월 5일
1

TIL

목록 보기
40/47
post-thumbnail

Alamofire

Alamofire는 Swift 언어를 위한 HTTP 네트워킹 라이브러리로, iOS 및 macOS 애플리케이션 개발에 사용된다. Alamofire를 사용하면 네트워크 요청을 쉽게 만들고 관리할 수 있으며, 서버와의 데이터 통신을 처리하는 데 도움이 된다.

Alamofire의 주요 특징

  • 간편한 HTTP 요청 생성: Alamofire는 간단하고 직관적인 API를 제공하여 GET, POST, PUT, DELETE 등의 HTTP 요청을 생성하고 실행할 수 있다.
  • 비동기 네트워킹: Alamofire는 비동기적인 네트워킹을 지원하므로 UI 스레드를 차단하지 않고 네트워크 요청을 처리할 수 있다.
  • JSON 및 이미지 다운로드: Alamofire는 JSON 데이터를 쉽게 요청하고 파싱하며, 이미지 다운로드 및 캐싱도 지원한다.
  • 인증과 보안: 인증 토큰, 사용자 이름 및 비밀번호와 같은 다양한 인증 방법과 함께 사용하기 용이하며, SSL 인증서 핀단 및 기타 보안 기능을 지원한다.
  • 업로드와 다운로드: Alamofire는 파일 업로드 및 다운로드도 처리할 수 있다.

Alamofire Pacakage

🔗 https://github.com/Alamofire/Alamofire

Alamofire 사용법

1. GET

import Alamofire

Alamofire.request("https://example.com/api/data").responseJSON { response in
    if let data = response.data {
        // JSON 파싱 또는 데이터 처리
    }
}

2. POST

import Alamofire

let parameters: Parameters = ["key": "value"]
Alamofire.request("https://example.com/api/post", method: .post, parameters: parameters).responseJSON { response in
    if let data = response.data {
        // JSON 파싱 또는 데이터 처리
    }
}

3. 이미지 다운로드

import Alamofire

Alamofire.download("https://example.com/image.jpg").responseData { response in
    if let data = response.result.value {
        let image = UIImage(data: data)
        // 이미지를 사용하거나 처리
    }
}

API 초기 셋팅하기

API

enum API {
    static let baseUrl: String = "https://youtube.googleapis.com/youtube/v3/"
    static let key: String = "발급받은 API KEY"
}

API를 받아오기 전에 base URL과 지난 포스팅에서 발급받은 API KEY를 enum으로 만들어준다.

API Manager

처음에 api를 html에서 확인했을 때 아래와 같이 데이터가 호출되는 걸 볼 수 있다.
지금은 video에 대한 정보를 가져오는 Endpoint로 불러왔기 때문에 이렇게 보이는 거고, 각자 필요한 데이터를 가이드에서 확인한 후 Endpoint를 지정해주면 된다.

{
    "kind": "youtube#videoListResponse",
    "etag": "fsSt6aCW9h1L51_Saqof0QamBa0",
    "items": [
        {
            "kind": "youtube#video",
            "etag": "bQuiUMzicTNZIVyp0wWPOJTB8rM",
            "id": "r7LzjutoXIo",
            "snippet": {
                "publishedAt": "2023-09-03T08:08:38Z",
                "channelId": "UCN5XdqTDRbyjXPF5NXUqWdA",
                "title": "파리지앵 캐릭터 만들어준 항도니랑 간만에 떠드는 무도 이야기",
                "description": "형돈아 항상 널 응원하고, 고마워!\n\n00:00 하이라이트\n01:04 오프닝\n01:42 항도니 입장\n17:59 Back to the 순정마초\n\n#정형돈 #무한도전 #파리돼지앵",
                "thumbnails": {
                    "default": {
                        "url": "https://i.ytimg.com/vi/r7LzjutoXIo/default.jpg",
                        "width": 120,
                        "height": 90
                    },
                    
                    ...
                    
                    "standard": {
                        "url": "https://i.ytimg.com/vi/r7LzjutoXIo/sddefault.jpg",
                        "width": 640,
                        "height": 480
                    },
                    
                    ...
                },
                "channelTitle": "요정재형",
                "tags": [
                    "정재형",
                    "요정재형",
                    "요정",
                    
                    ...
                    
                ],
                "categoryId": "22",
                "liveBroadcastContent": "none",
                "localized": {
                    "title": "파리지앵 캐릭터 만들어준 항도니랑 간만에 떠드는 무도 이야기",
                    "description": "형돈아 항상 널 응원하고, 고마워!\n\n00:00 하이라이트\n01:04 오프닝\n01:42 항도니 입장\n17:59 Back to the 순정마초\n\n#정형돈 #무한도전 #파리돼지앵"
                },
                "defaultAudioLanguage": "ko"
            }
        }
    ],
    "nextPageToken": "CAUQAA",
    "pageInfo": {
        "totalResults": 200,
        "resultsPerPage": 5
    }
}

나는 이 데이터 중에 썸네일 이미지(url)와 영상 제목(title), 채널명(channelTitle) 세개를 뽑아 쓸거다.

일단 API를 관리할 수 있는 Manager 클래스를 하나 만들어준다.

import Alamofire

class APIManager {
    static let shared = APIManager()
    private init() {}
    
    func fetchVideos(pageToken: String, completion: @escaping (Result<Any, AFError>) -> Void) {
        let url = API.baseUrl + "videos"
        let apiParam = [
            "part": "snippet",
            "chart": "mostPopular",
            "maxResult": 500,
            "regionCode": "KR",
            "key": API.key,
            "pageToken": "\(pageToken)"
        ] as [String: Any]
        
        AF.request(url, method: .get, parameters: apiParam)
            .validate()
            .responseJSON { response in
                completion(response.result)
                debugPrint(response)
            }
    }
}
  • APIManager: APIManager는 싱글톤 디자인 패턴을 사용하여 하나의 인스턴스만을 생성하고 공유함(앱 내에서 네트워크 호출을 관리하는 데 유용)
  • fetchVideos: YouTube API에 GET 요청을 보내는 역할을 하며, pageToken 매개변수를 받아와서 페이지를 넘길 때 사용함
  • url 및 apiParam: url은 YouTube API의 엔드포인트 URL을 나타낸다. apiParam은 요청에 필요한 매개변수를 포함하는 딕셔너리로 구성함. part는 "snippet"으로 설정되어 비디오 정보의 일부만 반환하도록 지정한다. chart는 "mostPopular"로 설정되어 인기 있는 비디오를 가져오며, 그 외에도 키, 지역 코드, 페이지 토큰 등의 정보가 포함된다.
  • AF.request: 네트워크 요청을 생성하고 보낸다. 이 요청은 지정된 URL로 가서 매개변수와 함께 GET 요청을 보냄
  • validate(): 응답의 상태 코드 및 헤더를 확인하여 요청이 성공했는지 여부를 결정(유효성 검사)
  • responseJSON: 네트워크 요청의 응답을 처리한다. 클로저 내에서는 completion 매개변수를 통해 결과를 반환하고, debugPrint를 사용하여 응답을 디버그 출력함

GET Data

API Manager를 통해 읽어온 데이터를 필요한 것만 골라서 사용하려고 한다.
일단 만들어둔 CollectionViewCell에 들어갈 썸네일, 영상 제목, 채널명을 저장할 빈 배열을 만들어준다. 그리고 셀을 선택했을 때 디테일페이지에 넘겨줄 video의 고유 id도 저장할 배열을 만들었다.

// MARK: - Variables
private var nextPageToken: String?
static var videoIds: [String] = []
private var thumbnails: [UIImage] = []
private var titles: [String] = []
private var users: [String] = []

loadVideo()

홈 화면을 로드할 때 프로퍼티로 만들어둔 배열에 필요한 항목들을 append해주고, 배열에 있는 순서대로 셀에 보여줄거다.

// MARK: - YouTube Video Load
private func loadVideo(pageToken: String? = nil) {
	APIManager.shared.fetchVideos(pageToken: nextPageToken ?? "") { [weak self] result in
		switch result {
		case .success(let data):
			if let json = data as? [String:Any],
            let items = json["items"] as? [[String:Any]] {
            	for item in items {
                	if let id = item["id"] as? String,
                       let snippet = item["snippet"] as? [String:Any],
                       let title = snippet["title"] as? String,
                       let thumbnails = snippet["thumbnails"] as? [String:Any],
                       let standard = thumbnails["standard"] as? [String:Any],
                       let thumbnailUrl = standard["url"] as? String,
                       let user = snippet["channelTitle"] as? String {
                		AF.request(thumbnailUrl).responseData { response in
                        	switch response.result {
                            case .success(let data):
                            	if let image = UIImage(data: data) {
                                	HomeViewController.videoIds.append(id)
                                    self?.thumbnails.append(image)
                                    self?.titles.append(title)
                                    self?.users.append(user)
                                    DispatchQueue.main.async {
                                    	self?.collectionView.reloadData()
                                    }
                            	} else {
                                	print("Failed to convert data to UIImage")
                                }
                            case .failure(let error):
                            print("Image download error: \(error)")
                		}
                       }
            	}
            }
            self?.nextPageToken = json["nextPageToken"] as? String
		}
		case .failure(let error):
        print(error)
		}
	}
}
  • private func loadVideo(pageToken: String? = nil): 이 메서드는 pageToken 매개변수를 받아와 YouTube API를 통해 비디오 데이터를 가져오는 역할을 한다. pageToken은 YouTube API의 페이지 매커니즘에서 사용되며, 페이지를 넘길 때 이전 페이지의 nextPageToken을 사용한다. 기본값으로 nil을 가지므로 초기 호출 시에는 페이지 토큰을 지정하지 않을 수 있다.
  • APIManager.shared.fetchVideos(pageToken: nextPageToken ?? ""): APIManager 클래스의 fetchVideos 메서드를 호출하여 YouTube API로부터 비디오 데이터를 가져온다. pageToken으로는 nextPageToken을 사용하며, 이 값이 nil이면 기본값으로 빈 문자열("")을 사용한다.
  • 비동기 처리: API 요청은 비동기로 처리되므로, API로부터 데이터를 가져오는 동안 앱의 동작이 차단되지 않는다. 비동기 클로저 내에서 API 응답을 처리한다.
  • 성공 및 실패 처리: API 요청이 성공하면 result에 성공한 데이터가 전달된다. 데이터를 JSON 형식으로 파싱하고, 비디오 정보를 추출한 다음 화면에 표시한다. 또한, nextPageToken을 업데이트하여 다음 페이지를 가져올 준비를 함
  • 이미지 다운로드: 비디오 정보에 포함된 썸네일 이미지의 URL을 사용하여 Alamofire를 통해 이미지를 비동기적으로 다운로드하고 화면에 표시하며, 이미지 다운로드 성공 또는 실패에 따라 적절한 처리를 수행한다.
  • UI 업데이트: 데이터를 가져온 후에는 메인 스레드에서 UI를 업데이트하고, 콜렉션 뷰를 리로드하여 비디오 정보를 화면에 표시한다.

무한 스크롤뷰

처음 데이터를 불러오면 5개밖에 안뜨는데, api url 자체에서 초기 보여주는 데이터가 5개라서 그렇다.
그래서 nextPageToken을 사용해서 다음 페이지를 로드하게 되는데, 그럼 '그 다음페이지를 로드하는 기준을 언제로 잡을까'를 고민해야 한다.

scrollViewDidScroll()

scrollViewDidScroll은 UIScrollViewDelegate 프로토콜에 정의된 메서드 중 하나로, 스크롤 뷰의 스크롤 동작이 발생할 때 호출되는 메서드이다. 이 메서드를 사용하면 스크롤 뷰의 스크롤 이벤트에 대한 응답을 구현할 수 있다.

  • 호출 시점: 스크롤 뷰가 스크롤될 때 호출됨
  • 호출 주체: 이 메서드는 UIScrollViewDelegate 프로토콜을 채택한 객체, 즉 스크롤 뷰의 대리자(delegate)가 구현하며, 스크롤 뷰의 대리자는 주로 뷰 컨트롤러
  • 활용 예시: 무한 스크롤(pagination) 구현, 스크롤 위치에 따른 UI 업데이트, 추가 데이터 로드 등을 처리할 때 유용하게 활용됨
  • 파라미터: 스크롤 뷰 자체를 나타내는 scrollView 파라미터를 받아 스크롤 뷰의 상태 및 속성에 접근할 수 있음

그럼 스크롤을 내렸을 때 로드할 함수를 구현하고 해당 함수를 delegate에 추가해준다.

private func loadMoreData() {
	loadVideo(pageToken: nextPageToken)
	nextPageToken = ""
}
extension HomeViewController: UICollectionViewDelegate, UICollectionViewDataSource {
	func scrollViewDidScroll(_ scrollView: UIScrollView) {
		let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height
        let scrollViewHeight = scrollView.frame.size.height
        
        if offsetY > contentHeight - scrollViewHeight {
            loadMoreData()
        }
    }
}
  • offsetY: scrollView의 contentOffset 속성을 이용하여 현재 스크롤 위치를 나타내는 값. 스크롤의 상단이 뷰의 가장 위쪽에서 얼마나 떨어져 있는지를 나타냄
  • contentHeight: scrollView의 contentSize 속성을 이용하여 스크롤 가능한 콘텐츠의 전체 높이를 나타내는 값. 이 값은 스크롤되는 콘텐츠의 총 높이를 나타냄
  • scrollViewHeight: scrollView의 프레임 크기에서 뷰의 실제 높이를 나타내는 값. 이 값은 스크롤 뷰가 화면에 표시되는 높이를 나타냄
  • (조건문) if offsetY > contentHeight - scrollViewHeight: 만약 현재 스크롤 위치 offsetY가 콘텐츠의 끝(contentHeight)에서 스크롤 뷰의 높이(scrollViewHeight)를 뺀 값보다 크다면, 이는 스크롤이 아래로 도달했다는 것을 의미. 이 경우 loadMoreData() 함수를 호출하여 추가 데이터를 로드함

🤳🏻적용화면

근데 지금은 문제가 좀 있다..
loadVideo가 스크롤을 내릴때 여러번 호출이 되서 같은 영상이 반복해서 보이고 있다..
조금 더 문제를 파악한 다음에 조치를 취해야겠다..ㅠ

profile
하지만 나는 끝까지 살아남을 거야!

2개의 댓글

comment-user-thumbnail
2023년 9월 6일

저는 아직 네트워크 활용이 어려운데 잘 활용하시는 것 같네요! 글 잘 참고하겠습니다!

1개의 답글