본문 바로가기
개발(Development)/iOS(아이폰)

[한글 번역_07] Start Developing iOS Apps (Swift) - Building the UI > Define Your Data Model

by 카레유 2015. 10. 30.
안녕하세요 카레유입니다.

Apple에서 제공해주는 Swift iOS 앱 개발 가이드를 한글 번역해보았습니다.

영어실력도, 개발실력도, 심지어 한국어 실력도 미흡합니다.

부족한 부분이 많지만, 많은 도움 되시길 바라겠습니다.

아직 작성 중인 내용으로 시간을 갖고 개선해 나갈 생각입니다.

감사합니다.




Define Your Data Model

이번 레슨에서는 FoodTracker앱의 데이터 모델(data model)을 정의하고 테스트해보겠습니다. 데이터 모델이란 앱 내부에서 사용하는 데이터 구조(structure of information)를 의미합니다. 그리고 이 데이터 구조를 관리하고, 뷰를 통해 보여주는 객체를 데이터소스(data source)라고 합니다.

Learning Objectives

At the end of the lesson, you’ll be able to:

  • 데이터모델(data model)을 만들 수 있습니다.

  • 커스텀 클래스에 실패 가능한 타입의 초기화 메서드(failable initializer)를 작성할 수 있습니다.

  • failable / nonfailable initializer의 개념 차이를 이해할 수 있습니다.

  • 유닛 테스트(unit test)를 통해 데이터 모델을 테스트할 수 있습니다.

Create a Data Model

meal scene에서 이름, 사진, 평가에 대한 정보를 보여주기 위해서는 해당 데이터를 저장하고 있어야 합니다. 해당 데이터를 저장하기 위해 필요한 것이 바로 데이터 모델입니다. 데이터 모델을 만들기 위해서는 데이터(name, photo, rating)를 저장할 프로퍼티로 구성된 클래스가 필요합니다.

To create a new data model class

  1. File > New > File 을 클릭하세요(단축키로 Command - N 을 누르셔도 됩니다)

  2. 다이얼로그의 왼쪽에서 iOS를 선택하세요

  3. 오른쪽에서 Swift File을 선택하고 Next를 클릭하세요

    이전과는 다르게 Cocoa Touch 클래스를 만들지 않았습니다. data model을 정의하기 위해서는  기본 클래스(base class)만으로도 충분합니다. 다른 클래스를 상속받을 필요가 없습니다. 

  4. Save As 필드에 Meal이라고 입력하세요

  5. save location은 현데 프로젝트 디렉토리가 디폴트입니다. 

    Group옵션도 현재 앱의 이름이 디폴트입니다. 

    Target 섹션에서는 FoodTraker 앱과 FoodTrackerTests 가 둘다 선택되게 해주세요

    image
  6. Click Create. - Create를 클릭해주세요

    Meal.swift 파일이 생성됩니다.

음식의 이름을 저장할 변수를 선언해야합니다. 이름을 저장할 name변수는 String타입으로, 사진을 저장할 photo변수는 UIImage타입으로, 평가점수를 저장할 rating 변수는 Int타입으로 선언하면 됩니다. 음식 정보에 이름과 평가는 빠져서는 안되는 정보입니다. 하지만 사진은 생략될 수도 있습니다. 따라서 UIImage 타입으로 선언하는 photo변수는 optional로 지정해야합니다. optional은 해당 변수에 값이 없을 수도 있다(nil)라는 것을 명시해주는 것입니다. 일반적인 프로그래밍 언어에서는 값이 없는 경우 예외(Exception)가 발생하지만, Swift에서는 optional을 이용해서 값이 없는 경우에 대해 적절한 처리를 해줄 수가 있습니다.

