[Swift] OpenAPI 활용 영화 순위 앱 만들기(3)

승민·2025년 5월 21일

Swift

목록 보기
10/10

  • 이전 내용 실습을 완료한 후 다음 내용을 진행할 수 있어요.
  • 이전 시간에는 API를 통해서 영화 정보를 가져오는 실습을 했어요.
  • 이번 시간에는 목록에 있는 내용의 상세 정보를 나타내는 실습을 할 예정이에요.

View Controller

  • root view controller는 UIWindow의 가장 첫 번째, 최상위 뷰 컨트롤러
  • 앱이 실행될 때 처음 사용자에게 보여지는 메인 뷰 컨트롤러
  • UIWindow의 최상위 뷰 컨트롤러
  • 앱 실행 시 사용자에게 가장 먼저 보여지는 화면
  • 꼭 UINavigationController나 UITabBarController 같은 컨테이너 컨트롤러일 필요는 없음
  • 단, 여러 화면 전환이 필요한 경우 보통 이런 컨테이너 컨트롤러가 root로 사용됨

root view controller

  • UINavigationController, UITabBarController와 같은 컨테이너 뷰
  • 컨트롤러가 주로 루트로 사용됨
  • 여러 화면(ViewController)을 stack(스택) 구조로 관리하는 컨테이너 컨트롤러
  • push/pop 방식으로 화면 이동을 지원
  • root view controller로 사용하면, 첫 화면(ViewController에서 시작해 다음 화면으로 이동하는 구조)을 쉽게 구현할 수 있음

Action Segue

  • 스토리보드에서 버튼 등 UI 요소와 화면(VC)을 바로 연결
  • UI 요소(버튼 클릭 등)의 액션이 발생하면 자동으로 화면 전환
  • 코드 필요 없이 동작

Manual Segue

  • 스토리보드에서 ViewController끼리만 연결하고, Identifier를 지정
  • 코드에서 performSegue(withIdentifier:)로 직접 호출해서 화면 전환
  • 코드로 조건을 줄 수 있음 (특정 상황에서만 이동 등)

Action Segue, Manual Segue 정리 표

구분Action SegueManual Segue
연결 방법UI 요소에서 바로 segue 연결ViewController끼리 segue 연결 후 Identifier 설정
실행 방식UI 액션 발생 시 자동 전환코드로 performSegue 호출 시 직접 전환
코드 필요별도 코드 필요 없음코드로 전환해야 직접 호출 필요
주요 용도단순히 UI로 바로 이동할 때조건부/특정 상황에서만 이동할 때

  1. Navigation Controller가 Root View Controller로 변경됨
  2. Navigation Controller에 연결된 View Controller에 Navigation Item(<back) 생김
  3. Navigation Controller에 storyboard entry point가 생김
  4. Navigation Controller와 View Controller의 segue가 연결됨

테이블 셀 클릭 시 화면 전환

segue 실행되기 직전에 자동으로 호출되는 메서드

func prepare(for segue: UIStoryboardSegue, sender: Any?)
  • 뷰 컨트롤러에게 segue가 수행될 예정임을 알림
  • segue 실행되기 직전에 자동으로 호출되는 메서드
    • segue: segue와 관련된 뷰 컨트롤러에 대한 정보
    • sender : segue를 시작한 객체
  • Segue로 화면이 전환되기 직전에 자동 호출되는 메서드
  • 다음 화면(ViewController)로 데이터를 전달할 때 사용
  • override해서, segue의 식별자(identifier)와 목적지(destination)를 확인한 뒤 데이터를 넘길 수 있음

prepare 함수

override func prepare(for segue: UIStoryboardSegue, sender: Any?)

WKWebView를 통한 영화 상세 정보 나타내기

퍼센트 인코딩(percent encoding)

let urlKorString = "https://search.naver.com/search.naver?query="+movieName
let urlString = urlKorString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!

탭바

// MapViewController.swift
import UIKit
import WebKit

class MapViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    override func viewDidLoad() {
        super.viewDidLoad()
        let urlKorString = "https://map.naver.com/p/search/영화관"
        let urlString = urlKorString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
        guard let url = URL(string:urlString) else { return }
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

디자인

테이블의 셀 테두리 라운딩 처리

아이콘 추가

LaunchScreen 이미지 추가

MVC 패턴으로 변환

