{"boxOfficeResult":{"boxofficeType":"일별 박스오피스","showRange":"20260510~20260510","dailyBoxOfficeList":[{"rnum":"1","rank":"1","rankInten":"1","rankOldAndNew":"OLD","movieCd":"20259773","movieNm":"슈퍼 마리오 갤럭시","openDt":"2026-04-29","salesAmt":"799924360","salesShare":"28.9","salesInten":"-17890220","salesChange":"-2.2","salesAcc":"12724669750","audiCnt":"82482","audiInten":"-1543","audiChange":"-1.8","audiAcc":"1318717","scrnCnt":"1422","showCnt":"4618"},{"rnum":"2","rank":"2","rankInten":"-1","rankOldAndNew":"OLD","movieCd":"20259791","movieNm":"악마는 프라다를 입는다 2","openDt":"2026-04-29","salesAmt":"709778320","salesShare":"25.6","salesInten":"-171168170","salesChange":"-19.4","salesAcc":"12150628470","audiCnt":"68337","audiInten":"-15964","audiChange":"-18.9","audiAcc":"1221631","scrnCnt":"1132","showCnt":"3898"},{"rnum":"3","rank":"3","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20254121","movieNm":"살목지","openDt":"2026-04-08","salesAmt":"581454750","salesShare":"21.0","salesInten":"-90855500","salesChange":"-13.5","salesAcc":"30967865120","audiCnt":"56204","audiInten":"-8532","audiChange":"-13.2","audiAcc":"3022843","scrnCnt":"1088","showCnt":"3653"},{"rnum":"4","rank":"4","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20261782","movieNm":"프로젝트 헤일메리","openDt":"2026-03-18","salesAmt":"257972810","salesShare":"9.3","salesInten":"-61661530","salesChange":"-19.3","salesAcc":"31576050620","audiCnt":"20769","audiInten":"-4527","audiChange":"-17.9","audiAcc":"2782964","scrnCnt":"608","showCnt":"1047"},{"rnum":"5","rank":"5","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20240532","movieNm":"짱구","openDt":"2026-04-22","salesAmt":"125904100","salesShare":"4.5","salesInten":"-6795680","salesChange":"-5.1","salesAcc":"3669868060","audiCnt":"12231","audiInten":"-682","audiChange":"-5.3","audiAcc":"376605","scrnCnt":"494","showCnt":"869"},{"rnum":"6","rank":"6","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20242837","movieNm":"왕과 사는 남자","openDt":"2026-02-04","salesAmt":"83398400","salesShare":"3.0","salesInten":"-4351430","salesChange":"-5","salesAcc":"162501475130","audiCnt":"8286","audiInten":"-372","audiChange":"-4.3","audiAcc":"16833524","scrnCnt":"385","showCnt":"566"},{"rnum":"7","rank":"7","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20262685","movieNm":"란 12.3","openDt":"2026-04-22","salesAmt":"41936150","salesShare":"1.5","salesInten":"-2007610","salesChange":"-4.6","salesAcc":"2302425760","audiCnt":"4210","audiInten":"-112","audiChange":"-2.6","audiAcc":"230387","scrnCnt":"297","showCnt":"398"},{"rnum":"8","rank":"8","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20262947","movieNm":"모탈 컴뱃 2","openDt":"2026-05-06","salesAmt":"27477950","salesShare":"1.0","salesInten":"-10056190","salesChange":"-26.8","salesAcc":"136112040","audiCnt":"2340","audiInten":"-819","audiChange":"-25.9","audiAcc":"12049","scrnCnt":"335","showCnt":"404"},{"rnum":"9","rank":"9","rankInten":"0","rankOldAndNew":"OLD","movieCd":"20247648","movieNm":"내 이름은","openDt":"2026-04-15","salesAmt":"13356750","salesShare":"0.5","salesInten":"-415750","salesChange":"-3","salesAcc":"1828145980","audiCnt":"1540","audiInten":"-5","audiChange":"-0.3","audiAcc":"204746","scrnCnt":"97","showCnt":"122"},{"rnum":"10","rank":"10","rankInten":"1","rankOldAndNew":"OLD","movieCd":"20262775","movieNm":"기동전사 건담: 섬광의 하사웨이 키르케의 마녀","openDt":"2026-04-22","salesAmt":"8243550","salesShare":"0.3","salesInten":"-2494900","salesChange":"-23.2","salesAcc":"504187900","audiCnt":"819","audiInten":"-330","audiChange":"-28.7","audiAcc":"41278","scrnCnt":"50","showCnt":"53"}]}}
영화진흥위원회에서 api로 받은 json입니다.


