본문 바로가기
👩‍💻 Programming/JavaScript

함수 장인이 되는 길_함수 호출에 대한 이해 from Secrets of the JavaScript Ninja

by codingBear 2022. 4. 6.
728x90
반응형

이번 글은 'Secrets of the JavaScript Ninja'의 'Chapter 4. Funtions for the journeyman: understanding function invocation'를 바탕으로 작성하였습니다.


핵심 Concepts

  • function의 내재된 parameter 2가지: argumnets & this
  • 함수 호출 방법
  • function contexts에 생긴 문제 해결

사전 지식 체크!

  • this parameter가 function context로 알려진 이유?
  • function과 method의 차이?
  • constructor function이 명확하게(explicitly) object를 반환하면 생기는 일?

들어가며

 이번 글에서는 내재적인(implicit) function parameter인 thisarguments에 대해 알아보겠다. this parameter는 function context로서 function이 호출되는 객체를 뜻한다. arguments parameter는 함수 호출에 전달되는 모든 arguments를 뜻한다. 특히 this parameter는 객체 지향 프로그래밍(Object-Oriented-Programming)의 핵심이다. 

 함수를 호출하는 방법은 이 두 가지 내재적 함수가 어떻게 정의되는지 지대한 영향을 끼친다.


Using implicit function parameters

 arguments parameter와 this parameter는 function상에 명확히 드러나지 않으나 아무도 모르게 function에 전달되어 function 내에서 언제든지 접근 가능하다. 다른 명시적인 parameter와 마찬가지로 function 내에서 참조될 수 있다.


The arguments parameter

 arguments parameter는 function에 전달되는 모든 arguments의 모음(collection)이다. 명시적인 parameter와 일치되는지 여부와 상관 없이 모든 function arguments에 접근 가능하게 해주기 때문에 매우 유용하다. 이는 JavaScript가 원래는 지원하지 않는 function overloading도 가능케 하고, variable number of arguments를 받는 variadic function도 허용한다. 사실 rest parameters가 등장하면서 arguments parameter의 필요성이 현저히 줄었으나 arguments parameter의 작동 원리를 아는 것은 legacy code를 다루는 데 매우 중요하다.

 arguments parameter는 arguments의 개수를 나타내는 데 쓰이는 length라는 property를 갖는다. 여러 개인 arguments 중 특정한 하나를 얻으려면 배열의 index number를 활용하면 된다. 예제 코드를 보면서 이 내용에 대해 알아보자.

 

 

  1. 위 예제 코드 에서 whatever function의 argument는 5개('1, 2, 3, 4, 5'), parameter는 3개('a, b, c')
  2. argument는 왼쪽부터 차례대로 선언된 parameter 3개에 각각 할당
  3. arguments.length로써 arguments 개수 확인 가능
  4. 배열 index number를 활용해 특정 값에 접근 가능(parameter에 할당되지 않은 초과 argument도 포함)

 위 예제 코드를 보면 알 수 있듯 arguments parameter는 length property로 개수도 구할 수 있고, index number를 활용해 특정 값에 접근도 가능하다. 이 같은 특성에 따라 arguments parameter를 array로 착각하기 쉽지만 둘은 전혀 다르다. arguments parameter에 array function(예를 들어 sort)을 쓰려고 하면 에러 메시지가 출력된다. 따라서 arguments를 '배열과 닮았고 사용에 제약이 있는 것' 정도로 이해하면 된다. 여기서 주의할 점은 앞서 살펴봤던 rest parameter는 배열이라는 점이다. 즉 배열 함수를 rest parameter에 써먹을 수 있다는 말이다. 

 앞서 언급했듯 function parameter에 arguments가 할당되었는지 상관 없이 arguments parameter 내 모든 argument에 접근 가능하다. 예제 코드를 활용해 주어진 arguments parameter를 모두 더하는 기능을 구현해보자.

 

 

 위 예제 코드를 보면 sum() 함수에 아무런 parameter도 선언되어 있지 않지만 함수에 부여된 모든 arguments에 for문을 활용하여 접근 가능하다. 위 예제 코드와 똑같은 기능을 rest parameter를 활용해서도 구현 가능하다.


