클로저(Closure) - Swift vs JavaScript

Swift 클로저와 JavaScript 클로저의 차이점

Young Kim
스위프트 프로그래밍
12 min readMar 29, 2016

--

처음 JavaScript를 공부할때 클로저에 대한 개념때문에 많이 고생한 기억이 있습니다. 그리고 시간이지나 Swift를 배우면서 등장한 클로저에서 JavaScript의 클로저에 익숙해진탓에 초반에 개념잡는데 어려움을 겪었습니다. 이 글은 더 이상 헷갈리지 않도록 스스로를 위해 쓰는글입니다. 또한 저와 같은 고생을 한 사람이 있다면 작은 도움이 되길 바라면서 씁니다.

클로저(Closure)는 함수형 언어들이 공통으로 가지는 개념입니다. 큰 틀에서 보자면 JavaScript의 클로저와 Swift의 클로저는 같다고 볼 수 있습니다. 왜냐하면 클로저는 외부변수를 캡쳐하는것을 의미하기도 하고 익명함수를 뜻하기도 하기 때문입니다.. 다만 일반적으로 JavaScript의 클로저(Closure)는 외부변수를 캡처하는것을 의미하는 전통적 의미의 클로저입니다. 반면에 Swift의 클로저(Clousre)는 익명함수에 가깝습니다.

JavaScript에서의 클로저(Closure)

JavaScript의 경우 클로저는 JavaScript 언어가 가지는 태생적인 부족함을 메꾸기 위해 사용합니다. JavaScript는 클래스가 없고 전역변수를 기반으로 하기때문에 private변수를 선언할 방법이없습니다. 즉 변수가 외부에 노출되는 치명적인 문제가 있습니다. 따라서 JavaScirpt에서는 클로저를 사용하여 private변수를 만듭니다. 예제로 아래의 코드를 보겠습니다.

var myObject = function() {
var privateValue = 0;
return {
getValue: function () {
return privateValue;
}
};
};

JavaScript에서 변수의 생명 주기는 함수단위 입니다. 즉 함수안에 존재하는 변수들은 외부에서 접근 할 수 없으며 함수의 호출과 함께 생성되었다가 함수의 종료와 함께 소멸됩니다. 위와 같은 함수의 생명주기가 중요한 이유는 변수 privateValue는 외부로부터 완전히 숨겨져 있다는 사실 때문입니다. 즉 외부에서는 접근 할 방법이 없고, 함수 내부에서만 접근 할수 있으니 일단 private변수를 만드는데에는 성공한 셈입니다. 그러나 만약 외부에서 접근 할 수 없다면 어떻게 사용 할까요? JavaScript에서는 그 문제점을 클로저로 해결합니다. JavaScript에서의 클로저는 전통적인 의미의 클로저인데 이때의 클로저는 “내부 함수와 그 주변환경(Context)를 포함한 상태”라고 정의 할 수 있습니다. 아래의 예제를 통해 알아보겠습니다.

var anotherObject = myObject();
anotherObject.getValue();

myObject는 getValue 함수를가진 객체를 반환합니다. 위와같이 myObject를 호출 할 경우 함수의 호출과 함께 privateValue가 생성되고 함수 getValue를 가진 객체를 반환하면서 종료될 것입니다. 이때 JavaScript에서의 변수 생명주기에 따라 함수가 종료되는 시점에서 privateValue에 대한 참조가 없기 때문에 privateValue는 소멸되는것이 맞습니다. 한편 myObject의 반환값은 antherObject에 할당되기 때문에 anotherObject는 getValue함수를 가진 객체가됩니다. 그렇다면 anotherObject로 getValue함수를 호출하면 어떻게될까요? myObject가 종료되면서 privateValue가 소멸 되었으므로 getValue는 privateValue를 반환하지 못하고 오류가 날까요? 결론부터 말하자면 그렇지않습니다. anotherObject의 getValue에서 privateValue를 참조하고 있기 때문에 myObject함수가 종료 되었음에도 privateValue의 값이 소멸되지 않는것입니다. 다시 말하자면 myObject가 반환하는 객체에는 privateValue가 없지만 실질적으로는 소멸되지 않고 살아남아 우리는 getValue함수를 통해 접근 할 수 있습니다.

//myObject가 반환하는 부분은 아래 코드지만 실질적으로 여기에 + privateValue까지 반환하는 셈입니다.return {
getValue: function () {
return privatevalue;
}
};

