카테고리 없음

iOS프로그래밍실무9주차 과제

jong133 2026. 5. 4. 16:40

영진위api를 이용해 키를 발급받아 05 03 영화순위의 데이터를 json을 한번 출력해보았다.
네이버와 오픈api를 비교해 보았다.

 

[“슈퍼 마리오 갤럭시"

,"악마는 프라다를 입는다 2"

,”살목지"

,”프로젝트 헤일메리"

,"짱구"

,”왕과 사는 남자"

," 12.3"

," 이름은"

,"사랑의 하츄핑 특별판"

,"극장판 반짝반짝 달님이: 싱어롱 파티”]

https://codebeautify.org/jsonviewer

 

Best JSON Viewer and JSON Beautifier Online

Online JSON Viewer, JSON Beautifier and Formatter to beautify and tree view of JSON data - It works as JSON Pretty Print to pretty print JSON data.

codebeautify.org

open api를 해석하는 방법

import Foundation

// MARK: - Root
struct BoxOfficeResponse: Codable {
    let boxOfficeResult: BoxOfficeResult
}

// MARK: - BoxOfficeResult
struct BoxOfficeResult: Codable {
    let boxofficeType: String
    let showRange: String
    let dailyBoxOfficeList: [DailyBoxOffice]
}

// MARK: - DailyBoxOffice
struct DailyBoxOffice: Codable {
    let rnum: String           // 순번
    let rank: String           // 해당일자 순위
    let rankInten: String      // 전일 대비 순위 변동분
    let rankOldAndNew: String  // 신규진입여부 (NEW / OLD)
    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        // 해당일 상영횟수
}

숫자 필드를 적절한 타입으로 변환하고 싶다면 아래처럼 Int / Double로 선언하고 CodingKeys로 매핑할 수 있습니다.

 
 
swift
// MARK: - DailyBoxOffice (타입 변환 버전)
struct DailyBoxOffice: Codable {
    let rnum: String
    let rank: Int
    let rankInten: Int         // 음수 가능
    let rankOldAndNew: RankStatus
    let movieCd: String
    let movieNm: String
    let openDt: String
    let salesAmt: Int
    let salesShare: Double
    let salesInten: Int        // 음수 가능
    let salesChange: Double    // 음수 가능
    let salesAcc: Int
    let audiCnt: Int
    let audiInten: Int         // 음수 가능
    let audiChange: Double     // 음수 가능
    let audiAcc: Int
    let scrnCnt: Int
    let showCnt: Int
}

// MARK: - RankStatus
enum RankStatus: String, Codable {
    case new = "NEW"
    case old = "OLD"
}

사용 예시

 
 
swift
let jsonData = json.data(using: .utf8)!
let decoder = JSONDecoder()

do {
    let result = try decoder.decode(BoxOfficeResponse.self, from: jsonData)
    let list = result.boxOfficeResult.dailyBoxOfficeList
    list.forEach { print("\($0.rank)위: \($0.movieNm)") }
} catch {
    print("Decoding error: \(error)")
}

JSON의 모든 숫자값이 String 형태로 전달되고 있어서, 서버 스펙이 확실하다면 타입 변환 버전을 쓰되 String으로 디코딩 후 직접 변환하는 방법도 안전합니다.

 

// 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: Codable {
    let boxOfficeResult: BoxOfficeResult
}

// MARK: - BoxOfficeResult
struct BoxOfficeResult: Codable {
    let boxofficeType, showRange: String
    let dailyBoxOfficeList: [DailyBoxOfficeList]
}

// MARK: - DailyBoxOfficeList
struct DailyBoxOfficeList: Codable {
    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 CodingKeys: String, CodingKey {
        case rnum, rank, rankInten, rankOldAndNew
        case movieCD = "movieCd"
        case movieNm, openDt, salesAmt, salesShare, salesInten, salesChange, salesAcc, audiCnt, audiInten, audiChange, audiAcc, scrnCnt, showCnt
    }
}

enum RankOldAndNew: String, Codable {
    case old = "OLD"
}

 

Kotlin (Data Class)

 
 
kotlin
import com.google.gson.annotations.SerializedName

