computedProperty

스위프트에서 변수는 property라고도 불리운다. 즉 연산용 변수라고 보면 된다. 

var foo: Double{
     get{
          //return calculater value
     }
     set(newValue){
          //foo has changed to newValue
     }
}

get에 해당하는 영역에는 변수 foo의 값을 불러올 때 사용되는 코드를, set에는 변수 foo에 새로운 값을 대입할 때 동작할 코드를 적어주면 된다. newValue는 대입할 경우에 들어올 새로운 값을 의미한다. 당연히 괄호안의 이름을 바꾸면 해당 이름의 변수로 사용이 가능하다. 생략이 가능하며 생략시에는 무조건 newValue로 사용해야한다.

var temp = 1
var x:Int {
    //x를 호출할 경우 temp를 반환
    get{
        return temp
    }
    
    //x에 대입할 경우 두배의 값을 temp에 대입
    set(newValue){
        temp = newValue * 2
    }
}

//x에 10을 대입(set) -> temp에 20이 저장된다.
x = 10

//x호출(get) -> temp의 값인 20이 출력된다.
print(x)

예제로 입력 값의 두배를 불러오는 computedProperty를 작성해보았다. 주의할 점은 return을 위한 임시변수가 필요하다는 점이다. 즉 x는 저장을 위한 변수가 아닌 말그대로 연산을 위한 변수인 것이다. 

 

아래 예제는 지난번 데모에 computedProperty를 적용한 부분이다. 

    //유일하게 앞면 상태의 카드를 나타내는 속성 이후에 두번째로 오픈되는 카드와 id를 비교하기위해 사용된다.
    var indexOfOneAndOnlyFaceupCard:Int? {
        //indexOfOneAndOnlyFaceupCard 호출시
        get{
            //리턴을 위한 임시 변수 생성. optional로 생성한다.
            var foundIndex:Int?
            
            //카드배열을 돌면서 앞면상태인 카드를 탐색한다.
            for index in cards.indices{
                if cards[index].isFaceUp{
                    //앞면인 카드발견 + foundIndex가 nil, 즉 앞면인 카드가 오로지 한장이라면,
                    if foundIndex == nil {
                        //앞면이 유일한 카드는 해당 카드가 된다.
                        foundIndex = index
                    } else{
                        //foundIndex에 값이 있는경우, 즉 앞면인 카드가 한장보다 많다면, 유일하게 앞면인 상태가 아니므로 nil을 반환한다.
                        return nil
                    }
                }
            }
            //foundIndex의 값으로 indexOfOneAndOnlyFaceupCard설정
            return foundIndex
        }
        //indexOfOneAndOnlyFaceupCard 대입시
        set{
            //카드 배열을 돌면서 카드의 앞면, 뒷면을 설정한다.
            for index in cards.indices{
                //사용자가 선택한 카드번호(nexValue)가 아닌 나머지 카드들은 모두 뒷면으로 설정한다.
                cards[index].isFaceUp = (index == newValue)
            }
        }
    }

Model에 해당하는 Concentration 클래스의 indexOfOneAndOnlyFaceupCard 변수이다. 해당 작업으로 인해 호출부분의 코드가 조금 더 간결해졌다.

추가로 computedProperty는 get만 설정하는것이 가능하다. 

//게임 생성. 카드 쌍의 수는 카드버튼 갯수의 절반이다.
lazy var game = Concentration(numberOfPairsOfCards: numberOfPairsOfCards)
    
var numberOfPairsOfCards: Int{
    //numberOfPairsOfCards 호출시 cardButtons배열 길이의 절반 반환
    return cardButtons.count+1/2
}

get만 설정할 경우 예제와 같이 return만 적는 것이 가능하다. 하지만 반대로 set만 설정하는 것은 불가능하다.


accessControl

접근제어. 아직 작성중인 클래스에 접근 가능역역을 표시하는 작업이다.

internal(default) - 같은 앱 내에서 어떤 객체든 접근 가능한 상태이다 .기본값이라 생략이 되어있다. 앱이나 프레이워크의 모든 요소가 접근이 가능한 상태이다.

private - 다른 앱에서 접근 불가능한 상태이다. 

private(set) - 변수를 위한 옵션이다. 다은 사람이 값에 접근이 가능하지만 설정은 할 수 없다. (읽기전용)

fileprivate - 해당 파일 안에 있는 어떠 것이든 서로 접근가능한 상태.

 

프레임 워크에만 사용하는 접근제어

public - 외부에서 접근 가능

open - 외부에서 접근 가능 + 서브클래스 생성 등 여러가지 수정 및 추가가 가능

 

*공유할 일이 없으면 비공개처리를 해두는 것이 좋다. 

아울렛 변수의 경우 공유할 경우가 없으니 모두 비공개로 하자.


extension

클래스에 함수 및 속성(변수)의 추가

extension Int{
    //arc4Random함수 추가. 기존의 UInt32의 자료형을 출력하는 함수를 Int형으로 반환하게 수정하였다.
    func arc4Random() -> Int{
        assert(self > 0, "it has to be more than 0")
        return Int(arc4random_uniform(UInt32(self)))
    }
}

