SSAC iOS 앱 개발자 데뷔과정 - 16

Sangwon Shin·2021년 10월 21일
0

SSAC

목록 보기
14/19

1️⃣ First Class Citizen

1급 객체는 아래 3가지 조건을 만족해야 합니다.

  • 변수나 상수에 할당할 수 있다.
  • 객체의 반환값으로 반환할 수 있다.
  • 객체의 인자로 넘길 수 있다.

지금까지 저희가 자주 사용하던 함수 가 1급 객체에 해당합니다.

왜 그런지 하나 하나 살펴 보도록 하겠습니다.


// 1) 변수나 상수에 함수를 대입할 수 있다.
func checkBankInformation(bank: String) -> Bool {
    let bankArray = ["우리", "KB", "신한"]
    return bankArray.contains(bank) ? true : false
}

//변수나 상수에 함수를 실행하고 나온 반환값을 shin 에 대입
let shin = checkBankInformation(bank: "KB")

// 변수나 상수에 함수 "자체" 대입 (함수 포인터랑 비슷한 느낌?)
let shinAccount = checkBankInformation //함수만 대입 -> 실행되지 않음
shinAccount("우리") //함수를 호출해야 실행이 된다.

먼저 함수를 변수나 상수에 대입하는 경우를 살펴 보겠습니다.

우선, 현재 은행이 사용자가 가입한 은행에 포함되는지를 확인하는 checkBankInformation 를 정의 했습니다.

1) let shin = checkBankInformation(bank: "KB")
2) let shinAccount = checkBankInformation

그리고 위와 같이 2개의 상수를 설정했습니다.

1 ) 의 경우, 지금까지 저희가 사용하던 함수의 형태입니다.
함수의 반환값이(True) shin 이라는 상수에 저장됩니다.

2 ) 의 경우, 함수의 반환값이 아닌 함수 자체를 상수에 대입하는 형태 입니다.
이때, shinAccount 의 type 을 확인해보면 (String) -> Bool 형태의 Function Type 임을 확인 할 수 있습니다.

❗️C 언어에서의 함수 포인터와 동일하게 생각하면 이해하기 편할 것 같습니다

그렇기 때문에 shinAccount 자체만으로는 함수가 실행되지 않고, 함수를 호출해야 함수가 호출되는 것을 확인 할 수 있습니다.

//Function Type 1 (String) -> String
func hello(nickname: String) -> String {
    return "저는 \(nickname)입니다."
}

//Function Type 2 (String, Int) -> String
func hello(nickname: String, userAge: Int) -> String {
    return "저는 \(nickname), \(userAge )입니다."
}

// hello 라는 함수는 오버로딩되어 타입 어노테이션을 통해 구분할 수 있다.
// 또는 함수의 식별자로 구분할 수 있따.
let a = hello(nickname: ) //식별자
let b: (String, Int) -> String = hello //type annotation

a("바보")
b("보바", 12)

위와 같이 함수 오버로딩을 통해 이름이 동일한 여러개의 함수를 상수에 대입하는 경우에는 타입 어노테이션, 함수의 식별자 를 통해서 원하는 함수를 대입할 수 있습니다.


// 2) 함수의 반환 타입으로 함수를 사용할 수 있다.

func currentAccount() -> String {
    return "게좌 있음"
}

func noCurrentAccount() -> String {
    return "게좌 없음"
}

//함수 실행 결과를 반환하는게 아니라 함수 자체를 반환 해준다.
// 가장왼쪽에 위치한 -> 를 기준으로 오른쪽에 놓인 모든 타입은 반환값을 의미한다.
func checkBank(bank: String) -> (() -> String) {
    let bankArray = ["우리", "KB", "신한"]
    return bankArray.contains(bank) ? currentAccount : noCurrentAccount
}

let minsu = checkBank(bank: "농협") // () -> String 으로 함수 type이 반환
minsu() // 이와같이 함수를 실행시켜야 String 값

다음은, 함수의 반환 타입으로 함수를 사용하는 경우를 확인 해보겠습니다.

func checkBank(bank: String) -> (() -> String) {
}

함수의 반환 값을 확인해보면 () -> String Void 값을 인자로, String 값을 반환하는 함수 Type 임을 알 수 있습니다.

즉, minsu 라는 상수에는 noCurrentAccount 라는 함수 자체가 저장됩니다.


// 3) 함수의 인자값으로 함수를 사용할 수 있다.
// 콜백함수로 자주 사용됨 (특정 구문의 실행이 끝나면 시스템이 호출하도록 처리된 함수)
func oddNumber() {
    print("Odd Number")
}

func evenNumber() {
    print("Even Number")
}

func resultNumber(base: Int, odd: () -> (), even: () -> () ) {
    return base.isMultiple(of: 2) ? even() : odd()
}

resultNumber(base: 9, odd: oddNumber, even: evenNumber) //Odd Number

마지막으로, 함수의 인자값으로 함수가 사용되는 경우를 확인 해보겠습니다.

func resultNumber(base: Int, odd: () -> (), even: () -> () )

함수의 parameter(Void) -> Void 함수 타입인것을 확인 할 수 있습니다.

하지만 이런 형태로 함수가 구성될 경우에는, 인자값으로 받는 함수에 따라서 함수의 결과값이 달라질 수 있습니다.

func plusNumber() {
    print("더하기")
}

func minusNumber() {
    print("빼기")
}

resultNumber(base: 9, odd: plusNumber, even: minusNumber) //빼기

이렇게 어떤함수가 인자값으로 들어가는지 상관없이 함수타입만 맞으면 동작하고, 실질적인 연산은 인자값으로 받는 함수에 달려 있기 때문에 브로커 함수 라고 표현합니다.


📃 Closure