// Root
data class BoxOfficeResponse(
    @SerializedName("boxOfficeResult")
    val boxOfficeResult: BoxOfficeResult
)

// BoxOfficeResult
data class BoxOfficeResult(
    @SerializedName("boxofficeType")
    val boxofficeType: String,
    @SerializedName("showRange")
    val showRange: String,
    @SerializedName("dailyBoxOfficeList")
    val dailyBoxOfficeList: List<DailyBoxOffice>
)

// DailyBoxOffice
data class DailyBoxOffice(
    @SerializedName("rnum")
    val rnum: String,           // 순번
    @SerializedName("rank")
    val rank: String,           // 순위
    @SerializedName("rankInten")
    val rankInten: String,      // 순위 변동분
    @SerializedName("rankOldAndNew")
    val rankOldAndNew: String,  // 신규진입여부 (NEW / OLD)
    @SerializedName("movieCd")
    val movieCd: String,        // 영화코드
    @SerializedName("movieNm")
    val movieNm: String,        // 영화명
    @SerializedName("openDt")
    val openDt: String,         // 개봉일
    @SerializedName("salesAmt")
    val salesAmt: String,       // 매출액
    @SerializedName("salesShare")
    val salesShare: String,     // 매출 점유율
    @SerializedName("salesInten")
    val salesInten: String,     // 매출액 증감분
    @SerializedName("salesChange")
    val salesChange: String,    // 매출액 증감 비율
    @SerializedName("salesAcc")
    val salesAcc: String,       // 누적 매출액
    @SerializedName("audiCnt")
    val audiCnt: String,        // 관객수
    @SerializedName("audiInten")
    val audiInten: String,      // 관객수 증감분
    @SerializedName("audiChange")
    val audiChange: String,     // 관객수 증감 비율
    @SerializedName("audiAcc")
    val audiAcc: String,        // 누적 관객수
    @SerializedName("scrnCnt")
    val scrnCnt: String,        // 스크린수
    @SerializedName("showCnt")
    val showCnt: String         // 상영횟수
)

// 사용 예시 (Gson)
// val response = Gson().fromJson(jsonString, BoxOfficeResponse::class.java)
// val list = response.boxOfficeResult.dailyBoxOfficeList
// list.forEach { println("${it.rank}위: ${it.movieNm}") }

Java (POJO + Gson)

 
 
java
import com.google.gson.annotations.SerializedName;
import java.util.List;

// Root
public class BoxOfficeResponse {
    @SerializedName("boxOfficeResult")
    private BoxOfficeResult boxOfficeResult;

    public BoxOfficeResult getBoxOfficeResult() { return boxOfficeResult; }
    public void setBoxOfficeResult(BoxOfficeResult boxOfficeResult) {
        this.boxOfficeResult = boxOfficeResult;
    }
}

// BoxOfficeResult
public class BoxOfficeResult {
    @SerializedName("boxofficeType")
    private String boxofficeType;

    @SerializedName("showRange")
    private String showRange;

    @SerializedName("dailyBoxOfficeList")
    private List<DailyBoxOffice> dailyBoxOfficeList;

    public String getBoxofficeType() { return boxofficeType; }
    public void setBoxofficeType(String boxofficeType) { this.boxofficeType = boxofficeType; }

    public String getShowRange() { return showRange; }
    public void setShowRange(String showRange) { this.showRange = showRange; }

    public List<DailyBoxOffice> getDailyBoxOfficeList() { return dailyBoxOfficeList; }
    public void setDailyBoxOfficeList(List<DailyBoxOffice> list) { this.dailyBoxOfficeList = list; }
}

// DailyBoxOffice
public class DailyBoxOffice {
    @SerializedName("rnum")
    private String rnum;        // 순번

    @SerializedName("rank")
    private String rank;        // 순위

    @SerializedName("rankInten")
    private String rankInten;   // 순위 변동분

    @SerializedName("rankOldAndNew")
    private String rankOldAndNew; // 신규진입여부 (NEW / OLD)

    @SerializedName("movieCd")
    private String movieCd;     // 영화코드

    @SerializedName("movieNm")
    private String movieNm;     // 영화명

    @SerializedName("openDt")
    private String openDt;      // 개봉일