assert(condition:Bool, message:String)함수는 조건식을 체크하고, 조건식이 맞지 않으면(false), 오류메세지(String)를 출력한다.

Int 클래스에 arc4random()함수를 추가하였으며, 해당 함수는 0보다 큰 정수만 입력받으므로 assert()함수로 조건을 체크한다. 

이렇게 추가를 하게되면 기존의 지저분하게 보이던 코드를 정리할 수 있다.

extension Int{
    var arc4random: Int{
        if self > 0{
            //0보다 큰 정수라면, 0과 정수사이의 랜덤 숫자 반환
            return Int(arc4random_uniform(UInt32(self)))
        } else if self < 0 {
            //0보다 작은 정수라면, 0과 음수사이의 랜덤 숫자 반환
            return -Int(arc4random_uniform(UInt32(abs(self))))
        } else {
            //0이라면, 0반환
            return 0
        }
    }
}

해당 함수는 변수로도 설정이 가능하며, arc4random()함수의 경우 0보다 큰 정수만 입력받는것을 보완하여 추가하였다.

아래의 코드처럼 호출부가 더욱 간결해졌다.

//기존의 두줄이었던 코드
let randomIndex = Int(arc4random_uniform(UInt32(emojiChoices.count)))
emoji[card.identifier] = emojiChoices.remove(at: randomIndex)

//Int클래스에 arc4random을 추가한 후
emoji[card.identifier] = emojiChoices.remove(at: emojiChoices.count.arc4random)

enum

열거형 데이터타입이다. 가질 수 있는 값들을 미리 지정해서 사용하는 것이 특징이다.

enum fastFoodMenuItem{
    case hamburger
    case fries
    case drink
    case cookies
}

//fastFoodMenuItem이라는 자료형을 사용할 수 있다.
var burger:fastFoodMenuItem = .hamburger

if burger == .hamburger{
    print("it is hamburger")
}

사진처럼 지정된 값들만 입력이 가능하며 좌변에 fastFoodMenuItem이라 명시를 해줘서 우변에는 타입추론이 가능하다.

타입추론을 지원하지 않는다면 var burger:fastFoodMenuItem = fastFoodMenuItem.hamburger 라고 적어야 했다.

enum fastFoodMenuItem{
    case hamburger
    case fries
    case drink
    case cookie
    
    func showMenu() {
        switch self{
        case .hamburger : print("hamburger")
        case .fries : print("fries")
        case .cookie : print("cookie")
        case .drink : print("drink")
        }
    }
}

//메뉴 생성
var menu0: fastFoodMenuItem = .hamburger

//메뉴 출력
menu0.showMenu()

함수 또한 설정이 가능하며 예시와 같이 해당 메뉴가 무엇인지 출력하는 코드를 작성해보았다.

enum fastFoodMenuItem{
	//연동자료 선언
    case hamburger(numberOfPatties: Int)
    //당연히 연동자료를 enum으로 선언이 가능하다.
    case fries(size: fryNumberOfSize)
    case drink(String, ounces:Int)
    case cookie
    
    func showMenu() {
        switch self{
        //연동자료를 사용하기 위해서는 let을 이용하면 된다. 정해진 연동자료보다 많이 선언하면 오류가 출력된다.
        case .hamburger(let pattyCount) : print("hamburger with \(pattyCount) patties.")
        case .fries(let size) : print("fries \(size) size.")
        case .cookie : print("cookie.")
        case .drink(let brand, let ounces) : print("drink. a \(ounces)oz \(brand)")
        }
    }
}

enum fryNumberOfSize{
    case large
    case small
}

var menu0: fastFoodMenuItem = .hamburger(numberOfPatties: 2)
var menu1: fastFoodMenuItem = .fries(size: .large)
var menu2 = fastFoodMenuItem.drink("sprite", ounces: 13)

menu0.showMenu()
menu1.showMenu()
menu2.showMenu()

mutating func changeTo(menu:fastFoodMenuItem){
        self = menu
    }

self를 사용하여 값의 변경이 가능하다. 유의할 사항은 mutating인데, 간단히 말하면 값의 변경이 일어난 다는 것을 스위프트에게 알리는 문구라고 한다.

var menu0: fastFoodMenuItem = .hamburger(numberOfPatties: 2)
menu0.showMenu()

menu0.changeTo(menu: .fries(size: .small))
menu0.showMenu()

해당 함수 호출로 메뉴를 변경하였다.


optional

스위프트를 접하고 가장 궁금했던 부분이 옵셔널이다. 이 옵셔널이 결국 enum이다. 

enum optional<T>{ //a generic type like Array<Element>, Dictionary<Key,Value>
    case none
    case some(<T>)
}

<T>의 의미는 어떤 형태든 들어올 수 있다는 얘기다. 

값이 존재하지 않으면 .none, 어떠한 값이 존재하면 .some()을 통해서 switch을 통해 우리가 알고있는 옵셔널로 동작하게 되는것이다. 

상수 y를 보면 x, foo(), bar, z가 모두 옵셔널의 형태로 연결되어 하나라도 nil이라면, nil을 반환하게 되는 구조이다. 이러한 구조를 옵셔널 체인이라고 부른다.


automaticReferenceCounting

