[ProJect 일지] 영화 예매 앱 만들기 (1)

 

오늘은 두번째 팀프로젝트가 시작되는 날이다. 발제가 끝난 후 영화 예매 앱을 만드는 프로젝트를 진행하기로 결정해 역할 분담을 하게 되었다.

 

먼저 팀원분들이 작업을 시작하기 전 영화 API를 받아와 데이터 모델링을 진행하였다.


데이터 모델링 설정 , API 받아오기

영화 데이터는 TMDB에서 API Key를 발급 받아 데이터를 가져왔다.

import Foundation

let url = URL(string: "<https://api.themoviedb.org/3/movie/popular>")!
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
let queryItems: [URLQueryItem] = [
  URLQueryItem(name: "language", value: "ko-KR"),
  URLQueryItem(name: "page", value: "1"),
]
components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems

var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
request.timeoutInterval = 10
request.allHTTPHeaderFields = [
  "accept": "application/json",
  "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI5MjVlZDdlZjAxOTM2NWI4ZjFiMWFkMjRjOTQ4NDkzOSIsInN1YiI6IjY2MjYwMTEwYWY5NTkwMDE3ZDZhMDBmOSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.F9AOBDGvoS2XnYPcxL6L2i4tw6n-27xbikaDh4YB_Qo"
]

let (data, response) = try await URLSession.shared.data(for: request)
print(String(decoding: data, as: UTF8.self))

TMDB 에서 이렇게 데이터를 받아 우리가 사용할 URLSession으로 바꿔주자 그전에 먼저 데이터 모델링이 쉽게 될 수 있도록 JSON 데이터를 바꿔주었다.

import Foundation

// MARK: - Welcome
struct Welcome: Codable {
    let page: Int
    let results: [Result]
    let totalPages, totalResults: Int

    enum CodingKeys: String, CodingKey {
        case page, results
        case totalPages = "total_pages"
        case totalResults = "total_results"
    }
}

// MARK: - Result
struct Result: Codable {
    let adult: Bool
    let backdropPath: String
    let genreIDS: [Int]
    let id: Int
    let originalTitle, overview: String
    let popularity: Double
    let posterPath, releaseDate, title: String
    let voteAverage: Double
    let voteCount: Int

    enum CodingKeys: String, CodingKey {
        case adult
        case backdropPath = "backdrop_path"
        case genreIDS = "genre_ids"
        case id
        case originalTitle = "original_title"
        case overview, popularity
        case posterPath = "poster_path"
        case releaseDate = "release_date"
        case title
        case voteAverage = "vote_average"
        case voteCount = "vote_count"
    }
}

이렇게 데이터 모델링을 마치고 네트워킹 매니저를 만들어 데이터를 올바르게 받아올 수 있도록 설정하자!

 

NetworkingManager 구현

새로운 그룹을 만들어 NetworkingManager를 만들어 준 후 발급 받은 API 데이터를 잘 받아올 수 있도록 URLSession을 만들어준다.

class NetworkingManager {
    
    static let shared = NetworkingManager() // Singleton 인스턴스
    
    private let moiveURL = "<https://api.themoviedb.org/3/movie/popular>"
    private let apiKey = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI5MjVlZDdlZjAxOTM2NWI4ZjFiMWFkMjRjOTQ4NDkzOSIsInN1YiI6IjY2MjYwMTEwYWY5NTkwMDE3ZDZhMDBmOSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.F9AOBDGvoS2XnYPcxL6L2i4tw6n-27xbikaDh4YB_Qo"
    
    private init() {}
    
    
    func fetchPopularMovies(completion: @escaping (Swift.Result<data, error="">) -> Void) {
        guard let movieURL = URL(string: moiveURL) else {
            let error = NSError(domain: "InvalidURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid movie URL"])
            completion(.failure(error))
            return
        }
        
        var request = URLRequest(url: movieURL)
        request.httpMethod = "GET"
        
        
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("Bearer \\(apiKey)", forHTTPHeaderField: "Authorization")
        
        
        var components = URLComponents(url: movieURL, resolvingAgainstBaseURL: true)!
        components.queryItems = [
            URLQueryItem(name: "language", value: "ko-KR"),
            URLQueryItem(name: "page", value: "1")
        ]
        guard let url = components.url else {
            let error = NSError(domain: "InvalidURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid movie URL"])
            completion(.failure(error))
            return
        }
        request.url = url
        
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
                let error = NSError(domain: "HTTPError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid HTTP response"])
                completion(.failure(error))
                return
            }
            
            guard let data = data else {
                let error = NSError(domain: "InvalidData", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data received"])
                completion(.failure(error))
                return
            }
            
            completion(.success(data))
        }
        task.resume()
    }
    
}
</data,>

이렇게 데이터를 받아주니 다행이 데이터가 오류 없이 잘 받아졌다.. 참 다행이었다. 혹시나 안될까 조마조마했는데..

 