    @SerializedName("salesAmt")
    private String salesAmt;    // 매출액

    @SerializedName("salesShare")
    private String salesShare;  // 매출 점유율

    @SerializedName("salesInten")
    private String salesInten;  // 매출액 증감분

    @SerializedName("salesChange")
    private String salesChange; // 매출액 증감 비율

    @SerializedName("salesAcc")
    private String salesAcc;    // 누적 매출액

    @SerializedName("audiCnt")
    private String audiCnt;     // 관객수

    @SerializedName("audiInten")
    private String audiInten;   // 관객수 증감분

    @SerializedName("audiChange")
    private String audiChange;  // 관객수 증감 비율

    @SerializedName("audiAcc")
    private String audiAcc;     // 누적 관객수

    @SerializedName("scrnCnt")
    private String scrnCnt;     // 스크린수

    @SerializedName("showCnt")
    private String showCnt;     // 상영횟수

    // Getters & Setters
    public String getRnum() { return rnum; }
    public void setRnum(String rnum) { this.rnum = rnum; }

    public String getRank() { return rank; }
    public void setRank(String rank) { this.rank = rank; }

    public String getRankInten() { return rankInten; }
    public void setRankInten(String rankInten) { this.rankInten = rankInten; }

    public String getRankOldAndNew() { return rankOldAndNew; }
    public void setRankOldAndNew(String rankOldAndNew) { this.rankOldAndNew = rankOldAndNew; }

    public String getMovieCd() { return movieCd; }
    public void setMovieCd(String movieCd) { this.movieCd = movieCd; }

    public String getMovieNm() { return movieNm; }
    public void setMovieNm(String movieNm) { this.movieNm = movieNm; }

    public String getOpenDt() { return openDt; }
    public void setOpenDt(String openDt) { this.openDt = openDt; }

    public String getSalesAmt() { return salesAmt; }
    public void setSalesAmt(String salesAmt) { this.salesAmt = salesAmt; }

    public String getSalesShare() { return salesShare; }
    public void setSalesShare(String salesShare) { this.salesShare = salesShare; }

    public String getSalesInten() { return salesInten; }
    public void setSalesInten(String salesInten) { this.salesInten = salesInten; }

    public String getSalesChange() { return salesChange; }
    public void setSalesChange(String salesChange) { this.salesChange = salesChange; }

    public String getSalesAcc() { return salesAcc; }
    public void setSalesAcc(String salesAcc) { this.salesAcc = salesAcc; }

    public String getAudiCnt() { return audiCnt; }
    public void setAudiCnt(String audiCnt) { this.audiCnt = audiCnt; }

    public String getAudiInten() { return audiInten; }
    public void setAudiInten(String audiInten) { this.audiInten = audiInten; }

    public String getAudiChange() { return audiChange; }
    public void setAudiChange(String audiChange) { this.audiChange = audiChange; }

    public String getAudiAcc() { return audiAcc; }
    public void setAudiAcc(String audiAcc) { this.audiAcc = audiAcc; }

    public String getScrnCnt() { return scrnCnt; }
    public void setScrnCnt(String scrnCnt) { this.scrnCnt = scrnCnt; }

    public String getShowCnt() { return showCnt; }
    public void setShowCnt(String showCnt) { this.showCnt = showCnt; }
}

// 사용 예시
// BoxOfficeResponse response = new Gson().fromJson(jsonString, BoxOfficeResponse.class);
// List<DailyBoxOffice> list = response.getBoxOfficeResult().getDailyBoxOfficeList();
// for (DailyBoxOffice movie : list) {
//     System.out.println(movie.getRank() + "위: " + movie.getMovieNm());
// }

Python (dataclass + dacite)

 
 
python
from dataclasses import dataclass
from typing import List
import json

@dataclass
class DailyBoxOffice:
    rnum: str           # 순번
    rank: str           # 순위
    rankInten: str      # 순위 변동분
    rankOldAndNew: str  # 신규진입여부 (NEW / OLD)
    movieCd: str        # 영화코드
    movieNm: str        # 영화명
    openDt: str         # 개봉일
    salesAmt: str       # 매출액
    salesShare: str     # 매출 점유율
    salesInten: str     # 매출액 증감분
    salesChange: str    # 매출액 증감 비율
    salesAcc: str       # 누적 매출액
    audiCnt: str        # 관객수
    audiInten: str      # 관객수 증감분
    audiChange: str     # 관객수 증감 비율
    audiAcc: str        # 누적 관객수
    scrnCnt: str        # 스크린수
    showCnt: str        # 상영횟수