자바는 가비지 컬렉션을 통해 힙메모리를 관리한다. 반면 스위프트는 가비지 컬렉션이 아닌 자동참조횟수정책을 이용하여 힙메모리를 관리한다. 힙내의 참조타입에 포인터를 만들때마다 스위프트는 어딘가에 있는 카운터에 1을 추가. 해당 포인터가 가리키는것이 없어지거나, 더이상 가리키지 않게 되었을 때 ,카운트를 줄이면서 0이 되면 힙메모리에서 해당하는 참조타입을 제거하게 된다. 

influencing ARC

참조횟수계산에 영향을 미치는 3가지 방식의 포인터가 있다. 

strong

일반적인 참조횟수계산방식이다. 포인터가 무언가를 가리키는 한 힙내에 계속 놔둔다.

weak

힙내의 어떤것을 가리키지만 가리키는 대상이 strong 포인터를 가지고 있으면 힙내에 유지, 더이상 strong 포인터를 가지고 있지 않게되면 해당 포인터에는 nil을 반환하고 가리키는 대상은 힙메모리에서 제거한다. (ex. optional, oulet)

unowned

참조횟수계산에 참여하지 않는다. 힙내부의 어떤 것을 가리키고 있을 때, strong포인터로서 인식하지 않는다. 가리키고 있는 대상이 힙내에서 사라졌을때, 해당 포인터에 접근하지 않는다. → 메모리 사이클을 피하기 위해 사용한다.

*메모리사이클: 힙 메모리내에서 서로를 참조하여 힙 메모리내에 계속해서 남게되는 일

'iOS > Stanford' 카테고리의 다른 글

day06_multiTouch  (0) 2022.03.26
day05_view  (0) 2021.12.30
day04_swift_part2  (0) 2021.10.25
day02_MVC  (0) 2021.10.03
day01_ios  (0) 2021.09.28

코드는 유지보수 및 수정이 용이하도록 작성해야 하는데, 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] ?? "?"  부분이 있다. 이러한 표현방법을 다른곳에서도 자주 봤지만 뜻을 잘 몰랐고 이번 강의를 통해서 이해하게 되었다. 해당 코드는 본래 아래와 같은 표현이다. 

if emoji[card.identifier] != nil {
    return emoji[card.identifier]!
} esle {
    return "?"
}

emoji딕셔너리가 카드의 id로 key를 가지고 있으면 해당하는 value를 unwrap하여 반환하고, 해당 키를 가지고 있지 않으면 "?"를 반환하는 코드를 return emoji[card.identifier] ?? "?"라는 한줄로 대치한 것이다. 

 

'iOS > Stanford' 카테고리의 다른 글

day06_multiTouch  (0) 2022.03.26
day05_view  (0) 2021.12.30
day04_swift_part2  (0) 2021.10.25
day03_swift_part1  (0) 2021.10.12
day01_ios  (0) 2021.09.28

edwith를 통해 스탠포드대학의 ios강의를 번역자막을 보면서 시청할수있다. 해당 강의를 시청하며 정리한 내용을 적어보려고 한다. 


what's in ios?

ios에 대한 간략한 설명을 해보자면 초기의 os X for iphone에서 3버전에는 iphone os, 2010년 4버전이 나오면서 ios라는 이름으로 정착되었다. ios는 크게 cocoa touch, media, core service, 

core OS로 구분되어 사용자와 하드웨어간의 의사소통을 담당한다.

 

사용자

cocoa touch

media

core service

core OS

하드웨어

 

cocoa touch는 전반적인 ui를 담당한다. 버튼, 슬라이더, 맵킷, 카메라 등 사용자와 가장 가까운 부분의 처리를 수행한다.

media는 말 그대로 비디오, 오디오, 이미지 등 ios의 미디어 부분을 할당한다. 

core service는 core location, network와 같은 핵심적인 서비스를 담당한다. 

core OS는 BSD계열 unix로 구성되어있으며, 당연하게도 c언어 베이스이다. 

 

해당 강의에서는 cocoa touch와 core service에 대해 다룰 예정이다.


Demo

이번 강의에 대한 카드뒤집기 데모를 만들었다. 

import UIKit


class ViewController: UIViewController {
    //카드를 뒤집은 수를 나타내는 레이블.
    @IBOutlet weak var flipCountLabel: UILabel!
    
    //flipCountLabel에 표시할 변수. 당연하게도 0부터 시작한다.
    var flipCount = 0 {
        //변수의 값 변동이 일어날 때마다 수행되는 메소드. flipCountLabel에 변경된 값을 전달하고 표시한다.
        didSet{
            flipCountLabel.text = "flipCount: \(flipCount)"
        }
    }
    
    //카드를 뒤집는 함수. 카드에 그려질 이모티콘(문자열)과 어떤 카드에 그릴지 UIButton을 매개변수로 받는다.
    func flipCard(withEmoji emoji:String, on button:UIButton) {
        if button.currentTitle == emoji{
            //카드가 앞면일 경우. 카드의 현재 문양(currentTitle)과 배개변수로 받은 이모티콘이 같다.
            button.setTitle("", for: UIControl.State.normal)
            button.backgroundColor =  colorLiteral(red: 1, green: 0.5763723254, blue: 0, alpha: 1)
        } else{
            //카드가 뒷면일 경우. 카드에 배개변수로 받은 이모티콘을 그리고(setTitle) 배경을 흰색으로, flipCount를 증가시킨다.
            button.setTitle(emoji, for: UIControl.State.normal)
            button.backgroundColor =  colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
            //flipCount가 증가되면 didSet이 수행된다.
            flipCount += 1
        }
    }
    