init?(string: String) 여기왜 ?가 있는거니
Claude 응답: Failable Initializer (실패 가능한 이니셜라이저)
Failable Initializer (실패 가능한 이니셜라이저)
init?는 Swift의 failable initializer입니다. 초기화가 실패할 수 있을 때 사용합니다.
반환 타입의 차이
| init(string:) | 반드시 인스턴스 반환 | 크래시 or 강제 처리 |
| init?(string:) | Optional 인스턴스 | nil 반환 |
예시
struct Color {
let hex: String
init?(string: String) {
guard string.hasPrefix("#") && string.count == 7 else {
return nil // 조건 불만족 시 nil 반환
}
self.hex = string
}
}
let valid = Color(string: "#FF5733") // Optional(Color)
let invalid = Color(string: "FF5733") // nil
왜 쓰나?
입력값이 항상 유효하다고 보장할 수 없을 때 사용합니다. 예를 들어:
- 문자열 → 숫자 변환 (Int("abc") → nil)
- 잘못된 형식의 URL, 날짜, 색상 코드
- 범위를 벗어난 값
? 덕분에 호출하는 쪽에서 옵셔널 바인딩으로 안전하게 처리할 수 있습니다.
if let color = Color(string: "#FF5733") {
print("유효한 색상: \(color.hex)")
} else {
print("잘못된 형식")
}


일단 이렇게 해서 네트워킹 1단계 주소 만들기는 끝났습니다.




getData() 함수 전체 설명
전체 흐름: 네트워크에서 데이터 받아오기
1단계 (48번줄) - URL 만들기
guard let url = URL(string: movieURL) else { return }
- movieURL(문자열)을 실제 URL 객체로 변환
- 변환 실패 시 (nil) 함수 종료
- URL(string:)이 init?이라 guard let으로 언래핑
2단계 (50번줄) - URLSession 만들기
let session = URLSession(configuration: .default)
- 네트워크 요청을 담당하는 세션 생성
- .default = 기본 설정 사용
3단계 (51~58번줄) - dataTask 만들기
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
print(error!)
return
}
guard let JSONdata = data else { return }
print(JSONdata)
}
- url로 실제 네트워크 요청을 준비
- 요청이 완료되면 클로저 { } 안이 실행됨
- error != nil → 에러 있으면 출력 후 종료
- data → 서버에서 받은 실제 데이터 (옵셔널이라 guard로 언래핑)
⚠️ 여기까지는 준비만 한 것, 아직 요청 안 보냄
4단계 (60번줄) - 요청 시작
task.resume()
- 이걸 호출해야 실제로 네트워크 요청이 시작됨
- dataTask는 기본이 일시정지 상태라 resume()이 필수
전체 요약
movieURL(String) → URL 객체 → URLSession → dataTask 생성 → resume()으로 요청 → 응답 수신