resultNumber(base: 9) {
    print("Odd Number")
} even: {
    print("Even Number")
}

앞서 살펴본 함수를 사용하기 위해서는 인자값으로 함수타입을 넘겨줘야 하기 때문에 매번 우리의 목적에 맞는 함수를 선언해야 하는 불편함이 있습니다.

그런 불편함을 Closure 를 통해서 해소할수 있습니다.
❗️자동완성된 함수에 enter를 한번더 누르면 위와 같은 형태로 바뀌게 됩니다.

그럼 Closure 는 어떤것을 의미할까요?

클로저는 코드의 블럭이자, 일급 객체 입니다.

{ (매개 변수들) -> 반환 타입 in
    실행코드
}

일급객체에 코드의 블럭이면 함수아닌가요? 라고 생각이 들 수 있습니다.
맞습니다 함수도 클로저의 한 형태입니다.

//함수를 사용한 경우
func studyClosure(subject: String) -> String{
    return "\(subject).... 너무 어려운거 아니오?"
}

let study1 = studyClosure(subject: "클로저")
print(study1) // 클로저.... 너무 어려운거 아니오?

//클로저를 사용한 경우
let study2 = { (subject: String) in
    print("\(subject).... 너무 어려운거 아니오?")
}
study2("클로저") // 클로저.... 너무 어려운거 아니오?

그러면 앞서 살펴본것과 마찬가지로, 클로저도 변수, 상수, 반환값, 함수의 인자 로 사용할 수 있습니다.


다양한 예제들을 살펴 보면서 개념을 잡아보겠습니다.

func todayNumber(result: (Int) -> String) {
    result(Int.random(in: 1...100))
}

Int 를 매개변수로 가지고, String 타입을 반환하는 함수 타입 result 를 매개변수로 가지는 함수가 있습니다. 🤮

천천히 다시 가겠습니다.

우리는 앞서 1급 객체 를 공부하면서 함수타입 에 대해서 공부했습니다.
현재 result 는 (Int) -> String 함수 타입입니다.
즉, 위의 함수는 함수를 인자로 받는 형태입니다.

따라서, todayNumber(어떤 함수) 를 호출하게 되면 String 이 반환됩니다.
❗️ 정확히는 어떤함수(random 정수)의 결과값이 반환됩니다.

그럼 우리는 위의 함수 동작을 확인하기 위해서 (Int) -> String 의 함수를 정의하고 해당함수를 매개변수로 넘겨줄 수도 있지만 클로저를 사용해보겠습니다.

todayNumber(result: { (number: Int) -> String in
    return "행운의 숫자 \(number)"
})

(Int) -> String 타입의 함수 대신 클로저를 인자값으로 넘겨준 형태입니다.

todayNumber(result: { (number)  in
    return "행운의 숫자 \(number)"
})

todayNumber 함수의 전달인자로 전달할 클로저는 함수에서 요구하는 형태를 준수하고 있다고 유추할수 있기 때문에 위와 같이 매개변수 타입을 생략할 수 있습니다. (문맥을 이용한 타입 유추)

todayNumber(result: {
    return "행운의 숫자 \($0)"
})

매개변수의 이름도 생략할 수 있습니다. 위와 같이 단축 인자 이름을 사용하게 되면 첫 번째 전달인자부터 $0,$1.. 로 표현됩니다.

todayNumber(result: {
    "행운의 숫자 \($0)"
})

위와 같이 클로저가 반환값을 가지고, 클로저 내부의 실행문이 단 한줄일 경우, return 을 생략할 수 있습니다. (제발 멈춰!!!!)


이번에는 클로저의 값 획득(Capture) 에 대해 알아보겠습니다.

클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득 할 수 있습니다.

역시 예제와 함께 살펴보겠습니다.

func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

() -> (Int) 함수타입을 반환받는 함수가 있습니다.

let incrementByTwo = makeIncrementer(forIncrement: 2)
let first: Int = incrementByTwo() // 2
let second: Int = incrementByTwo() // 4
let third: Int = incrementByTwo() // 6

let incrementByTen = makeIncrementer(forIncrement: 10)
let first2: Int = incrementByTen() // 10
let second2: Int = incrementByTen() // 20
let third2: Int = incrementByTen() // 30

makeIncrementer 함수 내부에 선언된 runningTotal 이라는 변수는 해당 함수 내부에서만 사용할 수 있습니다.

하지만 위의 결과를 살펴보면, makeIncrementer 함수의 반환값 (함수타입) 으로 선언된 상수 incrementByTwo 를 통해서 runningTotal 을 사용하고 있는 것을 확인 할 수 있습니다!

🤷‍♂️어떻게 가능할까요?
이는 함수 incrementer() 가 함수 주변의 runningTotal, amount 두 변수의 참조를 획득했기 때문입니다.
(이를 변수나 상수의 클로징 이라고 하며, 여기서 착안된 이름이라고 합니다.)


🏷 P.S.

클로저가 굉장히 어렵네요... ㅠ

아직까지 왜 why? 클로저를 사용하는가? 어떤 경우에 클로저를 써서 이득을 볼 수 있는지 정확하게 감이 잡히지 않았습니다.

(코드 줄이는거 -> 너무 줄여서 헷갈립니다..)
(값 캡처 -> 어차피 메모리 어딘가에 저장될 형태 였다면, 직접 관리할수 있게 전역변수로 선언하는것이..
-비동기 작업에 많이 사용된다고 하는데 아직 비동기에 대해 몰라서 필요성이 와닿지 않았나?)

천천히 많은 예제들을 보면서 다시 공부 해야겠습니다. 익숙해지는 과정이 필요할 것 같습니다.

profile
개발자가 되고싶어요

0개의 댓글