    //UIButton을 카드모양으로 배치하였다. 해당 카드들을 배열로 담았다.
    @IBOutlet var cards: [UIButton]!
    
    //카드에 그려넣을 그림을 이모티콘으로 표현한다. 배열로 담았다.
    let emojis = ["🐶","🦊","🐶","🦊"]
    
    //사용자가 카드를 터치할 때의 이벤트 함수. 어떤 카드가 터치된건지 UIButton을 매개변수로 넘긴다.
    @IBAction func touchCard(_ sender: UIButton) {
        if let cardNumber = cards.firstIndex(of: sender){
            //sender 즉 사용자가 터치한 카드가 cards배열에 몇번째 배열인지 반환한다.
            //해당 카드와 같은 배열번호의 이모지를 배개변수로 넘겨 flipCard()함수 수행.
            flipCard(withEmoji: emojis[cardNumber], on: sender)
        } else{
            //만약 카드가 cards배열에 없다면 오류문 출력
            print("card is not in cardsArray")
        }
    }
}

여기서 인상깊었던 부분은 함수부분이다. 처음 스위프트를 배울 때 함수선언부분에서 왜 매개변수명을 굳이 두개나 적을까 궁금했는데, 새touchCard()flipCard() 호출부분을 보면 스위프트가 어째서 매개변수명을 두개로 적는지 알 수 있었다. 애플은 코드를 읽을 때 최대한 사람의 입장에서 자연스럽게 코드를 읽을 수 있기를 바랬고, 그러한 의도가 호출부에서 잘 나타났다.

 

flipCard(withEmoji: emojis[cardNumber], on : sender) -> 전달자에 들어있는 이모지가 그려진 카드를 뒤집어라. 

 

반대로 선언부에서는 withemojion이라는 변수명이 부자연스러우니 emojibutton이라는 변수명으로 사용이 된다. 이러한 변수명의 선택은 코드의 유지보수면에서 큰 도움이 된다고 생각한다. 강의를 많이 찾아본 것은 아니지만 적어도 학원강의에서는 이러한 부분을 전혀 알려주지 않았던 것을 보면 확실히 본토 대학강의가 훨씬 더 영양가있다고 생각한다.

 

이번에 didSet{ }에 대해서도 처음 배웠다. 속성감시자라고 번역이 되어있는데, 우리가 알고있는 변수는 스위프트에서 속성이라 부른다고 한다. 속성(변수)값이 변경될 경우 항상 수행되는 작업을 명시해줄 수 있다. 해당 데모에서 flipCount를 증가할 때 마다 레이블 수정작업을 따로 적을 필요없이 didSet{ }에 레이블 수정작업을 적어주어 코드라인을 줄일 수 있게 되었다.

 

 

'iOS > Stanford' 카테고리의 다른 글

day06_multiTouch  (0) 2022.03.26
day05_view  (0) 2021.12.30
day04_swift_part2  (0) 2021.10.25
day03_swift_part1  (0) 2021.10.12
day02_MVC  (0) 2021.10.03

https://github.com/scenee/FloatingPanel#change-the-backdrop-alpha

 

GitHub - scenee/FloatingPanel: A clean and easy-to-use floating panel UI component for iOS

A clean and easy-to-use floating panel UI component for iOS - GitHub - scenee/FloatingPanel: A clean and easy-to-use floating panel UI component for iOS

github.com

현재 FloatingPanel을 이용하여 개발 중 만났던 삽질에 대해 글을 써본다. 

우선 FlotingPanel에 대해 간략하게 설명하자면 애플지도, 애플주식 앱을 켜면 나오는 하단의 bottomSheet이다.

이러한 기본 뷰 위에 또 다른 뷰를 띄우는데 쉽게 제작할 수 있게 라이브러리로 깃허브에 올라와있다. 관심있다면 해당링크로 들어가보자. 

FloatingPanel 라이브러리를 사용중 레이아웃을 수정하여 초기에 뜨는 뷰의 높이를 수정하려는데 아무리 수정해도 반영이 되질 않았다. 

class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate, FloatingPanelControllerDelegate{

    @IBOutlet var mapView: MKMapView!
    
    var fpc: FloatingPanelController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        urlrequest(count: 1)
        
        mapView.delegate = self
        
        fpc = FloatingPanelController(delegate: self)
        
        let contentVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ContentVC")
        fpc.set(contentViewController: contentVC)
        
        fpc.addPanel(toParent: self)

        floatingPaneldesign()
        
        fpc.show()
        fpc.layout = CustomFloatingPanelLayout()
    }

class CustomFloatingPanelLayout: FloatingPanelLayout{
    var position: FloatingPanelPosition = .bottom
    var initialState: FloatingPanelState = .tip
    
