유튜브 채널 iOS Academy에서 발견한 영상. lazy sequence에 관련된 영상이다. 재밌게 시청해서 가져왔다.
lazy
우선 lazy 키워드에 대한 간략한 설명을 하자면 말 그대로 lazy하게 변수를 할당하는 키워드이다. 대다수의 언어가 그렇겠지만 swift에서 변수, 상수의 경우 우선적으로 초기화가 이루어져야지만 컴파일이 완료된다. 하지만 lazy의 경우, 해당 변수의 초기화가 완료되었다고 판단하고 컴파일이 우선적으로 이루어진다. 이후 lazy로 생성된 변수에 접근하게 될 경우에 뒤늦게 초기화가 이루어지게 된다. 해당 부분의 경우 cs193p swift강의를 보면서 처음 마주했는데, 사실 당시에는 필요성이 크게 와닿지 않았다.
lazy sequence
앞선 얘기는 lazy stored property에 관련된 이야기였고 이번글은 lazy sequence에 관련된 이야기를 하려 한다. swift에서 collection 타입의 자료들을 다루게 되면 가장 많이 다루게 되는 부분이 고차함수가 아닐까 싶다. rest-api를 사용할 때 반환된 요청을 map() 혹은 filter()와 같은 고차함수들을 통해 파싱을 하게 되는데, 이때 입력되는 자료의 수가 많다면 성능 저하가 일어나기 쉬울 것이다. 해당 자료들을 모두 사용하게 된다면 어쩔 수 없으나 대다수의 경우 해당 자료에서 일부분의 자료만 뽑아내어 사용자에게 보여주는 일이 많다. 여기서 lazy sequence를 통해 어느 정도의 성능개선을 이루어낼 수 있다.
0부터 1,000,000이 담긴 배열을 생성, map() 메소드를 통해 원소값들을 가공한 뒤 필요한 자료를 호출한 모습이다. 1,000,001 개의 원소를 탐색, 마지막으로 threeTimesArr에 결과를 저장하는 과정이 더해져 1,000,002번의 자료 접근이 일어나게 된다. 곱셈이라는 단순한 작업이었지만 처리해야 할 자료의 수가 많고 더욱 복잡한 자료의 연산이 일어난다면 성능 저하를 유발하게 된다.
이번엔 lazy를 이용한 연산이다. 앞서 말했듯이 lazy는 해당 자료에 접근이 일어날 경우에 초기화가 이루어지게 된다. lazy를 통한 map()메소드는 마찬가지로 자료의 접근이 일어날 때에 필요한 부분에서 연산이 일어나게 되고, 자료를 반환하는 모습을 볼 수 있다. 따라서 arr [50]의 자료접근, threeTimesArr[50]에 자료를 저장하는 과정 총 두 번의 접근을 통해 값을 출력하는 모습을 보여준다. 개발과정에서 이러한 lazy sequence를 통해 성능개선을 이루어낼 수 있지 않을까 싶다.
이상 lazy 키워드를 통한 성능개선에 관련된 이야기였으며 아마도 iOS 개발을 하면서 정말 유용하게 사용할 수 있는 미세먼지팁이 되지 않을까 싶다. 혹시나 틀린 정보를 적었다면 성장중인 아기개발자를 위해 따끔한 지적 부탁드립니다.
코드는 유지보수 및 수정이 용이하도록 작성해야 하는데, MVC 아키텍쳐는 그러한 여러가지 방법들 중 하나이다.
What is MVC?
Model, View, Controller의 약자이다. 즉 하나의 프로젝트를 크게 모델, 뷰, 컨트롤러로 나누어서 작업하는 것을 의미한다.
Model - what your app is
View - contoller's minons
Controller - how model is presented
모델에는 해당 앱이 무엇인지를, 컨트롤러는 해당 모델이 어떻게 보여지는지를 기술하는 것이다. 그렇다면 뷰는? 컨트롤러가 지시하는대로 유저에게 모습을 보여주는 부분이라고 생각하면 된다.
전반적인 통신은 모델과 컨트롤러, 그리고 컨트롤러와 뷰 이렇게 통신을 하게된다. 즉 모델과 뷰는 직접적으로 통신을 할 수 없다는 얘기.
모델은 자신에게 변경사항이 일어나면 컨트롤러에게 신호를 보낸다. 그렇게 되면 컨트롤러는 모델을 재탐색하여 변경사항을 알아내고 이를 뷰에 반영하게 된다. 해당 통신을 ios 에서는 KVO(key-value observing)이라고 한다.
뷰는 사용자의 이벤트를 감지하고 이를 action을 통해 컨트롤러에게 알린다. 반대로 컨트롤러는 outlet을 통해 자신의 지시를 전달하게 된다. MVC모델은 컨트롤러(controller)가 무엇을(model) 어떻게 표현(view)할것인지 구분하여 코드를 작성하는 방법이라고 생각하면 된다.
그렇다면 화면이 여러개인 앱들이 있을텐데 그러한 앱들은 MVC를 어떻게 적용하느냐? 라는 질문이 생기게 된다. 이 경우, 여러개의 MVC가 만들어 지는데 각 MVC는 나머지 MVC들을 하나의 뷰로 여기게 된다.
Demo
해당 데모는 지난번 카드뒤집기 데모에 MVC를 적용하여 카드 짝맞추기 게임으로 만든 것이다.
Model
import Foundation
struct Card{
//카드가 앞면인지 나타내는 속성
var isFaceUp = false
//카드의 짝이 맞는지 나타내는 속성
var isMatchedUp = false
//카드의 짝을 확인하기 위한 속성
var identifier: Int
//정적 선언. 즉 card타입 자체에 부여되는 속성이다.
static var identifierFactory = 0
//정적 선언. card타입 자체에 부여되는 함수. id를 하나씩 증가시켜 반환한다.
static func getUniqueIdentifier() -> Int{
identifierFactory += 1
return identifierFactory
}
//카드의 초기화가 일어날 때 마다 고유의 id를 생성한다.
init(){
self.identifier = Card.getUniqueIdentifier()
}
}
import Foundation
class Concentration{
//card타입의 배열 생성
var cards = [Card]()
//유일하게 앞면 상태의 카드를 나타내는 속성 이후에 두번째로 오픈되는 카드와 id를 비교하기위해 사용된다.
var indexOfOneAndOnlyFaceupCard:Int?
//카드를 선택할 때 수행할 함수이다. 유저가 선택한 카드가 몇번째 배열인지 배개변수로 받는다.
func chooseCard(at index: Int){
print("card's id:",cards[index].identifier)
print("card's index:",index)
if !cards[index].isMatchedUp{
//유저가 선택한 카드가 만약 쌍을 이루지 못한 상태라면,
if let matchIndex = indexOfOneAndOnlyFaceupCard, matchIndex != index{
//만약 유저가 선택한 카드 제외, 앞면인 상태의 카드가 한장이 존재한다면, 해당카드와 id를 비교
if cards[matchIndex].identifier == cards[index].identifier{
//서로의 id가 같다면, 두 카드는 쌍을 이룬 상태로 전환
cards[matchIndex].isMatchedUp = true
cards[index].isMatchedUp = true
}
//그렇지 않다면 유저가 선택한 카드는 앞면을 향하게 한다.
cards[index].isFaceUp = true
//더이상 유일하게 앞면을 보고있는 카드는 없다.
indexOfOneAndOnlyFaceupCard = nil
} else{
//indexOfOneAndOnlyFaceupCard값이 nil이라면, 즉 유저가 선택한 카드 이외 앞면인 카드가 한장보다 많으면,
for flipDownIndex in cards.indices{
//모든 카드를 뒷면으로 바꾼다.
cards[flipDownIndex].isFaceUp = false
}
//유저가 선택한 카드는 앞면으로 바꾼다.
cards[index].isFaceUp = true
//유저가 선택한 카드는 앞면을 바라보는 유일한 한장이다.
indexOfOneAndOnlyFaceupCard = index
}
}
}
//생성할 카드쌍의 수를 입력받는다. 즉 입력받는 수의 두배만큼 카드를 만든다.
init(numberOfPairsOfCards:Int){
for _ in 0..<numberOfPairsOfCards{
//동일카드 한쌍을 배열에 추가한다.
let card = Card()
cards += [card,card]
}
//TODO: 카드섞기
for index in 0..<numberOfPairsOfCards{
let randomIndex = Int(arc4random_uniform(UInt32(numberOfPairsOfCards)))
let swpidentifier = cards[index].identifier
cards[index].identifier = cards[randomIndex].identifier
cards[randomIndex].identifier = swpidentifier
}
}
}
View
Controller
import UIKit
class ViewController: UIViewController {
//게임 생성. 카드 쌍의 수는 카드버튼 갯수의 절반이다.
lazy var game = Concentration(numberOfPairsOfCards: cardButtons.count+1/2)
//카드를 뒤집은 수를 나타내는 레이블.
@IBOutlet weak var flipCountLabel: UILabel!
//flipCountLabel에 표시할 변수. 당연하게도 0부터 시작한다.
var flipCount = 0 {
//변수의 값 변동이 일어날 때마다 수행되는 메소드. flipCountLabel에 변경된 값을 전달하고 표시한다.
didSet{
flipCountLabel.text = "flipCount: \(flipCount)"
}
}
//UIButton을 카드모양으로 배치하였다. 해당 카드버튼들을 배열로 담았다.
@IBOutlet var cardButtons : [UIButton]!
//사용자가 카드를 터치할 때의 이벤트 함수. 어떤 카드가 터치된건지 UIButton을 매개변수로 넘긴다.
@IBAction func touchCard(_ sender: UIButton) {
if let cardNumber = cardButtons.firstIndex(of: sender){
//sender 즉 사용자가 터치한 카드가 cards배열에서 몇번째 카드인지 넘겨준다.
game.chooseCard(at: cardNumber)
//뷰에 모델의 변경사항을 반영한다.
updateViewFromModel()
} else{
//만약 카드가 cards배열에 없다면 오류문 출력
print("card is not in cardsArray")
}
}
//모델의 변경사항을 뷰에 알려준다.
func updateViewFromModel() {
for index in cardButtons.indices{
//카드버튼과 카드는 배열 속 번호가 동일하다.
let button = cardButtons[index]
let card = game.cards[index]
if card.isFaceUp{
//앞면인 카드가 있다면, 카드에 해당하는 카드버튼에 이모지를 삽입
button.setTitle(emoji(for: card), for: UIControl.State.normal)
button.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
if !card.isMatchedUp{
//아직 짝이 없는 상태의 카드라면 flipCount증가.
flipCount += 1
}
} else{
//카드가 뒷면인 상태라면 해당 카드버튼은 이모지를 지우고 뒷면으로 바꾼다.
button.setTitle("", for: UIControl.State.normal)
//카드가 만약 짝이 맞는 상태라면 투명하게 그렇지 않다면 주황색으로 바꾼다.
button.backgroundColor = card.isMatchedUp ? #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0) : #colorLiteral(red: 1, green: 0.5763723254, blue: 0, alpha: 1)
}
}
}
//이모티콘 후보들이다. 배열로 담아놓았다.
var emojiChoices = ["🦇", "😱", "🙀", "👿", "🎃", "👻", "🍭", "🍬", "🍎"]
//emojiChoices에서 이모지를 뽑아 [카드의 id: 이모지] 쌍으로 딕셔너리를 생성한다.
var emoji = [Int:String]()
//카드에 이모지를 그려넣는 함수. 카드를 매개변수로 받는다. 이모지를 반환한다.
func emoji(for card: Card) -> String{
if emoji[card.identifier] == nil, emojiChoices.count > 0{
//만약 매개변수 카드의 id가 emoji딕셔너리의 키로 존재하지 않는다면, 이모지 후보에서 랜덤하게 뽑아온뒤 [카드id:이모지] 딕셔너리에 추가한다.
let randomIndex = Int(arc4random_uniform(UInt32(emojiChoices.count)))
emoji[card.identifier] = emojiChoices.remove(at: randomIndex)
}
//카드 id를 키값으로 이모지 반환
return emoji[card.identifier] ?? "?"
}
}
lazy var
Cannot use instance member 'cardButtons' within property initializer;
property initializers run before 'self' is available
컨트롤러를 작성하면서 새롭게 배운것은 lazy이다. game을 생성할 때 카드쌍의 수를 입력해야 하는데, 카드의 갯수를 늘리거나 줄이는 경우에, 이에 대응하여 game이 유연하게 동작하는 게 하기 위해 cardButtons 배열의 갯수를 읽어오도록 했다. 우선 스위프트는 효율적인 메모리관리를 위해 변수의 초기화가 이루어져야만 사용을 할 수 있다. 하지만 game변수를 초기화하기 위해 속성을 초기화하려다보니 속성을 초기화하려면 변수가 초기화 되어있어야 한다고 오류가 뜨고 말았다. 이러한 꼬인 부분을 해결하기 위해 사용한 것이 lazy였다. lazy로 선언된 변수는 일단 초기화가 완료된 것으로 취급이 되고, 해당 변수가 필요하게 되면 초기화가 시작된다. 이렇게 해서 상호간에 일으키는 오류를 해결했던 점이 흥미로웠다.
optional ?? unwrap
emoji(for: card) 함수를 보면 마지막에 return emoji[card.identifier] ?? "?" 부분이 있다. 이러한 표현방법을 다른곳에서도 자주 봤지만 뜻을 잘 몰랐고 이번 강의를 통해서 이해하게 되었다. 해당 코드는 본래 아래와 같은 표현이다.