그럼 이제 데이터 모델링 설정하고 네트워킹 연결까지 해주었으니, 데이터를 넣어줄 VC를 만들어 오토레이아웃을 잡아주자

 

ViewController 오토레이아웃 설정

내가 맡은 부분은 로그인 구현하는 부분과 영화 예매 앱의 첫 화면인 영화 목록을 만드는 부분을 맡게되었다.

먼저 영화 첫 화면인 영화 목록을 만드는 부분은 이렇게 구성하였다.

컬렉션 뷰 , 이미지 페이지 컨트롤러 등 여러가지 컴포넌트들을 활용해서 조금 다양한 구성으로 만들어 볼 예정이기 때문에 스크롤 뷰를 사용해 가로로 내릴 수 있도록 설정했다.

 

그 다음은 로그인 구현 부분인데 이부분은 UserDefaults 를 활용해 정보를 저장하고 실제로 로그인이 되도록 구현할 예정이다.

가입된 정보가 없다면 회원가입을 할 수 있도록 구성하였다.

 

이렇게 로그인 구현 및 영화 목록을 나타내는 VC의 오토레이아웃을 마치고 영화 목록에 아까 받은 네트워크 데이터를 활용해 컬렉션뷰에 영화 정보를 넣어보도록 하자

 

CollectionView 영화 정보 넣기

컬렉션 뷰에 영화 정보를 넣기 위해 코드를 구성해보자!

let netWorkingManager = NetworkingManager.shared
    var movieData: Welcome?
    
    @IBOutlet weak var profileImage: UIImageView!
    @IBOutlet weak var profileName: UILabel!
    @IBOutlet weak var profileIntro: UILabel!
    @IBOutlet weak var mainSearchBar: UISearchBar!
    @IBOutlet weak var mainPageImage: UIImageView!
    @IBOutlet weak var collectionViewHeaderLabel: UILabel!
    @IBOutlet weak var mainCollectionView: UICollectionView!

먼저 네트워크 데이터를 가져와야하니 싱글톤으로 구성된 네트워크 매니저를 가져와 변수에 담아두고 데이터 모델링을 해준 구조체를 가져와 movieData에 담아준다.

func fetchData() {
    netWorkingManager.fetchPopularMovies { [weak self] result in
        switch result {
        case .success(let data):
            do {
                let decoder = JSONDecoder()
                self?.movieData = try decoder.decode(Welcome.self, from: data)
                DispatchQueue.main.async {
                    self?.mainCollectionView.reloadData()
                }
            } catch {
                print("\\(error)")
            }
        case .failure(let error):
            print("\\(error)")
        }
    }
}

그 다음에 이렇게 네트워크 매니저에서 가져온 데이터를 VC에 가져와 컬렉션 뷰에 데이터를 넣을 수 있도록 함수를 구현해준다.

extension MainViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return movieData?.results.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as? MainCollectionViewCell else { fatalError("error")
        }
        
        if let movieData = movieData {
            let movie = movieData.results[indexPath.item]
            cell.collectionMainLabel.text = movie.title
            cell.collectionSubLabel.text = String(movie.voteCount)
        }
        
        return cell
        
    }
}

그 다음 컬렉션 뷰를 구현해야하니 이전에도 사용했던 컬렉션 뷰 델리게이트와 데이터소스를 구현해준다.

그리고 컬렉션 뷰에서 꼭 설정해주어야 하는 FlowLayout을 설정해주자!

func createFlowLayout() -> UICollectionViewFlowLayout {
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .horizontal
    layout.minimumLineSpacing = 10
    layout.minimumInteritemSpacing = 10
    layout.itemSize = CGSize(width: 200, height: 350)
    return layout
}

이렇게 설정한 뒤

func setupCollectionView() {
    let flowLayout = createFlowLayout()
    mainCollectionView.collectionViewLayout = flowLayout
    mainCollectionView.delegate = self
    mainCollectionView.dataSource = self
    mainCollectionView.alwaysBounceHorizontal = true
}

이렇게 컬렉션 뷰 setup 함수를 설정해 setupCollectionView() 를 viewDidLoad에 설정해 제대로 호출될 수 있도록 구성하였다.

 

이렇게 구현하니 당연히 잘 구현되었다고 생각했으나.. 자꾸 오류가 나는게 사람을 미치게 했다.. ㅎ

 

한참을 삽질하다 결국 찾은 오류가 이전에 깃허브를 만들면서 만났던 스토리보드 내 모든 컴포넌트를 다 구현한 후 다시 register를 활용하여 컬렉션 뷰 셀을 호출한게 잘못이었다..

 

이번 기회에 또 배웠다.. 그 register 호출을 삭제하니 오류 없이 잘 구현될 수 있었다.

 

이렇게!

오늘은 여기까지~ 이번 프로젝트는 구현해야할 내용이 생각보다 많을 것 같아 더 타이트하게 진행해야할 것 같다. 그럼 오늘은 여기까지!