    var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
            return [
                .full: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
                .half: FloatingPanelLayoutAnchor(absoluteInset: 270.0, edge: .bottom, referenceGuide: .superview),
                .tip: FloatingPanelLayoutAnchor(absoluteInset: 110.0, edge: .bottom, referenceGuide: .superview)
            ]
        }
}

문제의 코드. CustomFloatingPanelLayout에서 initialState 변수가 첫 화면이 나올때의 높이를 조절하는 부분인데 아무리 바꾸어도 적용이 되지 않았다. 며칠동안 initialState변수만 들여다 보고있었는데 결론은 메인 뷰 컨트롤러에 있는 fpc.show()부분이었다. quick help를 읽어보니 다음과 같이 적혀있었다. 

Summary
Shows the surface view at the initial position defined by the current layout

"현재 레이아웃으로 정의된 포지션에 뷰를 띄운다." 나는 여태 라이브러리 기본값의 위치로 뷰를 띄우고 나서 레이아웃을 바꾸었으니 첫 화면이 뜰때는 무조건 기본좌표로만 띄워졌던 것이다. 문제를 인지하고나서 fpc.show()fpc.layout = Custom... 두 줄의 위치를 바꾸어주니 내가 원하던데로 첫 화면부터 원하는 높이로 띄울 수 있게 되었다. 

앱 제작 도중 만난 에러다.

여기에 막혀서 며칠이나 해맸다.. 늘 그렇듯 정말 사소한 실수였고, 모르면 그만큼 고생하게 된다는 걸 느꼈다. 

 

func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView){
        if let eventAnnotation = view.annotation as? EventAnnotation{
            print("\(eventAnnotation.title)핀이 눌렸습니다.")
            if let contentVC = storyboard?.instantiateViewController(identifier: "SearchVC") as? SearchVC{
                contentVC.updateView(eventAnnotation.title!, "", "")
            }
        }
    }

메인 컨트롤러에 있는 맵뷰의 핀을 누르면 이벤트 처리를 통해 FloatingPanel(SearchVC를 할당하였다.)에 띄워져 있는 뷰에 접근하려하였다.

func updateView(_ text1:String, _ text2:String, _ text3:String) {
        self.APGroupName.text = text1
        self.addressDong.text = text2
        self.addressDetail.text = text3
    }

이벤트 발생 시 SearchVC 클래스의 IBOutlet label의 텍스트를 수정하려는 부분에서 해당 에러가 발생했다.

에러의 원인은 검색을 통해서 금방 찾을 수 있었는데 원인은 바로 할당되지 않은 변수(nil)에 값을 넣으려니 에러가 발생한 것.. 

여기서 눈치를 챘어야했는데 바보같이 updateView()부분만 계속해서 수정했었다.

 

열심히 삽질을 하다가 문득 FloatingPanel쪽을 들여다 보게되었고, 여기서 해법을 찾았다.

나는 FloatingPanel에 띄워져 있는 뷰 컨트롤러에 접근을 해야했는데, 스토리보드의 instantiateViewController()를 통해 접근하려 했으니, 존재하지 않는 뷰에 접근하려 해서 에러가 발생했던 것이다. 

클래스의 특성에는 initializer(생성자), self, inheritance(상속)가 있다. 

init( )

클래스는 객체생성 시 자동으로 단 한번만 생성자 메소드를 호출한다. 스위프트에서는 init()이라는 이름으로 사용한다. 

주로 클래스 멤버변수 초기화의 용도로 사용을 한다.

class AAA{
    var a = 10
    var b = "아기상어"
    
    init(aa:Int, bb:String){s
        //생성자 --> 멤버변수 초기화로 주로 사용한다.
        print("AAA initializer start: \(aa), \(bb)")
        a = aa
        b = bb
    }
    
    func fn_1(){
        print("AAA fn_1() start: \(a), \(b)")
    }
    
    func fn_2(){
        print("AAA fn_2() start: ")
    }
}
var cc1 = AAA(aa:1234, bb:"엄마상어")
var cc2 = AAA(aa:456, bb: "할아버지상어")
cc1.fn_1()
cc2.fn_1()

init()은 따로 호출하지 않아도 알아서 실행이 되는 모습을 볼 수 있다. 예시와 같이 생성자를 통해 객체 생성과 동시에 변수의 초기화가 이루어지니 더 간결하게 코드를 적을 수 있다.

var arr = [AAA(aa:10, bb:"정우성"),
           AAA(aa:20, bb:"정좌성"),
           AAA(aa:60, bb:"정남성"),
           AAA(aa:80, bb:"정중성")]

for qq in arr{
    qq.fn_1()   //배열로 담아 메소드 실행
}

배열을 이용하여 메소드를 실행할 수 있다는 점도 알아두자.


self

self는 멤버요소 접근자로서 자기 자신 클래스 내 멤버변수에 접근할 때 사용된다.

class AAA{
    var a:Int!      
    var b:String!   
    
    init(_ a:Int, _ b:String){
        self.a = a  //self -> 현재 인스턴스의 멤버요소 접근자.
        self.b = b  //매개변수와 멤버변수의 혼동을 막기위해 명시적으로 선언하기 위한 용도로도 쓰인다.
        
        ppp()
    }
    
