먹고 기도하고 코딩하라

Swift 의 기본 문법 (2) - 함수와 클로저 본문

앱/Swift

Swift 의 기본 문법 (2) - 함수와 클로저

2G Dev 2021. 11. 20. 13:33
728x90
728x90

 

저번 포스팅에서는 기본적인 변수 선언 방식과 자료형에 대해 알아봤다.

이번에는 함수와 클로저를 알아본다.

 

 

(5) 함수

(5)-(1) 선언

함수 키워드는 func 이다. func 함수이름 (매개변수 이름: 타입, 이름: 타입, ...) -> 반환형 { } 으로 나타낼 수 있다.

가령, sayHi라는 이름의 함수가 있고, String 타입의 name을 매개변수로 받아 print를 수행하는 함수가 있다고 쳐 보자. 이 때, 이름이 없다면 "John Doe"라고 하기로 하자. 그러면 이렇게 쓸 수 있다.

func sayHi(name: String = "John Doe") -> Void {
  print("Hi, I'm \(name).");
}

sayHi();	// Hi, I'm John Doe.
sayHi(name: "Roy");	// Hi, I'm Roy.

이처럼, 함수를 호출할 때 매개변수가 있다면 매개변수의 이름을 꼭 함께 써줘야 한다. 이름: 값 순으로 써 주면 된다.

단, 매개변수 이름을 써주기 싫다면 함수 헤더의 해당 매개변수 이름 앞에 _ 을 붙여주면 호출할 때 그 매개변수는 이름: 값 순으로 쓰지 않고 값만 쓸 수 있다.

만약 함수에 매개변수가 없다면 그냥 괄호 안을 비워둘 수 있다.

그리고 함수 반환형이 Void가 아니라면 무조건 return 문을 써줘야 한다. 위의 sayHi 함수의 경우, 반환값을 정의하지 않았기 때문에 Void라는 특별한 타입을 반환한다. Void는 간단히 ()를 사용한 빈 튜플이다.

 

두 개의 다른 함수가 같은 이름을 쓰는 것도 가능하다. 전달인자 레이블(argument label)이라는 걸 쓰면 매개변수 이름까지 똑같아도 다른 함수로 취급된다.

func sayHi(nick name: String) -> Void {
  print("Hi, My name is \(name).");
}

// 이렇게 _로 레이블을 채우면, 호출할 때 'name'을 넣지 않아도 된다
func sayHello(_ name: String) -> Void {
  print("Hello, \(name)");
}

sayHi();	// Hi, I'm John Doe.
sayHi(nick: "Paul");	// Hi, My name is Paul.
sayHello("Newman");	// 이렇게 호출 가능

전달인자 레이블은 2번째 sayHi의 매개변수 name 앞에 붙은 nick을 말한다.

함수 내부에서는 원래 이름인 name을 쓰지만, 바깥에서 함수를 호출할 때는 전달인자 레이블을 사용한다. 물론 첫 번째 함수를 쓰려면 name으로 호출하면 된다. (첫 번째 sayHi 함수는 전달인자 레이블이 없기 때문에)

 

(5)-(2) 가변 매개변수 (...)

가변 매개변수를 쓸 수도 있다. 이건 주로 매개변수가 몇 개가 들어올지 잘 모를 때 쓰는 용도이다.

타입이 정해져 있다면 그 타입을 쓰면 되고 아니라면 Any로 받으면 된다.

이 때 joined를 안 쓰고 그냥 print로 출력하면 [ ] 안에 원소들이 감싸져서 나오는데, joined 메소드의 separator를 이용하면 세련되게 정제해서 출력이 가능하다.

func callCustomers(customers: String...) -> Void {
  print("사이렌오더로 주문하신 \(customers.joined(separator: ", ")) 고객님 음료 나왔습니다.");
}

callCustomers(customers: "Claudia");	// 사이렌오더로 주문하신 Claudia 고객님 음료 나왔습니다.
callCustomers(customers: "Alice", "Bob", "Carol", "Dave");
// 사이렌오더로 주문하신 Alice, Bob, Carol, Dave 고객님 음료 나왔습니다.

 

(5)-(3) 여러 가지 값 반환하기