App Transport Security (ATS) 오류
HTTP로 요청하려고 해서 막힌 겁니다.
iOS는 기본적으로 HTTPS만 허용하고 HTTP는 보안상 차단합니다.
해결 방법 2가지
✅ 방법 1: URL을 HTTPS로 바꾸기 (권장)
movieURL이 http://로 시작하면 https://로 변경
let movieURL = "https://tokobis.or.kr/..." // http → https
방법 2: Info.plist에 예외 추가 (임시/테스트용)
Info.plist에 아래 항목 추가:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>tokobis.or.kr</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
⚠️ 방법 2는 앱스토어 심사에서 사유 소명이 필요할 수 있으니, 가능하면 방법 1(HTTPS) 을 쓰는 게 좋습니다.
소스링크를 http에서 https로 수정하면 된다.

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
// let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
import Foundation
// MARK: - Welcome
struct Welcome {
let boxOfficeResult: BoxOfficeResult
}
// MARK: - BoxOfficeResult
struct BoxOfficeResult {
let boxofficeType, showRange: String
let dailyBoxOfficeList: [DailyBoxOfficeList]
}
// MARK: - DailyBoxOfficeList
struct DailyBoxOfficeList {
let rnum, rank, rankInten: String
let rankOldAndNew: RankOldAndNew
let movieCD, movieNm, openDt, salesAmt: String
let salesShare, salesInten, salesChange, salesAcc: String
let audiCnt, audiInten, audiChange, audiAcc: String
let scrnCnt, showCnt: String
}
enum RankOldAndNew: String {
case old
}

영화 박스오피스 JSON 데이터를 쉽게 파싱하기 위한 Swift 구조체를 다음과 같이 작성할 수 있습니다. 또한 `Codable` 프로토콜을 채택하여 `JSONDecoder`로 간편하게 디코딩할 수 있도록 하였습니다. 빠뜨리지 않도록 각 프로퍼티 타입과 네이밍을 JSON 키에 맞추고, `CodingKeys`를 사용해 스네이크케이스 등도 대응합니다.
```swift
import Foundation
// MARK: - MovieData (Welcome 상응)
struct MovieData: Codable {
let boxOfficeResult: BoxOfficeResult
}
// MARK: - BoxOfficeResult
struct BoxOfficeResult: Codable {
let boxofficeType: String
let showRange: String
let dailyBoxOfficeList: [DailyBoxOffice]
enum CodingKeys: String, CodingKey {
case boxofficeType = "boxofficeType"
case showRange = "showRange"
case dailyBoxOfficeList = "dailyBoxOfficeList"
}
}
// MARK: - DailyBoxOffice
struct DailyBoxOffice: Codable {
let rnum: String
let rank: String
let rankInten: String
let rankOldAndNew: RankOldAndNew
let movieCD: String
let movieNm: String
let openDt: String
let salesAmt: String
let salesShare: String
let salesInten: String
let salesChange: String
let salesAcc: String
let audiCnt: String
let audiInten: String
let audiChange: String
let audiAcc: String
let scrnCnt: String
let showCnt: String
enum CodingKeys: String, CodingKey {
case rnum = "rnum"
case rank = "rank"
case rankInten = "rankInten"
case rankOldAndNew = "rankOldAndNew"
case movieCD = "movieCD"
case movieNm = "movieNm"
case openDt = "openDt"
case salesAmt = "salesAmt"
case salesShare = "salesShare"
case salesInten = "salesInten"
case salesChange = "salesChange"
case salesAcc = "salesAcc"
case audiCnt = "audiCnt"
case audiInten = "audiInten"
case audiChange = "audiChange"
case audiAcc = "audiAcc"
case scrnCnt = "scrnCnt"
case showCnt = "showCnt"
}
}
enum RankOldAndNew: String, Codable {
case old = "OLD"
case new = "NEW"
}
```
---
### 설명
- 최상위 구조체명을 `MovieData`로 변경하여 의미를 명확히 하였습니다.
- 각 구조체는 `Codable`을 채택해 JSON 디코딩 시 자동으로 매핑되도록 하였습니다.
- `dailyBoxOfficeList` 배열 안에 영화별 일별 박스오피스 정보가 들어있어, 반복문으로 쉽게 접근 가능하도록 설계했습니다.
- `RankOldAndNew` 열거형은 JSON 값에 맞게 대소문자 구분하여 `old`, `new` 값을 처리하도록 구현했습니다.
- 별도의 `CodingKeys`를 명시해 JSON 키와 프로퍼티 이름 매핑을 명확하게 했습니다. (필수는 아니지만 구조체 필드명과 다를 때 안전한 방법입니다.)
이 구조체를 프로젝트에 추가하고, 다음과 같이 데이터를 파싱할 수 있습니다.
```swift
if let decoded = try? JSONDecoder().decode(MovieData.self, from: jsonData) {
// decoded.boxOfficeResult.dailyBoxOfficeList 배열 접근 가능
}
```
필요하다면 타입을 `String` 대신 `Int`나 `Double`로 변환해 처리도 가능합니다. 원하시면 추가 도움 드리겠습니다.
Codable을 사용하는 이유는 한마디로 "Swift 객체와 외부 데이터(주로 JSON) 사이의 번역기" 역할을 하기 때문입니다.
우리가 서버에서 데이터를 받아오면 보통 JSON이라는 텍스트 형태의 덩어리입니다. 하지만 우리가 코드를 짤 때는 이 텍스트를 그대로 쓰지 않고, movieNm, rank 같은 변수가 담긴 구조체(Struct)로 다루고 싶어 하죠.
1. Codable이 하는 일
Codable은 사실 두 가지 프로토콜이 합쳐진 별명(Typealias)입니다.
- Decodable: JSON 같은 외부 데이터를 Swift 구조체나 클래스로 변환(해독)하는 능력
- Encodable: Swift 구조체나 클래스를 JSON 같은 데이터로 변환(부호화)하는 능력
2. 왜 굳이 이걸 써야 하나요?
① 자동 매칭 (편의성)
Codable을 쓰지 않으면, JSON에 있는 수십 개의 데이터를 우리가 일일이 꺼내서 변수에 담아줘야 합니다.
- Codable 미사용: "JSON의 'movieNm'이라는 키를 찾아서 String으로 바꾼 다음, 내 구조체의 movieNm 변수에 넣어줘..." (매우 귀찮음)
- Codable 사용: "이 구조체는 Codable 하니까, 알아서 JSON이랑 이름 똑같은 것끼리 쏙쏙 집어넣어!"
② 타입 안전성 (Type Safety)
JSON은 그냥 텍스트일 뿐이지만, Swift 구조체에 담는 순간 audiCnt는 숫자형인지 문자열형인지 명확해집니다. Codable은 이 과정에서 데이터 타입이 맞지 않으면 에러를 내뱉어주어, 잘못된 데이터가 앱에 들어오는 것을 방지합니다.
3. 질문하신 코드에서의 의미
struct DailyBoxOfficeList : Codable {
let movieNm : String
let audiCnt : String
}
여기서 : Codable을 붙임으로써, Swift는 이제 이 구조체를 보고 이렇게 이해합니다.
"아하! 이 구조체는 서버에서 오는 JSON 데이터의 'movieNm'과 'audiCnt' 키값을 찾아서 자기 변수에 자동으로 저장할 줄 아는 애구나!"
요약하자면
서버에서 받아온 JSON 덩어리를 우리가 쓰기 편한 Swift 구조체로 자동 변환하기 위해 Codable을 사용합니다. 만약 이걸 안 쓰면 JSON 파싱 코드를 수십 줄 직접 작성해야 하는 고난의 길이 시작됩니다.








