프론트엔드

ECMAScript6의 전달인자와 매개변수 사용

CyberI 2016. 11. 1. 15:11


외국 사이트에서 ECMAScript 6 의 전달인자와 매개변수에 관한 글을 봤는데, 알아두면 유용할 것 같아서 일부를 번역하였습니다. 이전 버전인 ECMAScript 5와의 비교를 통해서 어떻게 달라졌는지 아주 간결하고 알기쉽게 설명하고 있습니다. 이 글에서 설명하고 있는 구문은 크롬, 파이어폭스의 비교적 최신 버전이나 엣지 브라우저에서 동작하는 것을 확인할 수 있습니다. 


ECMAScript 6 에서 전달인자와 매개변수를 사용하는 방법


ECMAScript 6 (ECMAScript 2015) 는 ECMAScript 표준의 새로운 버전이며, 자바스크립트 파라미터 핸들링이 눈에 띄게 향상되었다. 이제 여러가지 새로운 특징 중에서 rest parameters, default values, destructuring을 사용할 수 있다.


전달인자(Arguments) VS 매개변수(Parameters)

전달인자(Arguments)와 매개변수(Parameters)는 종종 상호 교환가능하다고 언급된다. 그렇지만, 여기서는 이해를 돕기 위해서 명확한 차이를 둔다. 대부분의 표준에서, 매개변수(형식상의 파라미터;formal parameters)는 함수를 선언할 때 주어지는 것이고, 전달인자(실제의 파라미터;actual parameter)는 함수에 넘겨지는 것이다. 

  1. function foo(param1, param2) {
  2.  
  3. return param1+param2;
  4.  
  5. }
  6.  
  7. foo(1020);

이 함수에서, param1과 param2는 함수의 매개변수(parameter)이고, 함수에 넘겨지는 10과 20이라는 값은 전달인자(argument)이다.


전개연산자(Spread Operators)