Arguments object as an alias to function parameters

arguments parameter는 한 가지 희한한 특성을 가진다. 바로 function parameter에 할당된 값을 변경한다는 점이다. 아래 예제처럼 argument[0]에 새로운 값을 부여하면 첫 번째 function parameter 역시 새롭게 부여한 값으로 바뀐다. 다음 예제를 통해 자세히 알아보자.

 

  1. function parameter 'person'과 argument[0] 모두 'gardener'라는 값을 가짐
  2. arguments object는 function parameter의 에일리어스(alias)
    1. 따라서 arguments[0]의 값을 'ninja'로 변경하면 function parameter 'person'의 값도 함께 바뀜
    2. 마찬가지로 function parameter 'person'을 'gardener'로 변경하면 argument[0]의 값도 'gardener'로 바뀜

Avoiding aliases

 arguments object를 통한 function parameter의 에일리어싱(aliasing)​ 개념은 다소 헷갈릴 수 있다. JavaScript는 이를 피하기 위해 strict mode를 제공한다.

Strict mode
ES5에 추가된 기능. strict mode로 설정하면 arguments의 aliasing이 방지됨.

 

 예제 코드 맨 윗줄에 use strict라는 문구가 추가된 것을 볼 수 있다. 이 문구는 JavsScript 엔진에 strict mode를 실행하라고 알려준다. strict mode가 실행되면 평상시와는 달리 arguments object의 값이 바뀌어도 function parameter에 반영되지 않는다.


The this parameter: introducing the funciton context

함수가 호출될 때 명시적으로 선언된 parameters 외에도 this라는 내재적 parameter도 function에 전달된다. this parameter는 객체 지향 JavaScript의 핵심 요소이며 함수 호출과 관련된 객체를 참조하는데 이 때문에 function context라고도 부른다.

 this parameter는 Java 같은 객체 지향 언어에서 비롯된 개념인데 일반적으로  method가 정의된 class의 instance를 가리킨다. JavaScript에서 method로서 function을 호출하는 것은 function을 호출하는 여러 방법 중 하나에 불과하다. 추후 알게 되겠지만 this parameter가 가리키는 것은 Java 혹은 C#에서와 같이 함수가 정의된 방법과 위치에 의해서만 정해지지 않는다. 함수가 호출되는 방식에 의해서도 크게 영향을 받는다.


​Invoking functions

 JavaScript에서 함수를 호출하는 방법에는 크게 4가지가 있다.

  • function: example() 가장 직관적이며 간단한 방법
  • method: example.name() 객체 지향 프로그래밍과 밀접한 관련이 있음
  • contructor: new Example() 새로운 객체를 불러냄
  • apply or call methods: example.call(name) or example.apply(name)

 

 

 callapply는 function reference로 여겨지는 표현들을 괄호로 묶어 나타내는 function invocation operator이다.


Invocation as a function

 function을 function으로서 호출한다는 말을 들으면 무슨 바보 같은 소리인가 싶을 것이다. 하지만 다른 호출 방법들(method, constructors, apply/call)과 구분 짓기 위해서 'function으로서' 호출된다고 명확히 말해야 한다. function은 () operator로 호출하며 () operator가 적용된 표현은 function을 객체의 property로 참조하지 않는다.

 

 

 여기서 function context(this keyword의 값)은 크게 2가지이다.

  • nonstrict mode: global context(window object)
  • strict mode: undefined

 다음 예제 코드는 nonstrict mode와 strict mode의 차이점을 보여준다.

 

 

 strict mode는 대부분의 경우 nonstrict mode보다 직관적이다. 위 예제 코드에서 function은 function으로서 호출되는데 function을 호출해야 하는 객체가 지정되지 않았다. 따라서 this keyword의 값은 global window object가 아니라 undefined이 되는 게 합리적이다. 일반적으로 strict mode 하에서 JavaScript의 자잘한 버그들을 잡을 수 있다.


Invocation as a method