이래서 클로저는 내부함수와 그 주변환경을 포함한다고 하는것입니다. 함수의 생명주기에따라 myObject의 변수인 privateValue가 함수의 종료와 함께 소멸되었어야 하지만 반환하는 내부함수에서 참조하고 있기때문에 살아 남은것입니다. 결과적으로 우리는 anotherObject를 통해 원할때마다 getValue함수를 호출하여 privateValue의 값을 얻을수 있고 이 방법이 privateValue의 값을 얻을 수 있는 유일한 방법이기 때문에 private한 변수를 만들고 사용하는것에 성공한 셈입니다. 정리하자면 전역변수 기반인 JavaScript에서는 클로저를 사용하여 private변수를 만들고 사용할 수 있습니다.

Swift에서의 클로저(Closure)

이제 Swift의 클로저를 살펴보겠습니다. Swift에서 클로저는 항상은 아니지만 많은 경우 익명(Anonymous) 함수를 의미합니다. 익명 함수는 말그대로 이름이 없는 함수입니다. 익명 함수는 함수를 일회용으로 사용할 경우, 즉 다른곳에서는 사용 할 필요가없는 경우에 함수 표현식을 줄이기 위해 사용합니다. Swift에서 처음 클로저를 배웠을때 정확히 언제 써먹어야 될지 몰라서 거의 사용하지 않았는데 Stanford iOS 강의에 나온 굉장히 좋은 예제를 보고 난 후 클로저의 위력을 알게되었습니다. 아래의 예제를 따라가면서 클로저를 적용해보겠습니다.

계산기 앱을 만든다고 가정해보겠습니다. 버튼을 누를경우 눌러진 버튼이 곱하기, 나누기, 더하기 그리고 빼기중 어떤 버튼인지 확인한 후 해당하는 버튼에 대한 연산을 하는 앱을 만들어야합니다. 계산되어야 할 숫자들은 stack이라는 배열에 저장되어 있다고 가정합니다. 다시 구현 해야할 부분을 정리하자면 1. 어떤 버튼인지 구분 2. 각각의 연산(곱셈, 나눗셈, 덧셈, 뺄셈) 정도가 될 것입니다. 먼저 어떤 버튼인지 구분하기 위해서는 어떻게 할 수 있을까요? 가장 일반적으로 if문과 else if를 사용하여 구분 할 수 있겠지만 이 방법은 코드가 지저분해지기 때문에 추천하지 않습니다. 그보다는 switch를 사용하겠습니다. switch문을 사용하면 보통 아래와 같이 구현 할 수 있을것입니다.

@IBAction func operate(sender: UIButton) {
let operation = sender.currentTitle!
switch operation {
case "x":
if stack.count >= 2 {
results = stack.removeLast() * stack.removeLast()
//계산된 숫자를 화면에 업데이트하는 code가 이곳에 위치.
}
case "/":
if stack.count >= 2 {
results = stack.removeLast() / stack.removeLast()
//계산된 숫자를 화면에 업데이트하는 code가 이곳에 위치.
}
case "+":
if stack.count >= 2 {
results = stack.removeLast() + stack.removeLast()
//계산된 숫자를 화면에 업데이트하는 code가 이곳에 위치.
}
case "-":
if stack.count >= 2 {
results = stack.removeLast() - stack.removeLast()
//계산된 숫자를 화면에 업데이트하는 code가 이곳에 위치.
}
default: break
}
}

저를 포함한 많은 경우에 이렇게 구현하고 넘어가버리곤 합니다. 위의 코드는 동일한 코드가 반복되는 문제점이 있습니다. 만약 일반 계산기가 아니라 공학용 계산기를 구현하는 것으로 확장한다고 생각해보면 연산이 추가됨에 따라 반복되는 코드의 양이 점점 많아질 것입니다. 또한 계산된 숫자를 화면에 업데이트하는 부분이 길어지기라도 한다면 반복되는 부분이 더 많아질 것입니다.

//반복되는 부분
if stack.count >= 2 {
results = 연산 작업
//계산된 숫자를 화면에 업데이트하는 code.
//이곳이 길어질경우 반복되는 코드가 증가합니다.
}

이런 반복을 피해야 한다면 보통의 경우 연산 작업을 함수로 분리 할 것입니다. 연산 작업을 함수로 분리할 경우 아래와 같이 할 수 있습니다.