    func ppp(){
        print("a:\(a!), b:\(b!)")
    }
}
var zx = AAA(123, "조인성") //한번만 사용할 것이라면, 굳이 변수로 담지 않아도 된다.

위 예제는 init()의 매개변수와 클래스내의 멤버변수가 이름이 같아 혼동이 될수 있기에 명시적인 목적으로 사용했다.


self 예제 1

각 선분의 길이를 입력받으면 어떤 도형인지, 도형의 넓이, 도형의 둘레를 측정하는 클래스를 작성해보았다. 편의상 사각형은 직각사각형, 삼각형은 직각삼각형으로 취급했다.

class Shape{
    var name:String!
    var line:[Int]!
    var area = 0, border = 0
    
    init(_ line:Int...){    //사용자 정의 생성자 -- 필요한 멤버요소 획득
        self.line = line
        name = ["원","직사각형","직각삼각형"][line.count-1]   //line.count-1은 인덱스를 담당하게된다.
        calc()  //연산
    }
    
    func calc(){
        if name == "원"{
            let pi = 3.14
            self.area = Int(Double(self.line[0]) * Double(self.line[0]) * pi)
            self.border = Int(Double(self.line[0]) * 2.0 * pi)
        } else if name == "직사각형"{
            area = line[0] * line[1]
            border = (line[0] + line[1]) * 2
        } else if name == "직각삼각형"{
            area = line[0] * line[1] / 2
            border = line[0] + line[1] + line[2]
        }
    }
        
    func ppp(){
        print("\(name!): \(area), \(border)")
    }
}
var shapes = [
    Shape(5),
    Shape(5,6),
    Shape(5,6,8),
    Shape(10,20),
    Shape(14,16,21),
    Shape(8)
]

for sh in shapes{
    sh.ppp()
}


self 예제 2

학생클래스를 정의하고 학생 정보를 출력해보았다.

클래스명: Stud

입력내용: 이름,국어,영어,수학

출력정보: 이름,국어,영어,수학,총점,평균

class Stud{
    var name:String
    var kor:Int
    var eng:Int
    var math:Int
    
    var tot = 0
    var avg = 0
    
    init(_ name:String, _ scores:Int...){
        self.name = name
        self.kor = scores[0]
        self.eng = scores[1]
        self.math = scores[2]
        
        calc()
    }
    
    func calc(){
        tot = kor + eng + math
        avg = tot/3
    }
    
    func ppp(){
        print("name:\(name)\t국어:\(kor)\t영어:\(eng)\t수학:\(math)\t총점:\(tot)\t평균:\(avg)")
    }
}
var studs = [Stud("라이언",86,87,89),
            Stud("어피치",77,76,74),
            Stud("프로도",65,66,68),
            Stud("제이지",99,97,95)
            ]

for st in studs{
    st.ppp()
}


Inheritance

Inheritance(상속)는 상위클래스의 요소를 자식클래스가 가져와 사용할 수 있는 객체지향언어의 가장 큰 특징이다. 

상속의 선언은 다음과 같다. 

     class 자식클래스 : 부모클래스 {

          실행코드 ... 

     }

자식클래스는 부모클래스의 멤버변수와 메소드를 사용할 수 있는데, 메소드는 자식클래스의 필요에 따라 overriding(재정의)이 가능하다. 상속을 이용하는 가장 큰 이유이며 이렇게 하나의 메소드가 호출하는 주체에 따라 다르게 동작하는 것을 polimolphism(다형성)이라고 한다.

class Par{
    var a = 10
    var b = "부모"
    
    func fn_1(){
        print("부모 fn_1()")
    }

    func fn_2(){
        print("부모 fn_2()")
    }
}

class Child : Par{  //상속관계 정의(자식 : 부모)
    var c = 3000
    
    override func fn_2(){   //overriding 재정의. 부모의 메소드를 재정의하여 사용한다.
        print("자식 fn_2()")
    }
    
    func fn_3(){
        print("자식 fn_3()")
    }
    
}

class Child2 : Par{  //상속관계 정의(자식 : 부모)
    var d = 4040
    
    func fn_4(){
        print("자식2 fn_4()")
    }
}
var pp = Par()
var cc = Child()
var cc2 = Child2()

print("pp-----------")
print(pp.a,pp.b)
pp.fn_1()
pp.fn_2()

print("cc-----------")
print(cc.c)
print(cc.a,cc.b,cc.c)
cc.fn_3()
cc.fn_1()
cc.fn_2()

print("cc2-----------")
print(cc2.d)
//print(cc2.c)    //Child클래스의 멤버변수에 접근 불가.
print(cc2.a,cc2.b,cc2.d)
//cc2.fn_3()  //Child클래스의 메소드에 접근 불가.
cc2.fn_4()
cc2.fn_1()
cc2.fn_2()

예제를 보면 알 수 있듯이 자식 클래스들은 모두 부모 클래스에 접근이 가능하지만, 서로 다른 자식클래스들은 서로 간에 접근이 불가능하다. 또한 같은 함수를 호출하더라도 overriding이 되어있는지의 여부에 따라 다른 결과물이 출력되는 점도 확인 할 수 있다.


객체지향언어의 꽃이라고 볼 수 있는 상속은 누군가 체계적으로 작성한 클래스를 상속, 재정의하여 본인에게 알맞게 사용 가능하다는 점이 매력적인 기능이다.