//
// ViewController.swift
// MovieLJY
//
// Created by comsoft on 2026/05/04.
//
import UIKit
// ============================================================
// MARK: - 전역 데이터
// ============================================================
// 영화 이름 하드코딩 배열 (현재는 API 데이터로 대체되어 미사용)
let name = ["1:슈퍼 마리오 갤럭시",
"2:악마는 프라다를 입는다 2",
"3:살목지",
"4:프로젝트 헤일메리",
"5:짱구",
"6:왕과 사는 남자",
"7:란 12.3",
"8:내 이름은",
"9:사랑의 하츄핑 특별판",
"10:극장판 반짝반짝 달님이: 싱어롱 파티"]
// ============================================================
// MARK: - JSON 파싱용 구조체 (Codable)
// ============================================================
// 최상위 JSON 구조 { "boxOfficeResult": { ... } }
struct MovieData: Codable {
let boxOfficeResult: BoxOfficeResult
}
// boxOfficeResult 안의 구조 { "dailyBoxOfficeList": [ ... ] }
struct BoxOfficeResult: Codable {
let dailyBoxOfficeList: [DailyBoxOfficeList]
}
// dailyBoxOfficeList 배열의 각 영화 항목
struct DailyBoxOfficeList: Codable {
let movieNm: String // 영화 이름
let audiCnt: String // 당일 관객수
let audiAcc: String // 누적 관객수
let rank: String // 현재 순위
}
// ============================================================
// MARK: - ViewController
// ============================================================
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// 스토리보드에서 연결한 테이블뷰
@IBOutlet weak var table: UITableView!
// API에서 받아온 데이터를 저장하는 변수 (처음엔 nil)
var movieData: MovieData?
// 박스오피스 API 주소 (날짜: 20260510)
let movieURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=536e058824a8f5bcc8c39f075897985f&targetDt=20260510"
// ============================================================
// MARK: - UITableViewDataSource
// ============================================================
// 테이블 셀 개수: 고정 10개
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
// 각 셀 구성
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// "myCell" 식별자로 재사용 셀 꺼내기 (커스텀 셀 MyTableViewCell로 캐스팅)
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
// movieData가 있으면 해당 row의 영화 이름을 셀에 표시
// indexPath.row = 0~9 → dailyBoxOfficeList[0]~[9]
cell.moiveName.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm
return cell
}
// 섹션 개수: 1개
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
// ============================================================
// MARK: - 생명주기
// ============================================================
override func viewDidLoad() {
super.viewDidLoad()
// 테이블뷰의 delegate, dataSource를 이 ViewController가 담당
table.delegate = self
table.dataSource = self
// 화면 로드 시 API 호출
getData()
}
// ============================================================
// MARK: - 네트워크 통신
// ============================================================
func getData() {
// 1단계: 문자열 → URL 객체 변환 (실패 시 함수 종료)
guard let url = URL(string: movieURL) else { return }
// 2단계: URLSession 인스턴스 생성 (기본 설정)
let session = URLSession(configuration: .default)
// 3단계: dataTask 생성 - url로 요청 후 응답을 클로저에서 처리
let task = session.dataTask(with: url) { data, response, error in
// 에러 발생 시 출력 후 종료
if error != nil {
print(error!)
return
}
// data가 nil이면 종료
guard let JSONdata = data else { return }
// JSONDecoder로 Data → MovieData 구조체 변환
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(MovieData.self, from: JSONdata)
// 파싱 확인용 출력
print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm)
print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiAcc)
print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiCnt)
// 파싱한 데이터를 멤버변수에 저장
self.movieData = decodedData
// ⚠️ UI 업데이트는 반드시 메인 스레드에서!
// (네트워크 응답은 백그라운드 스레드에서 오기 때문)
DispatchQueue.main.async {
self.table.reloadData() // 테이블뷰 새로고침
}
} catch {
// JSON 파싱 실패 시 에러 출력
print(error)
}
}
// 4단계: 요청 시작 (resume 호출 전까지 요청 안 보냄)
task.resume()
}
}
전체 동작 흐름 요약
viewDidLoad()
└── getData()
├── URL 생성
├── URLSession으로 API 호출
├── JSON 수신
├── JSONDecoder로 MovieData 구조체에 파싱
├── self.movieData에 저장
└── 메인스레드에서 table.reloadData()
└── cellForRowAt 호출
└── movieData[row].movieNm → 셀에 표시
핵심 포인트 2가지
| Codable | JSON 키와 프로퍼티 이름만 맞추면 자동 파싱 |
| DispatchQueue.main.async | 네트워크는 백그라운드 스레드, UI는 반드시 메인 스레드에서 처리 |
DispatchQueue.main.async {
self.table.reloadData()
}ㅇ 여기안에서 리로드 데이터를 하면 제대로 잘뜬다.

