The Hobbyist Programmer
Posts about Swift, Python, and life, by Hyeonmin

SwiftUI로 코로나 현황 앱 만들기 Part 1 - 데이터 모델

February 9, 2022

이 글에 사용된 버전: Xcode 13.3, Swift 5.6

2022-04-24: API 수정

Swift 관련 글 첫 번째로 앱을 처음부터 끝까지 만들어보는 튜토리얼 시리즈를 시작하려고 한다. 내가 코딩을 처음 배울 때를 생각해 보면 문법을 공부하기보다는 잘 모르더라도 무작정 따라서 실제로 앱을 만들어보는 것이 더 좋았다. 계속 공부하고, 배우기 위해서는 흥미와 재미가 중요하다고 생각하기에, 내가 재미를 느꼈던 방식대로 풀어가겠다. 그럼 Xcode를 실행하고 시작해 보자.

새로운 프로젝트 생성

NewProject

Xcode를 실행하면 나오는 창에서 Create a new Xcode project를 선택한다. 또는 메뉴에서 File > New > Project를 선택해도 된다.

ChooseAppTemplate

앱의 템플릿을 선택하는 창이 나오면 iOS > App을 선택한다. 이 템플릿이 가장 기본이 되는 템플릿으로 앞으로 새로운 프로젝트를 생성할 때 가장 많이 선택하게 되는 템플릿이다.

ChooseSwiftUI

Product Name에 COVIDNumbers라고 이름을 지어주고, Interface는 SwiftUI, Language는 Swift가 맞는지 반드시 확인하자.

지금까지 해 온 이렇게 간단한 과정만으로도 벌써 실행 가능한 앱이 완성된다.

HelloWorld

상단 중앙에서 시뮬레이터 종류를 iPhone 13 Pro Max(사용하고 싶은 다른 종류의 시뮬레이터를 선택해도 무방하다)를 선택하고 왼쪽의 플레이 버튼 모양으로 생긴 Run 버튼을 누르면(단축키 command + R) 시뮬레이터가 실행되며 프로그래머의 영원한 시작점, Hello, world!가 화면에 표시된 아무 기능 없는 앱이 실행된다.

Simulator

Hello, world!

예전에는 개발하는 단계에서 작성한 코드가 UI로 어떻게 구현되는지 확인하려면 이렇게 시뮬레이터를 실행해야만 했지만, SwiftUI가 등장한 이후로는 시뮬레이터를 실행하지 않고도 실시간 프리뷰로 확인할 수 있게 되어서 UI 확인에 소요되는 시간이 많이 단축되었다.

Preview

오른쪽에 표시한 영역이 바로 그 프리뷰 영역이고, Resume 버튼을 누르면 프리뷰가 표시된다. 혹시 프리뷰 영역이 보이지 않는다면 option + command + return 단축키로 표시할 수 있다.

PreviewDone

프로젝트 생성이 완료되었으면 이제 이 글의 주제인 데이터 모델에 대해 알아보자.

데이터 모델(Data Model)

데이터 모델?

데이터 모델이란 앱에서 사용하는 데이터의 구조를 체계적으로 추상화하여 표현한 것을 말한다. 이렇게 써놓으니 내가 써놓고도 잘 모르겠다. 데이터의 구조 그 자체이자 설계도 같은 건데 예를 들어서 설명해 보자.

자동차를 데이터로 나타내고자 한다면 자동차들이 공통적으로 가지고 있는 특성으로 데이터를 구성하게 될 것이다. 이를테면 자동차의 제조사, 모델명, 연식, 차대번호, 색상, 배기량, 연비 등등이 되겠다. 특히 차대번호는 겹치는 일 없는 유니크한 특성이라 ID로 사용할 수도 있다. 데이터 모델에서 ID라는 것은 생각보다 중요한데 이건 차차 보게 될 것이다.

어떤 앱을 개발하느냐에 따라서 데이터 모델이 하나가 될지 여러 개가 될지가 결정된다. 화면이 하나 또는 둘 정도에 불과한 아주 단순한 앱이라면 단 하나의 데이터 모델로도 충분할 것이다. 하지만 보통의 앱들은 여러 개의 데이터 모델을 가진다. 이 앱에서도 수 개의 데이터 모델을 정의할 예정이다.

NovelCOVID API

아무튼 데이터 모델을 정의하기 위해서는 우선 우리가 사용할 데이터의 구조를 파악해 봐야 한다. 우리가 사용할 데이터는 NovelCOVID API에서 제공하는 데이터다. NovelCOVID API는 오픈소스 프로젝트이고, 별도의 API 키 없이 사용할 수 있다. 아래 링크에서 확인해 보자.

NovelCOVID API

우선 우리가 사용할 데이터는 확진자 수 등을 전 세계 데이터로 한 번에 보여주는 데이터이다. 좀 아래쪽을 보면 GET v3/covid-19/all이라는 소제목이 부여된 파트가 우리가 사용할 API이다. 이곳에서 아래와 같은 URL을 발견할 수 있다.

