함수형 자바스크립트를 위한 문법 다시 보기
함수형 자바스크립트를 잘 다루기 위해서는 숲을 보는 것보다 나무의 결을 들여다 보는 것이 중요하다. 자바스크립트 문법과 기본적인 동작에 집중해보자. 함수 하나가 정의되고 실행되고 참조되는 과정, 인자를 받거나 넘기는 과정, 클로저가 되거나 비동기가 일어나는 과정, 괄호, 대괄호, 점, 쉼표 등을 자세히 들여다 보자. 문법적 감각이 좋아지면 원하는 곳 어디에서나 함수를 열고 실행할 수 있게 된다.
객체와 대괄호 다시 보기
객체와 key
1 | var obj = { a: 1, "b": 2 }; // 1 |
일단 객체의 key 와 value에 대한 부분이다. 객체의 key와 value는 {}, . , [] 등을 통해 설정할 수 있다.
그중 어떤 문자열이든 key로 정의할 수 있는 곳이 있는데 1과 2 같은 곳이다. 이 두 가지 방식이 가진 공통점이 있다면 띄어쓰기, 특수 문자, 숫자 등을 가리지 않고 어떤 문자열이든 key로 만들 수 있다는 점이다.
1 | var obj2 = { "a a a": 1 }; |
그렇다면 {} 안쪽에서 key를 선언하는 것과 [] 안에서 선언하는 것은 차이가 없을까? {}의 문자열 부분에서는 코드를 실행할 수 없고 []의 안쪽에서는 코드를 실행할 수 있다.
1 | var obj5 = { (true ? "a" : "b"): 1}; // Uncaught SyntaxError |
{} 안쪽의 key 영역에서는 코드를 실행할 수 없다. [] 사이에는 문자열이 담긴 변수도 넣을 수 있고, 연산자도 사용할 수 있으며 함수도 실행할 수 있다. 즉, [] 에서는 코드를 실행할 수 있다.
1 | var obj5 = { [true ? "a" : "b"]: 1 } |
하지만 ES6 의 경우에서는 {} 안에 [] 를 사용하여 코드를 실행할 수 있게됐다.
함수나 배열에 달기
1 | function obj8() { } |
자바스크립트에서는 함수도 객체다. 그러므로 함수도 key/value 쌍으로 구성할 수 있다.
1 | var obj10 = [] |
배열도 객체이며 배열에도 숫자가 아닌 key를 사용할 수 있다. 단, 숫자가 아닌 key로 값을 할당할 경우 length는 변하지 않는다.
1 | var obj11 = []; |
배열에 숫자로 key를 직접 할당해도 push와 동일하게 동작한다. 자동으로 length도 올라간다.
delete
자바스크립트에서는 기본 객체의 메서드나 프로퍼티도 지울 수 있다.
1 | var obj = { a: 1, b: 2, c: 3 }; |
다른 언어를 다루었던 개발자라면 delete로 아무거나 지우기, 배열에 숫자가 아닌 key 사용하기 등을 봤을 때, 자바스크립트의 유연함을 난해하다거나 위험하다고 느낄 수 있다. 하지만 이 특징들을 문제가 아닌 자바스크립트의 특성으로 받아들인 후에 더욱 다양한 기법들이 나오기 시작했고 더 잘 동작하기까지했다.
함수 정의 다시 보기
기본 정의
자바스크립트에서 함수를 정의하는 방법은 다양하다. 대표적인 방법들은 다음과 같다.
1 | function add1(a, b) { |
함수를 정의하는 것은 이미 익숙하겠지만 확인해 볼 만한 부분이 있다. 바로 호이스팅인데, 호이스팅에 대해 어느 정도 알고 있더라도 읽어 보기를 권한다.
호이스팅
호이스팅(hoisting)이란 변수나 함수가 어디서 선언되든지 해당 스코프 최상단에 위치하게 되어 동일 스코프 어디서든 참조할 수 있는 것을 말한다. 아래의 코드 add1과 add2에는 호이스팅이 적용된다. ‘add2는 실행이 안 될 텐데 호이스팅이 아니지 않나?’하고 생각할 수 있지만 이것은 오해다. 물론 에러가 난 걸로 알 수 있듯이 add2는 선언하기 전 라인에서 실행할 수 없다. 하지만 분명히 add2도 호이스팅이 적용된 것이다. 결론부터 말하면 선언은 되었지만 아직 초기화되지 않은 상태에서 실행했기 때문에 에러가 난 것 이다.
1 | add1(10, 5); // 15 |
예제에서 add2는 실행되지 않았고, ‘add2 is not a function’ 이라는 에러 메시지가 출력되었다. 이번에는 선언한 적 없는 함수를 실행해보자.
1 | hi(); |
에러 메시지가 다르다. 자바스크립트에서는 아예 선언된 적이 없는 것을 참조하려고 할 때 이러한 에러가 난다. 실행하지 않고 참조만 하려고 해도 동일한 에러가 난다.
1 | console.log(add1); // function add1(a, b) { return a + b }; |
이번엔 에러가 나지 않았고 undefined가 출력되었다. 그렇다면 add1과 add2는 어떤 차이에 의해 실행이 되고 안 되는 것 일까?
이는 변수 선언과 함수 선언에서의 차이 떄문이다. 변수는 선언 단계와 초기화 단계가 구분되어 있다. 변수는 선언과 초기화가 동시에 이루어지지 않기 때문에 호이스팅에 의해 참조만 가능하고, 아직 값이 담기지 않아 실행은 불가능하다. 반면에 함수 선언은 선언과 동시에 초기화가 이루어지기 때문에 참조뿐 아니라 실행도 가능하다.
add2는 변수를 선언하여 익명 함수를 담았고 add1은 함수로 선언했다. 호이스팅에 의해 add1은 미리 실행할 수 있고 add2는 호이스팅에 의해 미리 참조할 수 있지만 값이 없어 실행할 수는 없다.
호이스팅 활용하기
함수 선언과 호이스팅을 이용하면 다음과 같이 코드를 작성할 수 있다.
1 | function add(a, b) { |
위와 같이 return 문 아래에 정의한 함수도 실행이 가능하다. 비교적 복잡한 코드를 하단부에 정의하고 실행부 코드는 깔끔하게 유지하는 등으로 활용할 수도 있다.
괄호없이 즉시 실행하기
1 | (function (a) { |
자바스크립트에서는 위와 같이 괄호를 통해 익명 함수를 즉시 실행할 수 있다. 괄호 없이 실행하면 에러가 발생한다. 많은 경우, 참조가 잘못되어 에러가 났다고 생각할 수 있지만 그렇지 않다. 에러가 난 이유는 익명 함수를 잘못 실행한 것이 아니라 익명 함수 선언 자체가 실패했기 떄문이다.
1 | function() {} |
실행 없이 선언만 시도해도 에러가 난다. 그런데 우리는 이와 비슷한데 에러가 나지 않는 코드를 봤었다.
1 | function f1() { |
이 예제는 1장에서 봤었던 함수를 값으로 다루는 패턴 중 하나다. 위 코드는 함수를 괄호로 감싸지 않았는데 문법 에러가 나지 않고 정상적으로 동작한다. 이 상황에서 에러가 나지 않는다면 괄호 없이 즉시 실행도 되지 않을까?
1 | function f1() { |
이 코드는 정상적으로 동작한다. f1이라느느 함수 안에 있는 익명 함수는 괄호 없이도 즉시 실행이 되었다. 만일 f1이라는 함수의 return 바로 뒤에서 함수를 즉시 실행하고 싶다면, 그 상황에서는 괄호 없이도 익명 함수를 즉시 실행할 수 있다.
1 | !function (a) { |
위와 같은 상황들에서는 괄호 없이도 익명 함수를 즉시 실행할 수 있다. 이 중 !를 이용한 방법은 꽤 알려진 편이다. 이 상황에서의 공통점은 무엇일까? 일단 모두 연산자와 함께 있고, 함수가 값으로 다뤄졌다. 그리고 모두 익명 함수 선언에 대한 오류가 나지 않는다. 앞에서 즉시 실행이 실패했던 것은 익명 함수를 잘못 실행한 것이 아니라 익명 함수 선언 자체를 하지 못해서였다.
유명(named) 함수
1 | var f1 = function f() { |
함수를 값으로 다루면서 익명이 아닌 f() 처럼 이름을 지은 함수를 유명 함수라고 한다. 함수를 즉시 실행한다거나 함수를 클로저로 만들어 리턴할 때, 함수를 메서도로 만들 때는 주로 익명 함수를 사용하게 된다. 이와 같은 상황에서 익명 함수 대신 유명 함수로 사용하는 것이 유용할 때가 있다.
1 | var f1 = function () { |
이렇게 하면 참조가 가능하지만 ‘위험 상황’ 부분처럼 함수 생성 이후 변경이 일어나면 더 이상 자기 자신을 참조하지 못하게 될 수 있다.
1 | var f1 = function () { |
위 방법은 이 전 코드의 문제를 해결하지만 arguments.callee는 ES5 Strict mode에서 사용할 수 없다. 유명 함수식을 사용하면 arguments.callee를 대체할 수 있다. 유명 함수는 함수가 값으로 사용되는 상황에서 자신을 참조하기 매우 편하다. 함수의 이름이 바뀌든 메서드 안에서 생성한 함수를 다시 참조하고 싶은 상황이든 어떤 상황에서든 상관없이 자기 자신을 정확히 참조할 수 있다.
1 | var f1 = function f() { |
유명 함수를 이용한 재귀
유명 함수는 재귀를 만들 때에도 편리하다. 다음은 깊이를 가진 배열을 펴 주는 flatten 함수다. 아래와 같은 함수를 만들 때 재귀와 유명 함수는 특히 유용하다.
1 | function flatten(arr) { |
- flatten 함수가 실행되면 먼저 즉시 실행할 f 라는 이름의 유명 함수로 만든다.
- 함수 f를 즉시 실행하면서 새로운 배열 객체를 생성하여 넘겨준다.
- 루프를 돌면서 배열이 아닐 때만 값을 push하고 배열인 경우에는 f를 다시 실행하여 배열을 펴고 있다.
이 코드가 재밌는 점은 즉시 실행과 유명 함수를 이용한 재귀라는 것이다. 만일 재귀로만 이 로직을 구현한다면 함수를 사용하는 개발자가 빈 배열을 항상 직접 넘겨주거나 if문을 체크하는 식으로 재귀를 제어해야 한다.
1 | function flatten2(arr, new_arr) { |
세 가지 방식의 코드 모두 장단점이 있다. flatten2는 if가 없고 가장 빠르지만 함수를 사용할 때 개발자가 직접 배열을 넘겨주어야 한다. flatten3은 사용하기 간단하지만 if가 있다. flatten은 if가 없으면서 사용하기 간단하지만 함수를 한 번 생성한다.
자바스크립트에서 재귀의 아쉬움
재귀를 이용하면 복잡한 로직이나 중복되는 로직을 제거할 수 있고 읽기 쉬운 로직을 만들 수 있어 편하다. 그러나 아직까지는 자바스크립트에서 재귀를 사용하는 것에 약간 부담스러운면이 있다. 환경에 따라 다르지만 대략 15,000번 이상 재귀가 일어나면 ‘Maximum call stack exceeded’라는 에러가 발생하고 소프트웨어가 죽는다. 따라서 자바스크립트에서 얼마나 깊은 재귀가 일어날 것인가 유의하며 함수를 작성해야 한다.
아직 자바스크립트의 실제 동작 환경에서는 꼬리 재귀 최적화가 되지 않았다. 그렇다고 자바스크립트에서 성능 때문에 재귀를 사용할 일이 없다는 것은 잘못된 얘기다. 자바스크립트의 실제 동작 환경에서는 비동기 프로그래밍이 많이 쓰이고 비동기가 일어나면 스택이 초기화 된다. 애초에 비동기 상황이었다면 어차피 스택이 초기화 될 것이므로 재귀 사용을 피할 이유가 없다.
함수 실행과 인자 그리고 점 다시 보기
() 다시 보기
함수를 실행하는 방법에는 (), call, apply가 있고, 함수 안에서는 arguments 객체와 this 키워드를 사용할 수 있다. 각각의 사용법과 용도, 특이사항들을 하나씩 확인해 보자.
1 | function test(a, b, c) { |
함수 실행 방법에 따른 차이를 정확히 확인하기 위해 test 함수를 만들었다.
1 | test(10); // 1 |
arguments는 함수가 실행될 때 넘겨받은 모든 인자를 배열과 비슷한 형태로 담은 객체다. length로 넘겨받은 인자의 수를 확인할 수 있고 index로 순서별 인자를 확인할 수 있다. 2의 경우 1과 거의 유사하지만 arguments 객체가 다르게 생성이 된다. 인자로 undefined를 직접 넘긴 경우와 넘기지 않아 자연히 undefined 상태가 되는 것 사이에는 분명한 차이가 있다.
인자 다시 보기
인자는 일반 변수 혹은 객체와 약간 다르게 동작하는 부분이 있다.
1 | function test2(a, b) { |
인자는 변수와 달리 객체의 값이 바뀐다. 2는 arguments[1]에 해당하는 값이 넘어왔고, 인자인 b와 arguments[1]은 서로 마치 링크가 걸린 것 처럼 연결되어 있다. b를 고치니 arguments[1]도 바뀌었다. 1에서도 b를 고쳤는데 arguments[1]에 영향을 주지 않는다. 이 부분에 대해서 정확히 알고 있지 않은 상태에서 인자를 변경하는 코드를 작성할 경우, 의도와 다른 상황이 일어날 수 있을 것이다.
this 다시 보기
위의 test 함수 내부의 console.log로 확인 하는 예제에서는 모든 this가 window 객체이다. 어떻게 해야 this에 다른 값이 들어갈 수 있을까?
1 | var o1 = { name: "obj1" }; |
기존에 있던 test 함수를 o1에 연결한 후 o1.test를 실행하니 this가 o1이 되었다. a1 역시 연결 후 실행하니 this가 a1이 되었다. 자바스크립트에서는 객체에 함수를 붙인 다음 그 함수를 . 으로 접근하여 실행하면 함수 내부의 this가 . 왼쪽의 객체가 된다.
1 | var o1_test = o1.test; |
o1.test를 o1_test에 담은 다음 . 없이 o1_test를 실행했더니 this가 다시 window가 되었다. 이런 차이를 알아야 함수를 값으로 잘 다룰 수 있다. 실제로 메서드로 정의된 함수를 일반 함수처럼 사용하는 경우가 있다. o1.test에 붙였기 때문에 o1이 this가 되는 것이 아니라 . 으로 접근하여 실행했기 때문에 o1이 this가 되는 것이다. 어디에 붙어 있는 함수인지보다 어떻게 실행했는지가 중요하다.
1 | (a1.test)(8, 9, 10); |
괄호로 전체를 감쌌지만 여전히 this는 a1이 찍히고 있다. 참조를 어떻게 했느냐가 중요하다. [] 를 이용해 test 메서드를 참조 후 실행해도 . 으로 접근하여 실행한 것과 동일한 결과를 낸다.
자바스크립트에서의 함수는 ‘어떻게 선언했느냐’와 ‘어떻게 실행했느냐’가 모두 중요하다. ‘어떻게 정의했느냐’는 클로저와 스코프와 관련된 부분들을 결정하고 ‘어떻게 실행했느냐’는 this와 arguments를 결정한다.
call, apply 다시 보기
자바스크립트에서 함수를 실행하는 대표적인 방법이 2개 더 남아 있다.
1 | test.call(undefined, 1, 2, 3); |
위 3가지 실행 모두 동일한 결과가 나온다. null 이나 undefined를 call의 첫 번째 인자에 넣으면 this는 windnow가 된다. void 0 의 결과도 undefined 이기 때문에 같은 결과가 나온다.
1 | test.call(o1, 3, 2, 1); |
함수의 메서드인 call은 Function.prototype.call 이다. test는 함수이자 객체이고 test 객체의 call은 함수 자신(test)을 실행하면서 첫 번째 인자로 받은 값을 this로 사용한다.
1 | o1.test.call(undefined, 3, 2, 1); |
call을 사용할 경우, 그 앞에서 함수를 .으로 참조했을지라도 call을 통해 넘겨받은 첫 번째 인자에 의해 this가 결정된다.
1 | test.apply(o1, [3, 2, 1]); |
apply는 call과 동일하게 동작하지만 인자 전달 방식이 다르다. 인자들을 배열이나 배열과 비슷한 객체를 통해 전달한다. 여기서 배열과 비슷하다는 것은 다음과 같은 값들을 사용할 수 있다는 말이다.
1 | test.apply(o1, { 0: 3, 1: 2, 2: 1, length: 3 }); |
{ 0: 3, 1: 2, 2: 1, length: 3 }은 Array도 아니고 Arguments도 아닌 그냥 일반 객체다. 숫자를 키로 사용하고 그에 맞는 length를 가지고 있다. 이와 같이 되어 있는 객체라면 apply를 통해 인자로 전달할 수 있다. 다른 함수를 통해 생성된 arguments도 apply로 전달할 수 있다.
call의 실용적 사례
계속해서 확인하고 있는, 일반적이지 않은 이런 기법들은 유명한 자바스크립트 개발자들의 코드에서 자주 등장한다.
1 | var slice = Array.prototype.slice; |
Array.prototye.slice의 경우, 키를 숫자로 갖고 length를 갖는 객체이기만 하면 Array가 아닌 값이어도 call을 통해 Array.prototype.slice를 동작시킬 수 있다. toArray와 rest 함수는 구현을 Native Helper
에게 위임하여 짧은 코드로 성능이 좋은 유틸 함수를 만들었다.
자바스크립트에서는 this 키워드 못지않게 call, apply, arguments 등도 중요하다. call, apply, arguments, bind 등을 알고 자바스크립트를 다루는 것과 그렇지 않은 것은 정말 큰 차이를 만든다. 그리고 이 모든 기능들은 자바스크립트의 함수와 관련되어 있다. 자바스크립트 진영의 객체지향 관련 라이브러리에도 상속이나 메서드 오버라이드 같은 것을 구현하기 위해서는 apply와 arguments 등을 사용해야 한다. 함수형 자바스크립트에서는 특히나 중요하다. apply, arguments는 좋은 도구들이며 실제로 매우 실용적이다.
Conclusion
오늘은 함수형 프로그래밍을 시작하기 앞서 기본적인 것들을 다시 확인해보는 시간이었다. 이미 알고 있던 내용도 있었지만, 복습하고 확실히 익히고자 하는 마음으로 정리했다. 해당 챕터의 내용이 많아 나머지 내용은 다음 시간에 정리해야겠다.
참조: 함수형 자바스크립트 프로그래밍