function은 object의 property에 할당되어 그 property를 사용하는 function을 참조함으로써 호출된다. 이러한 방식을 method로서 function을 호출한다고 한다. 

 

 

 function을 위 예제 코드처럼 method로서 호출하면 어떠한 장점이 있을까? 객체 지향 문법에 대한 배경지식이 있다면 method가 속한 object는 method 내용 안에서 this로 사용할 수 있음을 알 것이다. 같은 일이 function을 method로 호출할 때도 일어난다. function을 object의 method로 호출할 때, 해당 object는 function context가 되고 this parameter로써 function 안에서 사용 가능하다. 이는 JavaScript가 객체 지향 코드를 작성할 수 있게끔 하는 주요 수단 중 하나이다.

 다음 예제 코드를 통해 function을 function으로 호출하는 방법과 method로 호출하는 방법 간의 차이점과 유사점을 살펴보자.

 

 

 위 예제 코드에서 whatsMyContext function은 this로써 function context를 반환하여 사용자가 해당 function의 context가 무엇인지 알 수 있게 해준다. function의 이름으로 function을 바로 호출했을 때는 function을 function으로서 호출한 것으로 nonstrict mode 하에서 function context는 global context(window)가 된다. 그리고 getMythis라는 변수에 whatsMyContext function에 대한 참조를 생성한다. 이것은 function의 두 번째 instance를 생성하지 않고 같은 function에 대한 참조만을 생성한다.

 이렇게 참조를 생성한 변수로 function을 호출하면 function으로서 호출한 게 된다. 따라서 이렇게 호출한 function의 context는 앞의 경우와 마찬가지로 window가 된다.

 다음으로 조금 복잡한 object를 ninja1이라는 변수에다 whatsMyContext function에 대한 참조를 받는 getMyThis라는 property와 함께 정의한다. 이렇게 함으로써 object에 getMyThis라는 method를 생성하였다. whatsMyContext는 ninja1의 method가 아니다. whatsMyContext function은 여러 방법으로 호출되는 독립된 function이기 때문이다. 앞서 언급했듯 function을 method reference를 통해 호출할 때 function context는 해당 method의 object가 된다. (예제 코드의 경우 ninja1) function을 method로 호출하는 것은 JavaScript에서 객체 지향 문법을 구사할 때 핵심적인 개념이다. method로 function을 호출함으로써 method가 갖고 있는 object를 참조하기 위해 어떤 method 내에서든 this를 사용할 수 있게 된다.

 whatsMyContext function은 어떻게 호출되느냐에 따라 this에 의해 반환되는 function context가 달라진다. 예를 들어 똑같은 function이 ninja1과 ninja2에 공유되어도 function이 실행될 때 function에 접근할 수 있으며 method가 호출된 object에서도 작업을 수행할 수 있다. 각자의 object 내에서 같은 기능을 수행하는 제가끔의 function을 별도로 마련할 필요가 없다. 이것이 바로 객체 지향 프로그래밍의 핵심이다. 

 허나 이 같은 방식에도 제약은 있다. 무엇보다 두 가지 ninja object를 만들 때, 각자의 method로 쓰이는 같은 function을 공유할 수 있지만, 각각 object와 그 method를 설정하기 위해 유사하게 반복되는 코드를 사용해야 한다. 이 같은 문제를 앞으로 살펴볼 constructor를 통해 해결할 수 있다.


Invocation as a constructor

 function을 constructor로 쓰는 데 특별한 점은 없다. 여타 fucntion들처럼 선언하면 된다. 단 하나 예외는 arrow function이다. 아무튼 가장 큰 차이는 어떻게 function을 호출하는가이다. function을 constructor로 호출하려면 앞에 new라는 keyword를 붙여야 한다. 아래 예제를 통해 일반적인 function을 constructor로 호출하는 법에 대해 알아보자.

 