//
// ViewController.swift
// MovieLJY
//
// Created by comsoft on 2026/05/04.
//
import UIKit
// ============================================================
// MARK: - JSON 파싱용 구조체
// ============================================================
// 최상위 JSON { "boxOfficeResult": { ... } }
struct MovieData: Codable {
let boxOfficeResult: BoxOfficeResult
}
// boxOfficeResult 안의 { "dailyBoxOfficeList": [ ... ] }
struct BoxOfficeResult: Codable {
let dailyBoxOfficeList: [DailyBoxOfficeList]
}
// dailyBoxOfficeList 배열의 각 영화 항목
struct DailyBoxOfficeList: Codable {
let movieNm: String // 영화 이름
let audiCnt: String // 당일 관객수
let audiAcc: String // 누적 관객수
let rank: String // 순위
}
// ============================================================
// MARK: - ViewController
// ============================================================
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// 스토리보드에서 연결한 테이블뷰
@IBOutlet weak var table: UITableView!
// API에서 받아온 데이터 저장 변수 (초기값 nil)
var movieData: MovieData?
// API 기본 주소 (날짜는 뒤에 동적으로 붙임 → var로 선언)
var movieURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=536e058824a8f5bcc8c39f075897985f&targetDt="
// ============================================================
// MARK: - UITableViewDataSource
// ============================================================
// 셀 개수: 고정 10개
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
// 각 셀 구성
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// "myCell" 식별자로 재사용 셀 꺼내기
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
// row 번호에 해당하는 영화 이름을 셀에 표시
// movieData가 nil이면 아무것도 표시 안 함 (옵셔널 체이닝 ?.)
cell.moiveName.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm
return cell
}
// 섹션 개수: 1개
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
// ============================================================
// MARK: - 생명주기
// ============================================================
override func viewDidLoad() {
super.viewDidLoad()
table.delegate = self
table.dataSource = self
// 기본 URL 뒤에 어제 날짜 문자열을 붙여서 완성
// 예: "...&targetDt=" + "20260510" → "...&targetDt=20260510"
movieURL += makeYesterdayString()
// 완성된 URL로 API 호출
getData()
}
// ============================================================
// MARK: - 어제 날짜 문자열 생성
// ============================================================
func makeYesterdayString() -> String {
// 오늘 날짜에서 -1일 → 어제 날짜 계산
let y = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
// 날짜 형식 지정: yyyyMMdd (예: 20260510)
let dateF = DateFormatter()
dateF.dateFormat = "yyyyMMdd"
// Date → String 변환 후 반환
let day = dateF.string(from: y)
return day
}
// ============================================================
// MARK: - 네트워크 통신
// ============================================================
func getData() {
// 1단계: 문자열 → URL 객체 변환 (실패 시 함수 종료)
guard let url = URL(string: movieURL) else { return }
// 2단계: URLSession 인스턴스 생성
let session = URLSession(configuration: .default)
// 3단계: dataTask 생성 - 응답은 클로저에서 처리
let task = session.dataTask(with: url) { data, response, error in
// 에러 발생 시 출력 후 종료
if error != nil {
print(error!)
return
}
// data가 nil이면 종료
guard let JSONdata = data else { return }
// JSONDecoder로 Data → MovieData 구조체 변환
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(MovieData.self, from: JSONdata)
// 파싱 확인용 콘솔 출력
print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm)
print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiAcc)
print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiCnt)
// 파싱 데이터를 멤버변수에 저장
self.movieData = decodedData
// ⚠️ UI 업데이트는 반드시 메인 스레드에서!
// 네트워크 응답은 백그라운드 스레드로 오기 때문
DispatchQueue.main.async {
self.table.reloadData() // 테이블뷰 새로고침
}
} catch {
// 파싱 실패 시 에러 출력
print(error)
}
}
// 4단계: 요청 시작 (이 전까지는 요청 안 보냄)
task.resume()
}
}
이전 버전과 달라진 점
| movieURL | let (날짜 고정) | var (날짜 동적으로 붙임) |
| 날짜 | targetDt=20260510 하드코딩 | makeYesterdayString()으로 자동 계산 |
| 결과 | 항상 같은 날 데이터 | 실행할 때마다 어제 데이터 자동 조회 |
makeYesterdayString() 흐름
Date() → 오늘 날짜
byAdding: -1 → 어제 날짜
dateFormat → "yyyyMMdd"
return → "20260510"