함수는 여러 가지 값을 반환할 수 있다.

대신 반환할 값들을 튜플 () 로 감싸야 하며, 함수 헤더의 반환형에 타입과 이름을 정확히 맞춰서 써줘야 한다.

func returnSomeValues() -> (name: String, age: Int) {
    return ("Paul", 39);
}

print(returnSomeValues());	// (name: "Paul", age: 39)
print(profile.name);	// Paul
print(profile.1);	// 39

이렇게 튜플로 묶어서 반환하면 사용이 가능하다. 매개변수 이름으로 어떤 값에 접근하거나 인덱스로 접근이 가능하다. 

 

(5)-(4) 일급 객체로서의 함수

스위프트에서도 함수는 일급 객체이다. 무슨 말인가 하면 변수, 상수 등에 함수를 저장할 수 있다는 의미이다.

내 고향 자바스크립트에서는 함수 표현식을 굉장히 많이 쓴다. 이것도 비슷하게 생각하면 될 것 같다.

var vFunc: (String) -> Void = sayHi(name:);
vFunc();	// Hi, I'm John Doe

조금 혼란스럽지만 살펴보자. 콜론 뒤로는 타입이 온다. 그러므로, vFunc의 타입은 String 타입의 값을 매개변수로 갖고, 아무것도 반환하지 않는 함수이다.

이 함수는 앞에서 우리가 선언한 sayHi 함수이다. 그러니까 vFunc를 sayHi 함수 쓰듯이 쓸 수 있게 된 거다.

타입을 써주니까 살짝 꼬여 보이긴 하지만 어쨌든 제대로 쓴 게 맞다.

 

생각해보면 매개변수로도 함수를 쓸 수 있지 않을까 싶은 생각이 드는데 사실이다. 쓸 수 있다. 다만... 좀 복잡해질 뿐

func runFunc(function: (String...) -> Void) {
  function("Edward", "Fred", "George");
}
runFunc(function: callCustomers(customers:));

이 때 주의할 점은 customers 라고 콜론 없이 그냥 쓰면 안 된다. 그러면 스위프트 컴파일러가 customers라는 걸 scope에서 찾아서 함수 매개변수로 넣으려는 시도를 하기 때문이다. 조금 까다로우니 주의가 필요할 듯. 

 

(5)-(5) 클로저

이걸 이용해서 클로저도 만들 수 있다. 자바스크립트긴 한데 클로저 설명은 이미 해뒀으므로 이 글을 참고하면 좋을 것 같다.

사실은 함수도 클로저의 일종이다. 다만 이름이 있을 뿐. 클로저는 어떤 상수, 변수의 참조를 캡쳐해 저장할 수 있다. 스위프트의 클로저는 이렇게 생겼다.

{ (이름: 타입, 이름: 타입...) -> return 반환타입 in
  // statements
}

예제를 보자.

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

var incrementBySeven = makeIncrementer(7)
print(incrementerBySeven());	//7
print(incrementerBySeven());	//14
print(incrementerBySeven());	//21

incrementer는 함수가 된다. 무슨 함수냐고? 바로 makeIncrementer 안에서 선언되고 반환된 함수 incrementer이다. makeIncrementer 안의 incrementer는 값을 캡쳐(runningTotal)해서 갖고 있는 내부 함수이다. makeIncrementer의 매개변수인 amount도 역시 캡쳐해서 갖고 있다. incremenetBySeven을 여러 번 실행하지만, 매번 값이 7이 되는 게 아니라 누적이 되는 모습이다. runningTotal, amount가 캡쳐돼서 변수를 공유하기 때문에 누적된다. 일반적인 함수라면 불가능하지만 값을 잡아서 갖고 있는 내부 함수이기 때문에 누적되는 모습이다. (이해하기 좀 어렵다.. ㅎㅎ)

 

함수와 클로저는 참조(reference) 타입이다. 함수, 클로저를 상수나 변수(let, var)에 할당할 때, 실제로는 함수의 복사값(value)이 아니라 참조(reference)가 할당된다. 그래서 다음과 같은 것도 가능하다. 둘 다 똑같은 makeIncrementer(7)을 가리키고 있기 때문이다.