ECMAScript 5에서,  apply( 메소드는 함수에 배열(array)을 전달인자로 넘겨주기 위한 편한 도구였다. 예를 들어,  Math.max() 메소드로 배열안에서 가장 높은 값을 찾을 때 흔히 사용된다. 

  1. var array = [1,2,3];
  2. Math.max(array)// Error: NaN
  3. Math.max.apply(Math, array)// 3

Math.max() 메소드는 배열을 지원하지 않는다. 오직 숫자만 허용한다. 배열이  Math.max() 메소드에 넘겨지면 에러가 발생한다. 그러나  apply(메소드를 사용하면, 배열은 각각의 숫자로 보내지기 때문에,   Math.max() 메소드가 이를 처리할 수 있다. 

운좋게도, ECMAScript6에서 전개연산자(spread operator)를 도입함으로써, 우리는 더이상  apply(메소드를 사용할 필요가 없다. 전개연산자를 사용하면, 여러개의 전달인자를 쓸 때 표현을 확장할 수 있다.

  1. var array = [1,2,3];
  2. Math.max(...array)

여기에서, 전개 연산자는 array 를 확장하여 함수를 위한 각각의 값을 생성한다. 전개연산자를 사용하면서 ECMASCript5의  apply(를 사용하는 것도 가능하지만, 구문(syntax)이 혼란스러워진다. 전개연산자는 사용하기 쉬울 뿐만 아니라, 몇 가지의 특징이 더 있다. 예를 들어, 함수를 호출할 때 다른 전달 인자와 섞어서 사용할 수 있고 여러 번 사용할 수도 있다.

  1. function myFunction(){
  2.     for(var i in arguments){
  3.         console.log(arguments[i]);
  4.     }
  5. }
  6. var params=[2,3];
  7. myFunction(1,...params4, ...[5])// 1,2,3,4,5

전개 연산자의 다른 장점은 생성자에서도 쉽게 사용할 수 있다는 것이다.

new Date(...[2016,10,31])

물론, ECMAScript 5의 이전 코드를 그대로 쓸 수도 있지만, 타입 에러를 피하기 위해 복잡한 패턴을 써야할 필요가 있다.

  1. new Date.apply(null[2016424]);    // TypeError: Date.apply is not a constructor
  2. new (Function.prototype.bind.apply(Date, [null].concat([201656])));   // Mon Jun 06 2016 00:00:00 GMT-0700 (Pacific Daylight Time)


함수 호출 시 전개 연산자를 지원하는 브라우저는 다음과 같다.

-데스크탑: 크롬 46, 파이어폭스 27, 인터넷익스플로러 X, 엣지 지원, 오페라 X, 사파리 7.1

-모바일: 안드로이드크롬 46, 파이어폭스 모바일 27, 사파리 모바일 8, 오페라/IE 모바일 X


Rest parameters

Rest parameters는 전개연산자와 같은 구문을 가지고 있지만, 배열을 파라미터로 펼치는 대신에 파라미터들을 모아서 배열로 바꾼다. 아무런 파라미터도 없는 경우, rest parameter는 빈 배열을 반환한다.

  1. function myFunction(...options) {
  2.      return options;
  3. }
  4. myFunction('a''b''c');      // ["a", "b", "c"]

rest parameter는 특히 variadic function(가변적인 수의 전달인자를 받는 function)을 생성할 때 유용하다. 배열로 할 때의 장점을 가지면서, rest parameters는 손쉽게 arguments 객체를 대체할 수 있다. ECMAScript 5로 쓰여진 아래 함수를 생각해보자.

  1. function checkSubstrings(string) {
  2.   for (var i = 1; i < arguments.length; i++) {
  3.     if (string.indexOf(arguments[i]) === -1) {
  4.       return false;
  5.     }
  6.   }
  7.   return true;
  8. }
  9. checkSubstrings('this is a string''is''this');   // true

이 함수는 특정 문자열이 여러개의 문자 일부분을 포함하고 있는지 확인한다. ('this is a string' 이라는 문자열에 'is'와 'this'가 포함되어 있는지 체크한다.) 이 함수의 첫번째 문제점은 복수의 전달인자를 받는지 보기위해서 function의 내부를 들여다봐야한다는 것이다. 두번째 문제는 iteration이 0이 아닌 1로 시작한다는 것이다. 왜냐하면 arguments[0] 이 첫번째 인자를 가리키기 때문이다. 만약 추후에 다른 파라미터를 문자열의 앞이나 뒤에 추가한다고 할 때, 반복문을 고쳐주는 것을 잊어버릴 수도 있다. rest parameter를 사용하면, 이런 문제들을 쉽게 피할 수 있다.

  1. function checkSubstrings(string, ...keys) {
  2.   for (var key of keys) {
  3.     if (string.indexOf(key) === -1) {
  4.       return false;
  5.     }
  6.   }
  7.   return true;
  8. }
  9. checkSubstrings('this is a string''is''this');   // true

이 함수의 결과는 위의 것과 같다. 여기서 다시, 파라미터 string을 첫 번째로 넘겨지는 전달인자로 채우고, 나머지 전달인자들은 배열에 넣어서 변수 keys에 할당한다.

arguments 객체 대신에 rest parameters를 사용하는 것은 코드의 가독성을 향상시키고 자바스크립트의 최적화 이슈도 피할 수 있다. 그럼에도 불구하고, rest parameter에 제한 없는 것은 아니다. 예를 들어, 마지막 전달인자여야 한다는 것이다. 그렇지 않다면 구문 오류가 발생할 것이다. 

  1. function logArguments(a, ...params, b) {
  2.         console.log(a, params, b);
  3. }
  4. logArguments(51015);    // SyntaxError: parameter after rest parameter

다른 제한은 function 의 선언에서 오직 하나의 rest parameter만 허용한다는 점이다.

  1. function logArguments(...param1, ...param2) {
  2. }
  3. logArguments(51015);    // SyntaxError: parameter after rest parameter


Rest parameter 지원 브라우저

-데스크탑: 크롬 47, 파이어폭스 15, 인터넷익스플로러 X, 엣지 지원, 오페라 34, 사파리 X

-모바일: 안드로이드크롬 47, 파이어폭스 모바일 15, 사파리/오페라/IE 모바일 X


Default parameters

ECMAScript 5에서 자바스크립트는 디폴트 파라미터를 지원하지 않는다. 그러나 쉬운 다른 해결책이 있다. 논리 연산자 (||)를 함수 안에 사용함으로써, ECMAScript 5에서 쉽게 디폴트 파라미터를 사용하는 것처럼 할 수 있다.

  1. function foo(param1, param2) {
  2.    param1 = param1 || 10;
  3.    param2 = param2 || 10;
  4.    console.log(param1, param2);
  5. }
  6. foo(55);  // 5 5
  7. foo(5);    // 5 10
  8. foo();    // 10 10

이 함수는 두 개의 전달인자를 받지만, 전달 인자없이 호출될 때는 디폴트 값을 사용할 것이다. 함수 안에서, 없는 전달인자는 자동으로 undefined로 설정된다. 그래서 전달인자들을 감지하여 디폴트 값을 선언해줄 수 있다. 빠진 전달인자를 감지하고 디폴트 값을 설정하기 위해서 논리 연산자 (||)를 사용한다. 이 연산자는 첫번째 전달 인자를 조사한다. 그것이 true 라면, 그것을 반환하고 만약 아니라면, 연산자는 두번째 인자를 반환한다.

이런 접근법은 함수안에서 흔히 사용되지만, 이는 결함을 가지고 있다. 0이나 null을 전달하면 역시나 디폴트 값이 작동될 것이다. 왜냐하면 이 값들은 false라고 여겨지기 때문이다. 그래서 실제로 함수에 0이나 null을 넘겨야하는 경우, 전달인자가 빠졌는지 여부를 체크해 주는 대책이 필요하다.

  1. function foo(param1, param2) {
  2.   if(param1 === undefined){
  3.     param1 = 10;
  4.   }
  5.   if(param2 === undefined){
  6.     param2 = 10;
  7.   }
  8.   console.log(param1, param2);
  9. }
  10. foo(0null);    // 0, null
  11. foo();    // 10, 10

이 함수 안에서, 디폴트 값을 할당하기 전 넘겨지는 전달인자의 타입을 체크하여 그것이 undefined인지를 확실히 한다. 이 접근법은 조금 더 많은 코드를 요구하지만, 더 안전한 대안이면서 함수에 0이나 null을 넘기도록 해준다.


ECMAScript 6 에서는 더 이상 undefined 값을 체크할 필요가 없어졌다. 이제는 function의 선언부에 직접 디폴트 값을 입력할 수 있다.

  1. function foo(a = 10, b = 10) {
  2.   console.log(a, b);
  3. }
  4. foo(5);    // 5 10
  5. foo(0null);    // 0 null

보이는 것처럼, 전달인자를 생략하는 것은 디폴트 값을 작동시키지만, 0이나 null이 넘겨지는 경우는그렇지 않다. 심지어 디폴트 파라미터는 함수를 사용해서 얻을 수 있다.

  1. function getParam() {
  2.     alert("getParam was called");
  3.     return 3;
  4. }
  5. function multiply(param1, param2 = getParam()) {
  6.     return param1 * param2;
  7. }
  8. multiply(25);     // 10
  9. multiply(2);     // 6 (also displays an alert dialog)

getParam 함수는 오직 두번째 인자가 생략되었을 때만 호출된다는 것을 유의해야 한다. 그래서, multyply() 함수에 2개의 전달인자를 넘기면 alert는 표시되지 않을 것이다. 

디폴트 파라미터의 다른 흥미로운 특징은 다른 파라미터나 function 선언 안에 있는 변수를 참조할 수 있다는 것이다.

  1. function myFunction(a=10, b=a) {
  2.      console.log('a = ' + a + '; b = '  + b);
  3. }
  4. myFunction();     // a=10; b=10
  5. myFunction(22);    // a=22; b=22
  6. myFunction(24);    // a=2; b=4

심지어 function 선언 안에서 연산자를 수행할 수도 있다.

  1. function myFunction(a, b = ++a, c = a*b) {
  2.      console.log(c);
  3. }
  4. myFunction(5);    // 36

일부 다른 언어와 다르게, 자바스크립트는 호출할 때 디폴트 파라미터를 평가한다(evaluate)는 것을 유의해야 한다.

  1. function add(value, array = []) {
  2.   array.push(value);
  3.   return array;
  4. }
  5. add(5);    // [5]
  6. add(6);    // [6], not [5, 6]

Default parameter 지원 브라우저

-데스크탑:

(기본지원) 크롬 49, 파이어폭스 15, 엣지 14, 인터넷익스플로러/오페라/사파리 X

(디폴트 파라미터 다음에 디폴트가 없는 파라미터)  크롬 49, 파이어폭스 26, 엣지 14, 인터넷익스플로러/오페라/사파리 X

-모바일:

(기본지원) 크롬 49, 파이어폭스 15, 인터넷익스플로러/오페라/사파리 모바일 X

(디폴트 파라미터 다음에 디폴트가 없는 파라미터)  크롬 46, 파이어폭스 15, 인터넷익스플로러/오페라/사파리 모바일 X


Destructuring

Destructuring(구조해체하기)는 ECMAScript 6의 새로운 특징으로 배열이나 객체에서 값을 추출하거나 객체와 배열 리터럴과 유사한 구문을 사용하여 이들을 변수에 할당하는 것을 가능하게 해준다. 그 구문은 명확하고 이해하기 쉽고 특히 함수에 전달인자를 넘길 때 유용하다.

ECMAScript 5에서, 설정 객체는 종종 다량의 부가적인 파라미터를 처리하기 위해, 특히 프로퍼티들의 순서가 상관없을 때 사용된다.

  1. function initiateTransfer(options) {
  2.     var  protocol = options.protocol,
  3.         port = options.port,
  4.         delay = options.delay,
  5.         retries = options.retries,
  6.         timeout = options.timeout,
  7.         log = options.log;
  8.     // code to initiate transfer
  9. }
  10. options = {
  11.   protocol: 'http',
  12.   port: 800,
  13.   delay: 150,
  14.   retries: 10,
  15.   timeout: 500,
  16.   log: true
  17. };
  18. initiateTransfer(options);

이 패턴은 자바스크립트 개발자들이 흔히 사용하며, 잘 작동하지만 어떤 파라미터를 받아야 하는지 알기 위해서는 함수의 내부를 보아야만 한다. 구조가 해체된 파라미터(destructured parameters)를 사용하면, 명확하게 함수 선언부 안에 있는 파라미터를 나타낼 수 있다.

  1. function initiateTransfer({protocol, port, delay, retries, timeout, log}) {
  2.      // code to initiate transfer
  3. };
  4. var options = {
  5.   protocol: 'http',
  6.   port: 800,
  7.   delay: 150,
  8.   retries: 10,
  9.   timeout: 500,
  10.   log: true
  11. }
  12. initiateTransfer(options);

이 함수에서는, 설정 객체 대신 객체를 구조적으로 해체한 패턴을 사용했다. 이렇게 하면 함수가 좀 더 정확해질 뿐만 아니라 가독성도 좋아진다. 

또한 구조가 해체된 파라미터와 일반 파라미터를 결합해서 사용할 수 있다.

  1. function initiateTransfer(param1, {protocol, port, delay, retries, timeout, log}) {
  2.      // code to initiate transfer
  3. }
  4. initiateTransfer('some value', options);

함수 호출 시 파라미터를 생략하면 에러가 발생할 수 있다는 점을 유의해야 한다.

  1. function initiateTransfer({protocol, port, delay, retries, timeout, log}) {
  2.      // code to initiate transfer
  3. }
  4. initiateTransfer();  // TypeError: Cannot match against 'undefined' or 'null'

이런 형태는 필수로 요구되는 파라미터가 필요할 때는 이상적이지만, 만약 선택적인 파라미터가 필요하다면 어떻게 할까? 파라미터가 빠졌을 때 이런 에러를 예방하기 위해서, 구조가 해체된 파라미터에 디폴트 값을 할당하는 작업이 필요하다.

  1. function initiateTransfer({protocol, port, delay, retries, timeout, log} = {}) {
  2.      // code to initiate transfer
  3. }
  4. initiateTransfer();    // no error

이 함수에서, 파라미터가 생략되었을 때 디폴트 값으로 빈 객체가 제공될 것이다. 이제, 아무런 파라미터 없이 호출해도 이 함수는 에러가 발생하지 않을 것이다. 또한 각각의 파라미터에 대해 이런 식으로 디폴트 값을 할당할 수도 있다. 

  1. function initiateTransfer({
  2.     protocol = 'http',
  3.     port = 800,
  4.     delay = 150,
  5.     retries = 10,
  6.     timeout = 500,
  7.     log = true
  8. }) {
  9.      // code to initiate transfer
  10. }

이 예제에서, 모든 프로퍼티가 디폴트 값을 가지고 있기 때문에, 수작업으로 함수의 내부에서 undefined 파라미터를 확인하고 디폴트 값을 할당해주는 작업의 필요성이 사라졌다.

Destructuring 지원 브라우저

-데스크탑:

(기본지원) 크롬 49, 파이어폭스 2.0, 엣지 14, 인터넷익스플로러/오페라 X, 사파리 7.1 

(디폴트 값 할당이 함께 있는 경우)  크롬 49, 파이어폭스 47, 엣지 14, 인터넷익스플로러/오페라/사파리 X

-모바일:

(기본지원) 크롬 49, 파이어폭스 1, 인터넷익스플로러/오페라 모바일 X, 사파리 모바일 8

(디폴트 값 할당이 함께 있는 경우)  크롬 49, 파이어폭스 47, 인터넷익스플로러/오페라/사파리 모바일 X


전달인자 넘겨주기(Passing Arguments)

함수에 전달인자를 넘겨주는 데에는 두가지 방법이 있다. 참조에 의해서 혹은 값에 의해서. 참조에 의해서 넘겨지는 전달인자를 변경하는 것은 전역적으로(globally) 반영되지만, 값에 의해서 넘겨진 전달인자는 오직 함수 내부에만 반영된다. 

Visual Basic이나 PowerShell과 같은 일부 언어에서는, 전달인자를 값에 의해서 넘길지 참조에 의해서 넘길지 상세히 하는 옵션이 있다. 그러나 자바스크립트는 그렇지 않다.

전달인자로 값 넘기기(Passing arguments by value)

엄밀히 말하면 자바스크립트는 오직 값을 넘길 수만 있다. 함수에 전달인자로 값을 넘길때, 함수의 범위(scope) 안에 그 값의 복사본이 생성된다. 그러므로, 값에 일어난 어떤 변화도 함수 안에서만 반영이 된다.

  1. var a = 5;
  2. function increment(a) {
  3.     a = ++a;
  4.     console.log(a);
  5. }
  6. increment(a);   // 6
  7. console.log(a);    // 5

여기서, 함수 안에 있는 전달인자를 변경하는 것은 원래 값에 영향을 미치지 않는다. 그래서, 함수 밖에 있는 변수를 콘솔에 찍었을 때, 찍힌 값은 여전히 5이다.

전달인자로 참조 넘기기(Passing arguments by reference)

자바스크립트에서 모든 것은 값으로 넘겨지지만, 객체(배열 포함)를 참조한 변수를 넘길 때, 그 값은 객체를 참조한다. 변수에 의해서 참조가 된 객체의 속성을 바꾸면 근본적으로 그 객체를 바꾼다.

  1. function foo(param){
  2.     param.bar = 'new value';
  3. }
  4. obj = {
  5.     bar : 'value'
  6. }
  7. console.log(obj.bar);   // value
  8. foo(obj);
  9. console.log(obj.bar);   // new value

볼 수 있듯이, 객체의 속성(property)은 함수 내부에서 변경되었지만, 그 변경된 값은 함수의 밖에서도 보인다(visible). 배열이나 함수처럼 프리미티브가 아닌 값을 넘겼을 때, 뒷편에서는 메모리에서 원래의 객체의 위치를 가리키는 변수가 생성된다. 그런 후 이 변수는 함수에 넘겨지고, 이를 바꾸면 원래의 객체에도 영향을 미친다.


원래 글 보러가기