https://disease.sh/v3/covid-19/all?yesterday=true

URL이나 GET request에 대해서는 이후에 있을 네트워킹 편에서 따로 설명하겠다. 우선은 해당 주소를 사파리나 크롬과 같은 일반적으로 사용하는 웹브라우저에 복사해서 붙여 넣으면 아래와 같이 나온다.

Browser Result

뭔가 잘못된 것처럼 보이지만 제대로 된 것이 맞다. 해당 API를 통해서 온 결과는 JSON(JavaScript Object Notation)이라는 파일 포맷이다. JSON은 iOS 앱 개발에서 네트워크를 통해 데이터를 주고받을 때 사용하는 가장 흔한 포맷이다. 지금은 웹브라우저에서 포맷을 제대로 보여주지 않아서 저렇게 나올 뿐, 해당 주소를 다시 Postman 같은 앱에서 넣어보면 다음처럼 나온다.

Postman

더 잘 보이게 결과 부분만 가져오면 이렇게 된다.

{
    "updated": 1644204519242,
    "cases": 395882670,
    "todayCases": 77557,
    "deaths": 5758593,
    "todayDeaths": 200,
    "recovered": 314743461,
    "todayRecovered": 131515,
    "active": 75380616,
    "critical": 90593,
    "casesPerOneMillion": 50788,
    "deathsPerOneMillion": 738.8,
    "tests": 5192756470,
    "testsPerOneMillion": 658980.49,
    "population": 7879985120,
    "oneCasePerPeople": 0,
    "oneDeathPerPeople": 0,
    "oneTestPerPeople": 0,
    "activePerOneMillion": 9566.09,
    "recoveredPerOneMillion": 39942.14,
    "criticalPerOneMillion": 11.5,
    "affectedCountries": 225
}

cases는 누적 확진자 수, todayCases는 today 지만 데이터가 어제 기준이므로 어제 하루 확진자 수를 의미한다. API 요청이 2022년 2월 7일이니까, 2월 6일 기준으로 전 세계 확진자 수가 358,882,670, 약 3억5천9백만 명에 이른다.

이제 우리가 다룰 데이터 형태를 알았으니 데이터 모델을 정의할 때가 왔다.

데이터 모델 정의하기

이 API에서 제공하는 데이터가 많이 있지만, 우리는 저 정보를 다 사용하지는 않고 필요한 정보만 추출하여 아래 정보만을 사용하려고 한다.

구분설명
updated데이터 최종 업데이트 시각
cases누적 확진자 수
todayCases당일 확진자 수
deaths누적 사망자 수
todayDeaths당일 사망자 수
recovered누적 완치자 수
todayRecovered당일 완치자 수
active환자 수
population인구
affectedCountries영향을 받은 국가 수

이제 이 정보를 이용하여 실제 데이터 모델을 만들어보자. Xcode 왼편의 Project navigator에서 COVIDNumbers 폴더를 우클릭하여 New group을 클릭하여 새로운 폴더(그룹)를 만들고 폴더 이름을 Models라고 바꿔 준다. 그런 다음, Models 폴더를 다시 우클릭하여 New file을 선택하여 새로운 파일을 추가한다.

New File

iOS > Swift File > Next 순으로 진행하면 새로운 창이 뜬다.

New File 2

파일 이름을 Worldwide라고 지정해 주고, Where, Group, Target이 모두 일치하는지 확인 후 Create를 선택한다. 파일 추가를 제대로 진행했다면 아래와 같은 모습이 된다.

New File 3

프로젝트 내 파일은 관리가 용이하도록 논리적인 분류로 별도 폴더를 만들어 잘 정돈해 두는 것이 좋다. 프로젝트에 따라 몇 안 되는 파일만을 사용할 수도 있지만, 어마어마하게 많은 파일을 다뤄야 하는 경우도 있다. 이런 경우 파일 관리가 제대로 되어 있지 않다면 코드를 유지하는 데만도 많은 자원이 소모된다. 특정한 관리 방법이 정해져 있는 것은 아니며, 실제 사용하는 팀이나 개인, 그리고 Swift를 사용하는 커뮤니티에서 다른 사람들이 쉽게 알아볼 수 있는 방식이면 충분하다.

이제 해당 파일을 열고, 에디터에서 아래와 같이 코드를 작성한다.

// Worldwide.swift

import Foundation

struct Worldwide {
    let updated: Int
    let cases: Int
    let todayCases: Int
    let deaths: Int
    let todayDeaths: Int
    let recovered: Int
    let todayRecovered: Int
    let active: Int
    let population: Int
    let affectedCountries: Int
}