let alsoIncrementBySeven = incrementBySeven
print(alsoIncremenetBySeven());	// 28

 

또다른 예제를 살펴보자.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
  return s1 > s2
})

sorted의 by는 비교 함수를 받게 되어 있는데 이 비교 함수로 클로저를 준 모습이다. 오름차순이 아니라 내림차순으로 정렬하도록 만든 정렬 함수이다.

 

클로저는 함수 전달인자로 사용될 때가 많다. 야곰님 강의에서는 계산하는 함수를 만들고, 어떤 계산을 할지는 함수를 따로 받는 형식으로 만들었는데 이걸 한 번 뜯어보자.

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
  return method(a, b)
}

calculate 함수는 Int형 값 a, b를 받고 추가로 method라는 걸 받는데, 이 method는 Int 값 두 개를 받아서 Int 값을 돌려주는 함수이다. calculate 함수는 내부적으로 method에 a, b를 넣어 실행하고 그 값으로 나온 Int값을 그대로 돌려주는 역할을 한다.

이걸 어디다 써먹을 수 있을까? 간단하다. 외부에서 (Int, Int) -> Int 인 함수를 만들어 변수나 상수에 담아서 method에 등록해주면 어떤 계산이라도 calculate를 이용해서 할 수 있다.

let sum: (Int, Int) -> Int = { (a: Int, b: Int) in 
  print("sum")
  return a + b 
}
var multiply = { (a: Int, b: Int) in return a * b }
var result = calculate(a: 5, b: 3, method: {a, b in a - b})
result = calculate(a: 2, b: 10, method: { return $0 - $1 })
result = calculate(a: 10, b: 5, method: {a, b in a / b })

sum처럼 클로저에 여러 문장이 들어갈 수 있다. 하지만 이 때 in 뒤의 문장들을 중괄호로 감싸주면 unable to infer complex closure return type 하면서 에러가 나기 때문에 중괄호는 빼고 개행한 다음에 문장들을 쓰도록 하자.

 

클로저를 줄여서 쓰는 방법은 여러 가지가 있다. 이제 풀버전 클로저부터 극단적으로 줄여나가는 클로저까지 살펴보자. 

var result: Int

(1) 기본
result = calculate(a: 10, b: 10, method: {(left: Int, right: Int) -> Int in 
    return left + right 
})

(2) 매개변수 타입 제거
result = calculate(a: 10, b: 10, method: {(left, right) -> Int in 
    return left + right 
})

(3) 반환형 제거
result = calculate(a: 10, b: 10, method: {(left, right) in 
    return left + right 
})

(4) 인자 이름 제거
result = calculate(a: 10, b: 10, method: {return $0 + $1})
// 암시적 반환 표현으로, 단일 표현 클로저에서는 return 문 생략 가능
result = calculate(a: 10, b: 10, method: { $0 + $1 })	

(5) 하나의 연산만 하고 바로 return한다면 연산자만 남기기 가능(..)
result = calculate(a: 10, b: 10, method: +)

(6) 후행 클로저 (trailing closures)
result = calculate(a: 10, b: 10) {(left: Int, right: Int) -> Int in
    return left + right
}

이게 된다고..? 의 연속이다. 특히 5번의 경우에는 충격적일 정도...

매개변수와 타입을 제거할 수 있는 건, calculate에서 받는 method 인자가 Int 타입의 값 2개를 받아 Int 타입 하나를 반환한다는 걸 컴파일러가 알고 있기 때문이다. 인자 이름을 제거할 수 있는 건 순서대로 나타낼 수 있기 때문이고. 단, $0은 첫 번째 인자인 a, $1은 두 번째 인자인 b로, 순서대로 주어진다는 것을 숙지하는 게 좋겠다.

후행 클로저를 잘 보면 calculate 안에 method로 함수를 대입해준 게 아니라 a, b만 써주고 닫은 다음 method로 쓸 함수를 따로 중괄호로 빼서 적은 모습이다. 만약 클로저로 받아야 할 함수가 너무 길어진다면 이렇게 함수를 따로 빼서 써주는 게 가능한데 이런 걸 후행 클로저라고 부른다.

 

 

References

 

 

728x90
반응형
0 Comments
댓글쓰기 폼