@dataclass
class BoxOfficeResult:
    boxofficeType: str
    showRange: str
    dailyBoxOfficeList: List[DailyBoxOffice]

@dataclass
class BoxOfficeResponse:
    boxOfficeResult: BoxOfficeResult


# 사용 예시 (dacite 사용)
# pip install dacite
from dacite import from_dict

with open("boxoffice.json", encoding="utf-8") as f:
    raw = json.load(f)

response = from_dict(data_class=BoxOfficeResponse, data=raw)

for movie in response.boxOfficeResult.dailyBoxOfficeList:
    print(f"{movie.rank}위: {movie.movieNm}")


# dacite 없이 순수하게 파싱하려면:
def parse_response(data: dict) -> BoxOfficeResponse:
    result = data["boxOfficeResult"]
    movies = [DailyBoxOffice(**item) for item in result["dailyBoxOfficeList"]]
    box_result = BoxOfficeResult(
        boxofficeType=result["boxofficeType"],
        showRange=result["showRange"],
        dailyBoxOfficeList=movies
    )
    return BoxOfficeResponse(boxOfficeResult=box_result)

언어별 핵심 비교

항목KotlinJavaPython
모델 형태 data class POJO @dataclass
JSON 직렬화 Gson / Moshi Gson / Jackson json + dacite
보일러플레이트 최소 많음 적음
Null 안전성 컴파일 타임 런타임 런타임

 

보통 url은 명사만 넣는다. restful은 주소와 명사를 철저하게 분리한다. url은 말그대로 어디에있는 머 이런식으로 된다. rest의 규칙을 최대한 잘 지켜서 만든 서비스라고 불린다.
파싱은 xml데이터를 추출하여 분석한다.
여러개의 객체들을 나열할때는 대괄호를 해서 배열처럼 표현한다. xml보다는 훨씬더 깔끔하다.
데이터를파씽한다고 한다. 필요한 데이터를 얻을때 이 슬라이드는 JSON 데이터를 Swift 구조체로 모델링하는 방법을 설명하는 강의 자료입니다. 슬라이드 구성 설명 📌 왼쪽 - JSON 원본 데이터 실제 박스오피스 API의 JSON 응답 데이터입니다. ① boxOfficeResult → 최상위 객체 ② dailyBoxOfficeList → 배열 ([ ]) ③ movieNm, audiCnt → 배열 안 각 항목의 필드 📌 가운데 - JSON Viewer (트리 구조) codebeautify.org/jsonviewer 사이트에서 JSON을 트리 형태로 시각화한 모습입니다. ① boxOfficeResult 객체 안에 ② dailyBoxOfficeList 배열 (10개 항목)이 있고 ③ 각 항목 안에 movieNm 등 필드가 있음 📌 오른쪽 - Swift 구조체 코드 JSON 구조를 그대로 Swift struct로 매핑한 모습입니다. swiftstruct MovieData: Codable { // ① 최상위 let boxOfficeResult: BoxOfficeResult } struct BoxOfficeResult: Codable { // ② 중간 객체 let dailyBoxOfficeList: [DailyBoxOfficeList] } struct DailyBoxOfficeList: Codable { // ③ 배열 안 항목 let movieNm: String let audiCnt: String } 📌 하단 - 데이터 접근 방법 swiftdecodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm 디코딩된 데이터에서 첫 번째 영화의 이름에 접근하는 방법 핵심 메시지 요약 JSON 구조Swift 타입{ } 객체struct[ ] 배열[타입]문자열 값String전체 구조중첩 struct로 계층 표현 JSON의 계층 구조(depth)를 그대로 Swift struct의 중첩 구조로 옮기는 것이 핵심입니다.

 

테이블뷰로 뷰를 완전히 화면 가득히 다 덮혔다. 이렇게 하는것을 컨스트레인트를 만든다.
보통 컨스트레인토 마진스를 체크를 하고 많이 쓴다. 그러면 딱 예쁘게 사각형이 채워져서이다.
기말에 나온다. dequeueReusableCell은 리턴값이 UITableViewCell형이다.
indexPath.description하면은 이렇게 각각의 배열식으로 문자열이 나온다.

//

//  ViewController.swift

//  MovieLJY

//

//  Created by comsoft on 2026/05/04.

//

 

import UIKit

let name = ["1:슈퍼 마리오 갤럭시"

            ,"2:악마는 프라다를 입는다 2"

            ,"3:살목지"

            ,"4:프로젝트 헤일메리"

            ,"5:짱구"

            ,"6:왕과 사는 남자"

            ,"7:란 12.3"

            ,"8:내 이름은"

            ,"9:사랑의 하츄핑 특별판"

            ,"10:극장판 반짝반짝 달님이: 싱어롱 파티"]

class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {

    

    

 

    @IBOutlet weak var table: UITableView!

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        return 10

    }

    

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell

        cell.moiveName.text = name[indexPath.row]

        print(indexPath.description)

        return cell

    }

    func numberOfSections(in tableView: UITableView) -> Int {

            return 5

    }

    override func viewDidLoad() {

        super.viewDidLoad()

        table.delegate = self

        table.dataSource = self

    }

 

 

}

 