'iOS > TJ' 카테고리의 다른 글

day12_funcVariable, selfCall, class  (0) 2021.06.29
day11_param, return, funcCall  (0) 2021.06.18
day10_func()  (0) 2021.06.17
day09_dictionary, set  (0) 2021.06.16
day08_multiArray, tuple  (0) 2021.06.03

local variable, global variable

변수는 함수내에서 선언이 되면 해당 함수가 호출 시에 생성이 되고 함수가 종료되면 소멸된다. 이러한 변수를 지역변수라고 한다. 함수 밖에서 선언이 된 변수는 전역변수라고 부르며 프로그램이 실행되면 생성이 되며 프로그램이 종료되어야 소멸한다. 

var a = 10      //전역변수  -> 함수 내외에서 호출 가능
var b = 20      //전역변수

func fn_1(){
    var c = 3000    //지역변수 -> 해당함수 내에서만 호출 가능, 함수 종료시 소멸.
    print("\t fn_1()시작 a:\(a), b:\(b), c:\(c)")
    a = 11  //전역변수 a 변경
    b = 21  //전역변수 a 변경
    print("\t fn_1()끝 a:\(a), b:\(b), c:\(c)")
}

func fn_2(){
    var d = 4000    //지역변수 -> 해당함수 내에서만 호출 가능, 함수 종료시 소멸.
    print("\t fn_2()시작 a:\(a), b:\(b), d:\(d)") //전역변수 b호출
    a=13    //전역변수 a 변경
    var b = 23  //지역변수 b 선언 -> 우선순위가 전역변수보다 높다.
    print("\t fn_2()끝 a:\(a), b:\(b), d:\(d)")  //지역변수 b 호출
}

func fnTot(){
    var e = 5432
    print("fnTot()시작 a:\(a), b:\(b), e:\(e)")
    fn_1()      //fn_1() 함수를 호출했다 하더라도, fn_1()의 지역변수를 호출할수는 없다.
    print("fnTot()중간 a:\(a), b:\(b), e:\(e)")
    a=12        //전역변수 a 변경
    fn_2()      //fn_2() 함수를 호출했다 하더라도, fn_2()의 지역변수를 호출할수는 없다.
    print("fnTot()끝 a:\(a), b:\(b), e:\(e)")
}

fnTot()

지역변수의 특징은 선언된 함수 내에서만 접근이 가능하다는 점이다. 다른 함수가 호출하였다 하더라도 해당 지역변수에는 접근할 수 없으며, 반대로 전역변수의 경우 어디서든 접근이 가능한 것이 특징이다.


selfCall

selfCall(재귀함수)는 자기 자신을 호출하는 함수를 뜻한다. 반복문과 마찬가지로 탈출조건을 적지 않으면 무한루프에 빠지게 되니 주의깊게 작성해야 한다. 

//재귀함수를 이용하여 0부터 매개변수까지의 합 구하기
var pre = ""
func fn(no:Int) -> Int{
    let myPre = pre
    pre += "\t"
    
    var res = no
    
    print("\(myPre)fn() 시작:\(no), \(res)")
    
    if no > 0 {     //조건
//        let rr = fn(no:no-1) //증감 : -1
//        res += rr
//        print(no, rr, res)
        res += fn(no:no-1)
    }
    
    print("\(myPre)fn() 끝:\(no), \(res)")
    return res
}

var qq = fn(no:3)    //초기값
print("main: \(qq)")

pre변수는 함수의 출력부분을 편하게 구분하기 위해 출력한 것이므로 주의깊게 보지 않아도 된다. fn(3)의 호출 흐름을 간단하게 그려보았다.


class

클래스는 표현하고자 하는 것을 자료형으로 정의한 것이라고 생각하면 된다. 

표현하고자 하는 것의 속성 및 상태를 변수로 표현하고, 그것의 동작을 함수로 표현한다. 이때 클래스에 정의한 변수를 멤버변수, 클래스에 정의한 함수를 메소드라고 지칭한다. 클래스를 정의하고 사용하기까지 4단계를 거친다.

  1. 클래스 정의
  2. 클래스 형의 변수의 선언
  3. 생성잘르 통해 생성 (heap영역에 생성)
  4. 인스턴스 클래스의 호출

클래스 정의

클래스를 정의한다. 멤버변수를 통해 표현하고자 하는 것의 속성과 특징을 서술하고, 메소드를 통해 동작을 표현한다.

class AAA {
    var aa = 10     //멤버변수: 상태
    var tt = "아기상어"     //멤버변수
    
    func fn_1(){        //메소드: 동작
        print("\(aa), \(tt) AAA fn_1() 실행") //해당 클래스의 멤버요소 접근 가능
    }
    
    func fn_2(){        //메소드
        print("AAA fn_2() 실행")
    }
}

클래스형 변수 선언

클래스를 실체화하여 사용하기 위해 변수로 선언한다. 

var a1:AAA  //정의된 클래스를 통해 변수를 선언한다.
var a2:AAA //물론 여러개를 만들 수 있다.

생성

선언한 변수를 사용하기 위해 생성자로 초기화한다.