function constructor와 constructor function은 엄연히 다르다. function constructor는 문자열로부터 새로운 function을 생성하게 해주는 함수 선언 방식이다. 예를 들어 new Function('a', 'b', 'return a + b')와 같이 작성하면 parameter 두 개와 그 합을 반환하는 새로운 function이 생성된다. 즉 문자열로부터 function을 동적으로 생성하는 것이다. 반면 contructor function은 object instances를 생성하고 초기값을 설정하는 데 쓰인다.

The superpowers of contructors

 new keyword로 function을 호출할 때 새로운 빈 object instance가 생성됨과 동시에 function context인 this parameter가 function에 전달된다. function이 할당되어 그 function을 새롭게 생성된 object의 method로 만드는 skulk라는 property를 해당 object에 contructor는 생성한다. 

 일반적으로 constructor가 호출될 때 몇 가지 특별한 작업이 수행된다. new keyword로 function을 호출할 때 일어나는 일의 순서는 다음과 같다.

 

  • 빈 object 생성
  • object는 this parameter로서 constructor에 전달되고 constructor의 function context가 됨
  • 생성된 object는 new operator의 값으로 반환

 

 constructor를 생성하는 목적은 새로운 object를 만들어 constructor의 값으로 반환하는 것이다. 위에 제시된 예제 코드를 부분부분 살펴보며 이해를 깊이 해보자.

 

 

 skulk method는 앞에서 살펴봤던 whatsMyContext function과 똑같은 기능을 수행하여 function context를 반환한다. constructor를 정의함과 함께 constructor를 두 차례 호출함으로써 새로운 Ninja object를 두 개 생성한다. 호출을 하여 반환된 값들은 새롭게 생성된 Ninja object에 대한 참조가 되는 변수에 저장된다.

 

 

 그러고 나서 아래 테스트 코드를 실행한다.

 

 

  new keyword와 함께 function을 호출하면 새롭게 생성된 object를 반환한다. 하지만 늘 새롭게 생성된 object만을 반환하는지 다음 차례에서 살펴보자.


Constructor return values

 constructor는 새로운 object를 생성하고 새롭게 생성된 object는 new operator를 통해 constructor 호출의 결과값으로 반환된다. constructor가 자신의 값을 반환할 때 무슨 일이 생길까?

 

 

 위 예제 코드에서 function을 단순히 function으로서 호출하면 결과값 '1'이 반환된다. 만약 new keyword를 통해 constructor로 호출한다면 새로운 ninja object가 생성되어 반환된다. 

 위 예제와 조금 다른 예를 한번 살펴보자. constructor function이 다른 object를 반환하는 경우이다.

 

 

 위 예제 코드의 접근 방법은 조금 다르다. 단계별로 살펴보자.

 

  1. property 'rules: false'를 갖는 puppet object 생성
  2. Emperor function을 정의하여 새롭게 생성된 object의 property rules를 'true'로 수정
  3. 해당 function은 global puppet object를 반환
  4. Emperor function을 new keyword를 통해 constructor로 호출

 

 위의 과정을 차례차례 밟아나갔다면 이상한 점을 발견했을 것이다. constructor로 호출한 emeperor function은 this를 활용해 object의 초기값을 'true'로 설정했으나 전혀 다른 puppet object를 반환한다. 즉 호출된 constructor은 puppet object를 반환한다는 것이다. 

 위에서 살펴본 예제 코드들을 통해 다음과 같은 결론을 도출할 수 있다.

 

  • constructor가 object를 반환하는 경우, 해당 object는 new로써 생성된 contructor의 결과값으로 반환된다. 새로 만든 object에 this로써 constructor에 전달되었던 object는 삭제된다.
  • constructor가 object 이외의 값을 반환하는 경우 해당 반환값은 무시되고, 새롭게 생성된 object가 반환된다.

 

constructor의 초기 return 값  constructor의 최종 return 값 
object 초기 object
object 이외의 값 새롭게 생성된 object

 

 이러한 특징 때문에 constructor function은 일반적으로 다른 function들과 다르게 작성된다. constructor function의 작성 방법에 대해 다음 내용에서 자세히 살펴보자.