To define a data model for a meal

  1. assistant editor가 열려있다면, 아래처럼 툴바를 통해 Standard editor로 돌아오세요

    image
  2. Meal.swift 파일을 열어주세요

  3. 현재 파일의 상단에서 Foundation프레임워크를 임포트(import)하고 있습니다. 아래와 같이 UIKit을 임포트 하도록 변경해주세요

    1. import UIKit

    디폴트 상태에서 일반적인 Swift파일은 Foundation프레임 워크를 임포트 합니다. Foundation프레임워크를 임포트하면 Foundation에서 제공하는 데이터 구조를 이용하여 작업할 수 있게 됩니다. 하지만 UIKit을 임포트하면 Foundation프레임워크에도 접근할 수 있을 뿐만 아니라, UIKit에서만 제공하는 다른 기능도 사용할 수 있습니다. 따라서 여기서는 UIKit을 임포팅 하여 사용하겠습니다.

  4. 클래스 구현부에 아래와 같이 주석을 작성하고, 데이터를 저장할 프로퍼티들 선언해주세요

    1. class Meal {
    2. // MARK: Properties
    3. var name: String
    4. var photo: UIImage?
    5. var rating: Int
    6. }

    위의 코드는 데이터를 저장하기 위한 기본적인 프로퍼티들을 선언하고 있습니다. Meal 객체가 생성되고 나면 이 프로퍼티들에 접근하여 사용할 수 있게 되는데, 앱을 사용하면서 음식의 이름을 바꾸거나, 사진을 교체하는 등의 작업을 하면 프로퍼티에 저장된 값들이 변경되어 버립니다. 그렇기 때문에 프로퍼티들을 상수(let)가 아닌 변수(var)로 선언한 것입니다.

  5. 프로퍼티를 선언한 코드 아래에 다음과 같이 initializer를 선언해주세요

    1. // MARK: Initialization
    2. init(name: String, photo: UIImage?, rating: Int) {
    3. }

    이니셜라이져(initializer)는 객체를 생성하고, 객체의 프로퍼티 값을 초기화 하는 메서드입니다. 

  6. 파라미터로 들어온 값을 프로퍼티에 할당하도록 이니셜라이저 메서드의 구현부를 작성해주세요

    1. // Initialize stored properties.
    2. self.name = name
    3. self.photo = photo
    4. self.rating = rating

    하지만 Meal객체를 생성할 때, name 파라미터를 비워두거나 rating값을 음수로 넣어주는 등 부적절한 값으로 이니셜라이져 메서드를 호출하는 경우에 대한 처리방식이 필요합니다. 이렇게 부적절한 파라미터로 객체 생성을 시도하는 경우에는 nil을 리턴하고 객체를 생성하지 못하도록 해야합니다. 이 작업 하기 위한 코드를 작성해보겠습니다.

  7. 이니셜라이저 구현부의 맨 마지막 부분에 아래와 같이 if문을 추가하여 부적절한 값이 들어오는 경우를 체크하고, 그런 경우 nil을 리턴하도록 작성해주세요

    1. // Initialization should fail if there is no name or if the rating is negative.
    2. if name.isEmpty || rating < 0 {
    3. return nil
    4. }

    이니셜라이저가 nil을 리턴할 수도 있게 되었습니다. 이 경우 이니셜라이저에 특별한 표시를 해주어야 합니다.

  8. init키워드에 물음표(?)를 붙이라는 fix-it 기능이 작동할 것입니다. 클릭하여 물음표를 붙여주세요

    image
    1. init?(name: String, photo: UIImage?, rating: Int) {

    이렇게 객체생성 및 초기화 작업 수행시 nil을 반환할 수도 있는 이니셜라이져를 물음표(?)를 붙여서 표현하고, failable initializer라고 합니다.

현재까지 완성된 init?(name: photo: rating:) 이니셜라이저의 모습은 아래와 같습니다.

  1. // MARK: Initialization
  2. init?(name: String, photo: UIImage?, rating: Int) {
  3. // Initialize stored properties.
  4. self.name = name
  5. self.photo = photo
  6. self.rating = rating
  7. // Initialization should fail if there is no name or if the rating is negative.
  8. if name.isEmpty || rating < 0 {
  9. return nil
  10. }
  11. }

Checkpoint: Command-B단축키를 이용하여 프로젝트를 빌드해주세요(메뉴바에서 Product > Build 를 이용해도 됩니다). 아직 Meal클래스로 할 수 있는 것은 없지만, 빌드를 해봄으로써 컴파일러가 코드 상에 에러가 없는지를 체크할 수 있게 됩니다. 문제가 있을 경우 컴파일러는 warning이나 error 표시를 하며 절절하게 고쳐주면 됩니다. 만약 빌드를 했는데 문제가 발생했다면, 이 레슨을 처음부터 다시 시작해보시기 바랍니다.

Test Your Data

Although your data model code builds, you haven’t fully incorporated it into your app yet. As a result, it’s hard to tell whether you’ve implemented everything correctly, and if you might encounter edge cases that you haven’t considered at runtime. 데이터모델(data model) 코드를 빌드하긴 했지만 아직 앱에서 작동하지는 않는 상태입니다. 따라서 실제로 모든 코드가 잘 작성 되었는지 확신할 수 없으며, 컴파일 환경이 아닌 실행환경(runtime)에서 예상치 못한 문제가 발생할 가능성을 배제할 수도 없습니다.

To address this uncertainty, you can write unit tests. Unit tests are used for testing small, self-contained pieces of code to make sure they behave correctly. The Meal class is a perfect candidate for unit testing. - 이런 불확실성을 해결하기 위해 유닛테스트(unit test)를 수행해볼 수 있습니다. Unit tests란 작고, 독립적인 코드가 정상적으로 작동하는지 체크하는데 사용됩니다. 우리가 만든 Meal 클래스는 unit test를 해보기에 아주 좋은 대상입니다.

Xcode는 자동으로 Single View Application 템플릿의 일부로 유닛테스트를 생성합니다.

To look at the unit test file for FoodTracker

  1. project navigator에서 "FoodTrackerTests" 폴더를 펼쳐주세요(폴더 옆의 삼각형 클릭)

    image
  2. FoodTackerTest.swift 파일을 열어주세요

이 파일에 있는 코드를 한번 살펴보시기 바랍니다.

  1. import UIKit
  2. import XCTest
  3. class FoodTrackerTests: XCTestCase {
  4. override func setUp() {
  5. super.setUp()
  6. // Put setup code here. This method is called before the invocation of each test method in the class.
  7. }
  8. override func tearDown() {
  9. // Put teardown code here. This method is called after the invocation of each test method in the class.
  10. super.tearDown()
  11. }
  12. func testExample() {
  13. // This is an example of a functional test case.
  14. XCTAssert(true, "Pass")
  15. }
  16. func testPerformanceExample() {
  17. // This is an example of a performance test case.
  18. self.measureBlock() {
  19. // Put the code you want to measure the time of here.
  20. }
  21. }
  22. }

이 파일이 import하고 있는 XCTest 프레임워크는 Xcode의 테스트용 프레임워크입니다. unit tests 는 XCTestCase를 상속한 FoodTrackerTest 클래스에 정의되어 있습니다. setUp()과 tearDown()메서드를 설명하는 주석이 있습니다. 주요 테스트 항목은 functional test(모든 것이 기대한대로 값들을 잘 생성하는지 체크)와 performace test(기대한만큼 빠르게 코드가 작동하는지 체크)입니다. 성능이 문제가 될만한 무거운 코드는 없으므로 지금은 functional test만 진행해보겠습니다. 테스트 하고자 하는 메서드를 골라서 앞에 "test"를 붙여 적절한 이름의 테스트 함수를 선언하세요. Meal클래스가 제대로 초기화되는지 테스트해보기 위해 testMealInitialization 메서드를 만들어보겠습니다.

To write a unit test for Meal object initialization

  1. FoodTrackerTests.swift 파일에서 테스트 템플릿을 모두 지워 비워주세요

    1. import UIKit
    2. import XCTest
    3. class FoodTrackerTests: XCTestCase {
    4. }

    이번 레슨에서는 템플릿 테스트 메서드를 사용할 일이 없습니다.

  2. 마지막 괄호( } ) 안에 아래 주석을 추가해주세요

    1. // MARK: FoodTracker Tests

    주석을 작성하면 누구나 코드를 알아보기 쉽고, 나중에 특정 코드를 찾기도 쉽습니다.

  3. 주석 밑에 아래와 같이 unit test를 만들어봅시다.

    1. // Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
    2. func testMealInitialization() {
    3. }
  4. 먼저 반드시 성공하게 되어있는 테스트 케이스를 추가해봅시다. testMealInitialization()메서드의 주석 밑에 아래의 코드를 작성하세요

    1. // Success case.
    2. let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
    3. XCTAssertNotNil(potentialItem)

    XCTAssertNotNil()메서드는 초기화 작업 결과 Meal 객체의 nil이 아닌지 테스트합니다. 즉, 제대로된 파라미터로 Meal 객체를 초기화가 정상적으로 수행되었는지를 체크합니다.

  5. 이번엔 초기화에 실패할 테스트 케이스를 추가해보겠습니다. testMealInitialization() 메서드에 아래의 주석과 코드를 작성해주세요

    1. // Failure cases.
    2. let noName = Meal(name: "", photo: nil, rating: 0)
    3. XCTAssertNil(noName, "Empty name is invalid")

    XCTAssertNil은 파라미터에 들어온 객체가 nil인지를 체크합니다. 즉, 파라미터로 들어온 noName이 nil이 맞는지를 확인해줍니다. 여기서는 name파라미터가 빈 문자열이므로 초기화는 실패할 것이고, 테스트를 통해 이를 명시적으로 확인할 수 있게 해줍니다.

  6. 이번에는 초기화에 실패하는 케이스를 "성공한다" 고 가정하는 테스트를 해보겠습니다. 아래의 코드를 작성해주세요

    1. let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
    2. XCTAssertNotNil(badRating)

    rating 값이 음수이므로 당연히 초기화에 실패할 것이며, 이를 명시적으로 테스트해볼 수 있습니다.