  1. Model: 모델 구조체, 네트워크 데이터 로더(Model Layer)
  2. View: Storyboard의 TableView, MyTableViewCell (코드는 변화 거의 없음)
  3. Controller: ViewController (View와 Model 연결 역할만 집중)

1. Model 분리

1-1. 모델 구조체 (변경 거의 없음)

// MovieModels.swift
import Foundation

struct MovieData: Codable {
    let boxOfficeResult: BoxOfficeResult
}
struct BoxOfficeResult: Codable {
    let dailyBoxOfficeList: [DailyBoxOfficeList]
}
struct DailyBoxOfficeList: Codable {
    let movieNm: String
    let audiCnt: String
    let audiAcc: String
    let rank: String
}

1-2. 모델: 네트워크 담당 객체 분리

// MovieService.swift
import Foundation

class MovieService {
    static let shared = MovieService()
    
    private let baseURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=<API_KEY>&targetDt="
    
    func fetchDailyBoxOffice(completion: @escaping (Result<[DailyBoxOfficeList], Error>) -> Void) {
        let urlStr = baseURL + MovieService.yesterdayString()
        guard let url = URL(string: urlStr) else { return }
        
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            guard let data = data else { return }
            do {
                let movieData = try JSONDecoder().decode(MovieData.self, from: data)
                let list = movieData.boxOfficeResult.dailyBoxOfficeList
                completion(.success(list))
            } catch {
                completion(.failure(error))
            }
        }
        task.resume()
    }
    
    /// 컨트롤러에서 날짜 때문에 반복적으로 사용하므로 static method로
    static func yesterdayString() -> String {
        let today = Date()
        let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMdd"
        return formatter.string(from: yesterday)
    }
}

2. View 분리 (변화 없음, 그대로)

  • MyTableViewCell.swift 그대로 사용
  • Storyboard의 TableView도 원래대로

3. Controller (ViewController) - 조정

ViewController에서 Model과 View를 분리하고,
데이터 요청, 뷰 표시 역할로 명확히 한정합니다.

ViewController.swift (코드 정리 및 역할 분리)

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var table: UITableView!
    
    // MARK: Model
    var movies: [DailyBoxOfficeList] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        table.delegate = self
        table.dataSource = self
        fetchMovies()
    }
    
    private func fetchMovies() {
        MovieService.shared.fetchDailyBoxOffice { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success(let movieList):
                    self?.movies = movieList
                    self?.table.reloadData()
                case .failure(let error):
                    print("영화 정보 로딩 실패: \(error)")
                }
            }
        }
    }
    
    // MARK: - UITableView

    func numberOfSections(in tableView: UITableView) -> Int { return 1 }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return movies.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as? MyTableViewCell else {
            return UITableViewCell()
        }
        let movie = movies[indexPath.row]
        cell.movieName.text = "[\(movie.rank)위] \(movie.movieNm)"
        // 숫자 변환
        if let aCnt = Int(movie.audiCnt) {
            let numF = NumberFormatter()
            numF.numberStyle = .decimal
            cell.audiCount.text = "어제: \(numF.string(for: aCnt)! )명"
        } else {
            cell.audiCount.text = "어제: -명"
        }
        if let aAcc = Int(movie.audiAcc) {
            let numF = NumberFormatter()
            numF.numberStyle = .decimal
            cell.audiAccumulate.text = "누적: \(numF.string(for: aAcc)! )명"
        } else {
            cell.audiAccumulate.text = "누적: -명"
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "🍿박스오피스(영화진흥위원회제공:" + MovieService.yesterdayString() + ")🍿"
    }
    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
        return "made by gsmin"
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // nothing
    }
    
    // MARK: - Navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let dest = segue.destination as? DetailViewController,
              let idx = table.indexPathForSelectedRow?.row else { return }
        dest.movieName = movies[idx].movieNm
    }
}

4. 나머지 뷰/컨트롤러 (변경 없음)

MyTableViewCell.swift, DetailViewController.swift,
MapViewController.swift는 변화 없음.


5. 분리된 구성

  • Model: MovieModels.swift, MovieService.swift
  • View: Storyboard, MyTableViewCell.swift
  • Controller: ViewController.swift, DetailViewController.swift, MapViewController.swift

이렇게 하면 네트워크 로직의 재활용성이 높아지고(다른 뷰 컨트롤러에서도 호출 가능),
ViewController는 Model과 View 연결에만 집중된, 패턴의 본래 취지에 맞는 구조가 됩니다.


6. 전체 구조 요약

📁 Model
  ├─ MovieModels.swift (Codable)
  └─ MovieService.swift (네트워크)

📁 View
  ├─ MyTableViewCell.swift
  └─ Storyboard 파일

📁 Controller
  ├─ ViewController.swift (Table, Navigation)
  ├─ DetailViewController.swift
  └─ MapViewController.swift

정리

  • 영화의 목록을 보고 선택 시 상세 페이지로 이동하는 기능을 구현했어요.
  • 탭바를 통해 지도와 연결할 수 있고, 코드를 MVC로 변환하는 것까지 해봤어요.

출처 : Smile Han - iOS 프로그래밍 기초

0개의 댓글