Cding considerations for constructors

 constructor로 호출되는 function은 일반적인 function과 구분 짓기 위해 명명법이 따로 있다. 일반적인 function이나 method의 이름으로는 해당 function의 기능을 나타내는 동사를 주로 쓰고 소문자로 시작한다. 반면 contructor는 object를 나타내는 명사를 이름으로 붙이고 대문자로 시작한다.


Invocation with the apply and call methods

 이때까지 살펴봤듯 function 종류의 주된 차이점은 실행 중인 function에 전달되는 this parameter에 의해 참조되는 function context로 끝나는 object이다. method는 해당 method만의 object를 갖고 function의 최상위 계층은 strict mode에 따라 window 혹은 undefined을 갖는다. constructor는 새롭게 생성된 object instance를 갖는다.

 지금부터는 event handler가 호출될 때 function context는 event가 바인딩된 object로 설정된다는 점을 고려해주길 바란다. 예제를 통해 해당 내용을 살펴보자.

 

 

 위의 button object에서는 button이 클릭될 때 발생되는 event handler로 기능하는 click method를 정의한다. 해당 method의 clicked 값은 'true'로 설정한다. 즉 button이 클릭되면 click method가 발동되어 clicked property를 'true'로 바꾸는 구조이다.

 허나 위 코드는 잘 작동하지 않는다. click function의 context가 우리가 의도한 button object를 참조하지 않기 때문이다. 

 앞선 내용을 떠올려보자. 우리가 fucntion을 button.click()을 통해 호출한다면 context는 button이 될 것이다. 왜냐하면 function은 button object의 method로써 호출되었기 때문이다. 그러나 이 예제에서 브라우저의 event-handling 시스템은 호출의 context를 event의 target element로 정의하여 context가 button object가 아닌 button element가 되도록 한다. 즉 click 상태를 잘못된 object에다 설정한 것이다.

 위 예제 코드에서 나타난 오류를 잡기 위해 arrow function과 bind method를 활용할 수 있다. 아래 예제들을 통해 자세히 알아보자.


Using arrow functions to get around function contexts

arrow function은 code를 간결하게 작성할 수 있다는 특징 이외에도 callback funtion을 더 원활히 기능하게 해준다는 특징이 있다. arrow function은 자신의 this parameter 값을 가지지 않는다. 대신 처음 정의될 때의 this parameter 값을 상속받아 저장한다. 

 

 

 위 코드에서 유일하게 바뀐 점은 click method의 function declaration을 arrow function으로 변경한 것이다. 앞서 언급했듯 arrow function은 호출될 때 자신만의 내재된 this parameter를 가지지 않고 생성될 당시의 this parameter 값을 저장한다. 예제 코드의 경우 click arrow function이 constructor function 내에 생성되며, this parameter는 새롭게 생성된 object이고, 사용자 혹은 브라우저가 click function을 호출할 때마다 this parameter의 값은 항상 그때마다 새로 생성되는 object에 바운딩되는 것이다. 


Caveat: arrow functions and object literals

 앞선 예제는 클릭을 하여 constructor가 생성될 때마다 this값이 arrow function에 주어지는 코드였다. 다음 예제를 보면서 arrow function이 global object를 반환하는 경우를 알아보자.

 

 

 위 코드를 실행하면 다음과 같은 test 결과값이 나온다.

 

 

  arrow funtion은 해당 arrow function이 생성될 때마다 this parameter의 값을 가져온다. 여기서 click arrow function은 object literal의 property 값으로 만들어지며 object literal은 global code안에 생성된다. 따라서 arrow function의 this값은 globalc code의 this값인 window object가 되는 것이다. 따라서 clicked property는 button object가 아닌 window object에 정의된다.


Using the bind method