ViewController.swift 코드 설명


전체 구조 한눈에 보기

 
 
ViewController
├── UITableViewDelegate   (테이블 동작/이벤트 처리)
└── UITableViewDataSource (테이블 데이터 제공)

1. 전역 데이터

 
 
swift
let name = ["1:슈퍼 마리오 갤럭시", "2:악마는 프라다를 입는다 2", ...]
  • 영화 이름 10개를 담은 문자열 배열
  • class 밖에 선언된 전역 상수라 앱 어디서든 접근 가능

2. Protocol 채택

 
 
swift
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource
프로토콜역할
UIViewController 화면(View) 기본 기능
UITableViewDelegate 셀 선택, 높이 등 동작 처리
UITableViewDataSource 셀 개수, 셀 내용 등 데이터 제공

3. IBOutlet

 
 
swift
@IBOutlet weak var table: UITableView!
  • 스토리보드의 TableView와 코드를 연결한 변수
  • weak → 메모리 순환 참조 방지

4. DataSource 메서드

섹션 수

 
 
swift
func numberOfSections(in tableView: UITableView) -> Int {
    return 5  // 섹션 5개
}

행(Row) 수

 
 
swift
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 10  // 각 섹션마다 10행
}

⚠️ 주의: 섹션 5개 × 행 10개 = 총 50개 셀이 생성되는데,
name 배열은 10개뿐이라 indexPath.row가 0~9를 반복하므로
의도한 동작인지 확인이 필요합니다.

셀 구성

 
 
swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
    cell.moiveName.text = name[indexPath.row]
    print(indexPath.description)
    return cell
}
코드설명
dequeueReusableCell 셀을 재사용해서 메모리 절약
"myCell" 스토리보드에서 지정한 셀 Identifier
as! MyTableViewCell 커스텀 셀 클래스로 강제 캐스팅
cell.moiveName.text 셀 안의 Label에 영화 이름 표시
print(indexPath.description) 몇 번째 섹션, 몇 번째 행인지 콘솔 출력

5. viewDidLoad

 
 
swift
override func viewDidLoad() {
    super.viewDidLoad()
    table.delegate = self
    table.dataSource = self
}
  • 화면이 메모리에 로드될 때 최초 1회 실행
  • 테이블뷰의 delegate와 dataSource를 자기 자신(ViewController) 으로 지정

⚠️ 개선이 필요한 부분

 
 
swift
// 1. name 배열 크기와 행 수를 하드코딩 대신 연동 권장
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return name.count  // 10 대신 name.count 사용
}

// 2. 오타 수정
cell.moiveName.text  →  cell.movieName.text  // "moive" → "movie"

// 3. 섹션이 5개면 각 섹션별로 다른 데이터를 보여줄 의도인지 확인 필요