@IBAction func operate(sender: UIButton) {
let operation = sender.currentTitle!
switch operation {
case "x": performOperation(multiply)
case "/": performOperation(divide)
case "+": performOperation(add)
case "-": performOperation(subtract)
default: break
}
}
func performOperation(operation: (Double, Double) -> double) {
if stack.count >= 2 {
results = operation(stack.removeLast(), stack.removeLast())
//계산된 숫자를 화면에 업데이트하는 code.
}
}
func multiply(op1: Double, op2: Double) -> Double {
return op1 * op2
}
func divide(op1: Double, op2: Double) -> Double {
return op2 / op1
}
func add(op1: Double, op2: Double) -> Double {
return op1 + op2
}
func subtract(op1: Double, op2: Double) -> Double {
return op1 - op2
}

이제 코드가 좀 정리가 된거같나요? 분명 코드의 가독성은 나아졌습니다. 그런데 코드의 길이가 더 길어졌습니다. 프로그램이 점점 커지면 분명 이득은 되겠지만 당장 보기에 함수가 너무 많은것 처럼 느껴집니다. 바로 이럴 때 클로저를 사용하면 위력이 엄청납니다. mutiply, divide, add, subtract 와 같은 함수를 익명함수로 바꾸어 줌으로써 코드를 비약적으로 줄일 수 있습니다. 참고로 클로저의 기본 표현식은 아래와 같습니다.

{(매개변수) -> 반환 타입 in 
실행할 구문
}

따라서 클로저의 기본 표현식을 사용하여 코드를 바꾸어 보면 아래와 같이 바꿀 수 있습니다.

@IBAction func operate(sender: UIButton) {
let operation = sender.currentTitle!
switch operation {
case "x":
performOperation({ (op1: Double, op2: Double) -> Double in
return op1 * op2
})
case "/":
performOperation({ (op1: Double, op2: Double) -> Double in
return op2 / op1
})
case "+":
performOperation({ (op1: Double, op2: Double) -> Double in
return op1 + op2
})
case "-":
performOperation({ (op1: Double, op2: Double) -> Double in
return op1 - op2
})
default: break
}
}
func performOperation(operation: (Double, Double) -> double) {
if stack.count >= 2 {
results = operation(stack.removeLast(), stack.removeLast())
//계산된 숫자를 화면에 업데이트하는 code.
}
}

어떤가요? 코드가 보다 짧고 간결해 졌습니다. 그러나 클로저의 진짜 위력은 이제부터 입니다. 위의 코드에서 클로저는 performOperation의 argument로 들어가는데, performOperation을 선언할때 에서 어떤 타입의 argument가 들어올지를 미리 정의했으므로 클로저 표현식에서는 타입을 생략해도 됩니다. 타입을 생략하면 클로저 표현식은 아래와 같습니다.

performOperation({ (op1, op2) in 
return op1 * op2
})

또한 performOperation 함수가 어떤 값을 return 할지도 함수를 선언할때 정의해 놨으므로 return 또한 아래와 같이 생략 할 수 있습니다.

performOperation({ (op1, op2) in op1 * op2 })

이게 끝이아닙니다. 충격적이게도 Swift에서는 들어오는 매개변수의 이름을 생략 할수도 있습니다. 첫번째 들어오는 매개변수는 $0, 두번째 들어오는 매개변수는 $1로 대체하여 사용하면 됩니다. 그럼 아래와 같은 코드가 나옵니다.

performOperation({ $0 * $1 })

간결해진 클로저 표현식을 사용하여 계산기 프로그램을 구현해보면 아래와 같은 코드가 나옵니다.

@IBAction func operate(sender: UIButton) {
let operation = sender.currentTitle!
switch operation {
case "x": performOperation({ $0 * $1 })
case "/": performOperation({ $1 / $0 })
case "+": performOperation({ $0 + $1 })
case "-": performOperation({ $0 - $1 })
default: break
}
}
func performOperation(operation: (Double, Double) -> double) {
if stack.count >= 2 {
results = operation(stack.removeLast(), stack.removeLast())
//계산된 숫자를 화면에 업데이트하는 code.
}
}

보시다시피 코드가 엄청나게 줄었을 뿐더러 가독성도 많이 증가하였습니다. 프로그램이 확장될수록 클로저의 위력은 더욱 커집니다. 앞으로는 클로저를 활용하여 코드를 간결하게 만드는 연습을 해봐야겠습니다.

--

--

Young Kim
스위프트 프로그래밍

Startup, 운동, 영화 그리고 프로그래밍에 관심이 많은 학생입니다.