완성된 testMeaInitialization() unit test는 아래와 같습니다.

  1. // Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
  2. func testMealInitialization() {
  3. // Success case.
  4. let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
  5. XCTAssertNotNil(potentialItem)
  6. // Failure cases.
  7. let noName = Meal(name: "", photo: nil, rating: 0)
  8. XCTAssertNil(noName, "Empty name is invalid")
  9. let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
  10. XCTAssertNotNil(badRating)
  11. }

Command -U 단축키를 눌러서 동시에 모든 unit tes들을 시행해볼 수 있고, 각기 하나씩 개별 테스트 해볼 수 있습니다. 위의 마지막 케이스는 nil인 객체를 넣으면서 non-nil이 확실하냐고 테스트 해봤으므로 fail이 나올 것입니다.

To run the testMealInitialization() unit test

  1. FoodTrackerTests.swift 파일에서 testMealInitailization() 유닛 테스트를 찾으세요

  2. 테스트 이름왼쪽에서 다이아몬드로 표시되는 부분을 찾으세요

    image
  3. 마우스를 그 위에 올려두면 작은 Run 버튼이 나타납니다.

    image
  4. Run버튼을 눌러서 unit test를 실행하세요

Checkpoint앱이 unit test를 실행하면, 처음 두 케이스는 pass 하지만, 마지막 케이스는 fail이 됩니다.