우선 struct에 대해 알아보자. Swift에서 커스텀 타입(앱 내에서 사용할 데이터의 형태)을 정의하는 방법은 세 가지가 있다. struct, class, enum인데 이 중 데이터 모델을 정의하는 방법은 structclass가 쓰인다. struct는 value 형식이고 class는 레퍼런스 형식이어서 이 특징을 잘 이해하고 자신의 앱에 어떤 방식이 나을지 고려하여 사용해야 한다. 개발에서는 결과에 이르는 길이 한 가지만 있지는 않다. 여러 가지 방법 중 가장 적합한 방법을 선택해야 하고, 그렇게 결정한 사유를 잘 이해하는 것이 중요하다. 나는 이 모델의 경우 value 타입이 더 적정하여 struct로 정의했고, 전 세계 데이터를 보여줄 것이므로 타입 이름은 Worldwide라고 붙였다. struct vs class에 대해서는 별도의 포스트를 통해 다뤄 볼 예정이다.

let 키워드의 경우 값이 변하지 않는 상수(constant)를 선언하는 키워드이다. 값이 변하는 변수(variable)를 선언할 때는 var를 사용한다. 이처럼 struct 같은 타입 내에서 선언된 상수나 변수를 해당 타입의 property라고 부르고, 여기서 사용되지는 않았지만 타입 내에서 정의된 함수(func 키워드)는 method라고 부른다. 이 앱에서는 API를 통해 데이트를 다운로드해 우리가 사용하는 타입으로 디코딩 한 후 데이터를 변경할 일은 없으므로 let을 사용했다. 값이 변경되지 않을 것이라면 var보다는 let을 사용하는 것이 앱을 최적화하는 방법이다.

Int는 Swift에 내장된 타입으로 소수점이 없는 수, 즉 정수(integer) 값을 나타내는 데 쓰인다. 소수점이 있는 값이라면 Double을 주로 사용한다. 아까 API를 통해서 온 결과를 보면 우리가 사용할 데이터들은 모두 소수점이 없는 것을 확인할 수 있으므로 Int를 사용해도 안전하다.

Swift에서는 다음과 같은 형태로 변수나 상수를 선언한다.

let someNumber: Int = 5

해당 구문은 Int 타입의 데이터를 저장하는 someNumber라는 이름의 상수가 만들어졌고 값은 5가 할당되었다는 의미를 나타낸다. Swift에는 타입을 알아서 추론하는 type inference 기능이 있기 때문에 여기서 Int는 생략할 수 있고, 생략되더라도 여전히 Int로 인식한다. 따라서 아래 구문은 위 구문과 동일한 의미가 된다.

let someNumber = 5

아래의 경우는 type inference에 의해 Double이 된다.

let pi = 3.14

우리가 만든 모델에서는 초깃값이 부여되지 않았는데, 이처럼 초깃값을 부여하지 않을 때는 : Int 같은 형태로 타입을 꼭 정의해야 한다. 그리고 여기서 초깃값이 부여되지 않은 것은 Worldwide initializing(초기화라는 번역이 와닿지 않는데 해당 타입의 인스턴스를 처음 만든다는 것을 의미하고, 인스턴스는 해당 타입을 통해 만들어진 개별 데이터를 의미한다) 과정을 통해 부여되기 때문이다.

실제로 initializing하여 새로운 인스턴스를 만드는 구문은 아래와 같다. 아래에서는 property 값을 모두 0으로 할당하여 인스턴스를 생성하였다.

let instanceOne = Worldwide(updated: 0, cases: 0, todayCases: 0, deaths: 0, todayDeaths: 0, recovered: 0, todayRecovered: 0, active: 0, population: 0, affectedCountries: 0)
let instanceTwo = Worldwide(updated: 0, cases: 0, todayCases: 0, deaths: 0, todayDeaths: 0, recovered: 0, todayRecovered: 0, active: 0, population: 0, affectedCountries: 0)

참고로 변수명 등에서 여러 단어로 구성된 이름을 사용할 때 코딩의 세계에서는 띄어쓰기를 사용할 수 없다. Swift에서는 띄어쓰기 대신 여러 단어로 된 이름을 사용할 때 알아보기 쉽게 camelCase를 사용하는 것이 관례이다. camelCase는 새로운 단어가 시작되는 부분을 대문자로 표기하는 방식으로 중간에 대문자가 낙타의 혹처럼 보이는 것에서 유래된 이름이다. structclass 같은 타입 이름에는 첫 문자를 대문자로 시작하는 UpperCamelCase를 사용하고, property 선언이나 함수를 정의할 때는 첫 문자를 소문자로 시작하는 lowerCamelCase를 사용한다.

camelCase와 유사한 방법으로 띄어쓰기를 언더스코어(_)로 표기하는 snake_case 방법도 있다. 예를 들면, new_name 이런 식으로 사용한다. Python에서는 snake case가 일반적으로 통용되는 방법인데 개인적으로는 snake case는 언더스코어로 인해 전체 길이가 길어진다는 문제와 키를 한 번 더 입력해야 된다는 문제로 인해 camelCase를 더 선호한다. 하지만 각 커뮤니티에서 통용되는 방법을 사용하는 것이 협업을 위해서는 좋다.

여기서 Part 1을 마친다. 우리는 지금까지...

에 대해 다루었다. Part 2에서는 네트워킹 코드를 작성하여 앱에서 인터넷을 통해 NovelCOVID API에서 실제로 데이터를 다운로드해 보겠다.