a1=AAA()    //생성 및 대입
a2=AAA()

인스턴트 클래스 호출 및 접근

변수를 통해 메소드 호출 및 멤버변수에 접근 한다.

print(a1)   //인스턴스 변수 호출 ::> 클래스 타입을 알 수 있다.

print(a1.aa,a1.tt)    //멤버변수 호출
print(a2.aa,a2.tt)    //멤버변수 호출

a1.fn_1()	//메소드 호출
a2.fn_1()	//메소드 호출

a1.aa = 123     //멤버변수에 값 대입
a1.tt = "아빠상어"  //멤버변수에 값 대입

print("a1.aa:",a1.aa,"/ a1.tt:",a1.tt)    //멤버변수 호출
print("a2.aa:",a2.aa,"/ a2.tt:",a2.tt)    //멤버변수 호출

print("-----------------")

a1.fn_1()   //메소드 호출
a1.fn_2()   //메소드 호출

a2.fn_1()
a2.fn_2()

실행 결과를 보면 멤버변수는 각 독립된 존재인 것을 알 수 있다.


클래스 예제

클래스를 통해 입력받은 점수로 합계와 평균을 구하는 클래스를 생성해보았다.

class Exam{     //클래스명은 대문자로 시작한다.
    var name = ""
    var score = [Int]()
    var tot = 0, avg = 0
    
    func first(_ nn:String, _ jj:Int...){
        //1.입력부
        self.name = nn
        self.score = jj
        
        //2.계산부
        self.calc()
        
        //3.출력부
        self.ppp()
    }
    
    func calc(){
        tot = 0
        for i in score{
            tot += i
        }
        avg = tot / score.count
    }
    
    func ppp(){
        print("\(name)\t\(score)\t총점:\(tot)\t평균:\(avg)")
    }
}

비중있게 보아야 할 부분이라면 first()함수에서 매개변수의 값을 멤버변수에 옮겨 담은 후, calc()함수와 ppp()함수를 호출하여 한번에 입력, 계산, 출력을 동작하게 하는 부분이다. 코드가 짧다면 굳이 함수를 나눌 필요없이 first()함수 안에 다른 코드들을 적으면 되지만, 함수의 내용이 커지면 예제와 같이 분리하여 작성하는 것이 분업, 코드의 유지보수에도 더 편리하다. 

var st1 = Exam()	//Exam()형태의 변수 생성
var st2 = Exam()

st1.first("한가인", 77,78,71)  //입력과 동시에 출력.
st2.first("두가인", 87,88,81)

'iOS > TJ' 카테고리의 다른 글

day13_initializer, self, inheritance  (0) 2021.07.01
day11_param, return, funcCall  (0) 2021.06.18
day10_func()  (0) 2021.06.17
day09_dictionary, set  (0) 2021.06.16
day08_multiArray, tuple  (0) 2021.06.03

gcp로 운영 중이던 웹페이지가 db를 읽어오지 못하는 현상이 생겼다. 아래는 웹페이지에서 볼 수 있는 문구.

마지막 따옴표 사이에 읽어와야할 db명이 적혀있다.

java.sql.SQLSyntaxErrorException: Could not connect to address=(host=127.0.0.1)(port=3306)(type=master) : Unknown database 'wifi '

문제를 파악하기 위해 서버로 접근하여 확인해본 결과 db가 없어졌다. 

sql파일을 가지고 있어서 다시 올리면 그만인데 문제는 며칠 간격으로 db가 계속 사라진다는 것.. 

처음 웹페이지를 운영해본터라 서버 문제인 줄 알고 로그는 확인도 안 하고 반복해서 sql만 새로 올렸다.

반복되는 오류에 뒤늦게 로그를 확인해봐야겠다는 생각이 들었다. 문제는 mariaDB만 그런 건지 db를 올리면 기본값이 로그를 남기지 않게끔 되어있었다. 그래서 이번에는 로그를 남기게 설정해놓고 주기적으로 들어가서 확인해 보았다. 

 

결론

 새벽 4시경에 누군가 db에 접속하여 자료를 건드는 로그를 발견.

지워진 당일날 확인하니 db자체는 남아있지만 안에 있는 테이블과 자료들은 없다. readme를 읽어보니 데이터를 복구하고싶으면 토르 브라우저로 접속하여 비트코인을 보내란다.. 즉 랜섬웨어다. 

처음에는 기존 db도 안남겨놓고 readme만 있었는데, 나는 readme가 mariaDB에서 작성된 기본 db로 착각해서 열어볼 생각을 안 했었다.

돈 안 보내고 쭉 지켜봤더니 이번에는 내 데이터는 백업되어 있단다.. 48시간 내로 돈 안 주면 유출할 거라는 문구로 변경되어 있다. 나름 신기한 경험을 해보았고 로그의 중요성을 깨달았다. 로그 볼 생각 안 했으면 계속 반복해서 sql만 올렸을 테니.. 해당 gcp서버는 파기 후 다시 가동하면 그만이다. 다만 걱정되는 건 어떤 경로로 감염이 된 건지를 잘 모르겠다.

'web > errors' 카테고리의 다른 글

서버 재부팅 후 도메인 연결 안되는 문제  (0) 2021.06.10

+ Recent posts