앞서 살펴본 적 있는 apply나 call과 같이 모든 function에서 bind method에 접근 가능하다. 아래 예제 코드는 위 예제 코드와 내용은 같으나 어떻게 호출되는지 상관 없이 context는 항상 특정한 object에 바운딩되어 있다.

 

 

 bind method는 모든 function에서 사용 가능하다. 또한 전달된 object(여기서는 button object)에 바운딩된 새로운 function을 생성하고 반환하게끔 설계되었다. this parameter의 값은 bound function이 어떻게 호출되는지 상관 없이 항상 해당 object에 설정된다. bound function은 원래 function처럼 기능하는데 그 까닭은 code의 내용이 똑같기 때문이다.

 button object는 bind의 argument로 쓰여 버튼이 클릭될 때마다 button object가 context로서 호출된다.

 bind method를 호출해도 원래 function의 내용을 수정하지 않는다는 점에 주목하자. bind method는 완벽히 새로운 function을 생성한다.


Using the apply and call methods

 JavaScript는 function을 호출하고 function context로 사용할 어떠한 object든 명확히 지정하는 수단을 제공한다. 바로 apply와 call이라는 두 가지 method이다. 

 function은 method를 포함한 다른 object 유형과 마찬가지로 property를 가진다.

 apply method를 사용하여 function을 호출하려면 apply에 두 가지 parameter를 전달해야 한다. 첫 번째 parameter는 function context로 쓰일 object이고 두 번째 parameter는 호출될 arguments로 쓰일 값들로 이루어진 배열이다. call method도 apply method와 유사하지만 arguments를 배열 형태가 아닌 곧바로 입력한다는 점이 다르다.

 

 

 위 예제 코드에서 juggle function을 정의한다. 이 function은 주어지는 arguments를 모두 합하고 this keyword에 의해 참조되는 function context에 해당 합을 property로 저장한다. 그리고 ninja1과 ninja2라는 object 두 개를 만들어 각각 apply method와 call method의 function context로 사용한다. 

 apply와 call은 우리가 선택한 function context를 활용할 수 있고 특히 callback function을 호출할 때 유용하다.


Forcing the function context in callbacks

 우리가 선택한 object를 function context로 강제하는 구체적인 예제를 살펴보자.

 

 

 functional style을 보다 효과적으로 만들기 위해 모든 array object는 array 안의 각 요소를 콜백하는 forEach function에 접근할 수 있어야 한다. 이 같은 반복 function은 현재 요소를 parameter로서 콜백에 전달 가능하다. 하지만 대부분은 현재 요소를 콜백의 function context로 만든다.

 

 

 반복 function은 첫 번째 argument로 반복될 것으로 예상되는 array object가 오고, 두 번째 argument로는 callback이 온다. function은 array entry를 반복하면서 각 entry에 대한 callback function을 호출한다.

 여기서 callback funtion의 call method를 사용하는데 현재 반복되는 entry를 첫 번째 parameter로 전달하고 loop index를 두 번째로 전달한다. 테스트문을 실행해보면 아래와 같이 성공적으로 결과값이 반환됨을 볼 수 있다.

 

 

 apply와 call은 거의 같은 method로 보인다. 그렇다면 어떻게 쓰임새를 구분할까? 변수에 관련 없는 값이 많거나 literal로 지정된 경우, call은 argument list에 arguments를 직접 나열할 수 있게 해준다. 반면 이미 array 안에 argument 값들을 갖고 있거나 arguments를 모으는 것이 더 편하다면 apply가 더 적절할 것이다.


함께 보기

  • apply method

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/applyhttps://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply

 

Function.prototype.apply() - JavaScript | MDN

apply() 메서드는 주어진 this 값과 배열 (또는 유사 배열 객체) 로 제공되는 arguments 로 함수를 호출합니다.

developer.mozilla.org

  • call method

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call

 

Function.prototype.call() - JavaScript | MDN

The call() method calls a function with a given this value and arguments provided individually.

developer.mozilla.org

  • this

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

 

this - JavaScript | MDN

A function's this keyword behaves a little differently in JavaScript compared to other languages. It also has some differences between strict mode and non-strict mode.

developer.mozilla.org

  • constructor

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor

 

Object.prototype.constructor - JavaScript | MDN

The constructor property returns a reference to the Object constructor function that created the instance object. Note that the value of this property is a reference to the function itself, not a string containing the function's name.

developer.mozilla.org

 

728x90
반응형

댓글