image

위에서 보는바와 같이 unit test는 코드에 이러가 있는지 여부를 체크하기에 좋습니다. 만약 마지막 케이스에서 non-nil이 나올 거라고 예상했다면 테스트를 통해 그게 에러였음을 파악할 수 있습니다. (물론 여기서는 의도적으로 unit test에 fail하게 만들었지만요. 이제 제대로 고쳐봅시다)

To fix the test case

  1. FoodTrackerTest.swift 파일에서 testMealInitialization() 유닛 테스트를 찾으세요

  2. 마지막 코드를 아래처럼 바꾸세요

    1. XCTAssertNil(badRating, "Negative ratings are invalid, be positive")

최종 완성된 testMealIntialization() 유닛 테스트는 아래와 같습니다

  1. // Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
  2. func testMealInitialization() {
  3. // Success case.
  4. let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
  5. XCTAssertNotNil(potentialItem)
  6. // Failure cases.
  7. let noName = Meal(name: "", photo: nil, rating: 0)
  8. XCTAssertNil(noName, "Empty name is invalid")
  9. let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
  10. XCTAssertNil(badRating, "Negative ratings are invalid, be positive")
  11. }

Checkpoint다시 유닛 테스트를 실행(Run)해보세요. 모든 케이스가 pass될 것입니다.

image

unit test는 간과하기 쉬운 에러를 잡아내는데 반드시 필요한 코딩의 영역입니다. unit test라는 이름에서도 알 수 있듯이, unti test는 모듈화해서 사용하는 것이 좋습니다. 각각의 테스트는 기본적이고 구체적인 테스트 하나만을 수행하는게 좋습니다. unit test를 지나치게 길거나 복잡하게 쓰면,  코드의 잘못된 부분을 찾는게 대단히 어려워질겁니다.


댓글