What do the three dots (…) mean in JavaScript?

Adrian Oprea

Aug 28, 2018·3 min read


 
By OtterGFDLorCC-BY-SA-3.0, from Wikimedia Commons  

Originally published athttps://oprea.rocks/blog/what-do-the-three-dots-mean-in-javascript/
 
The title of the article is froma question I was asked to answer on Quora. Below, is my attempt to explain what do the three dots do in JavaScript. Hopefully, this removes the fog around the concept, for people who will find this article in the future and have the same question.

Array/Object spread operator

Assume you have the following object:

const adrian = { 
    fullName: 'Adrian Oprea', 
    occupation: 'Software developer', 
    age: 31, 
    website: 'https://oprea.rocks'
};

 
Let’s assume you want to create a new object(person) with a different name and website, but the same occupation and age.
 
You could do this byspecifying only the properties you want and use the spread operator for the rest, like below:  

const bill = { 
    ...adrian,
    fullName: 'Bill Gates', 
    website: 'https://microsoft.com'
};

 
What the code above does, is to spread over theadrianobject and get all its properties, then overwrite the existing properties with the ones we're passing. Think of this spread thing as extracting all the individual properties one by one and transferring them to the new object.
 
In this case, since we specified thefullNameandwebsiteproperties after the spread operator kicked in, the JavaScript engine is smart enough to know that we want to overwrite the original values of those properties that are coming from the original object.
 
Simply saying, three dots operator is deep-cloning object. Let's compare shallow cloning and deep-cloning
 


 
The above code shows that {...adrian} = JSON.parse(JSON.stringfy(adrian))
 
It’s a similar situation with arrays. Except that instead of spreading over keys and values, the operator spreads indexes and values. Unlike object spread, where you won’t have duplicate properties because that’s just how JavaScript objects work (you can’t have an object with twofullNameproperties), with arrays you may end up with duplicate values, if you plan to implement something similar to our object example.
 
This means that the code below will result in you having an array with duplicate elements.  

const numbers1 = [1, 2, 3, 4, 5];
const numbers2 = [ ...numbers1, 1, 2, 6,7,8]; // this will be [1, 2, 3, 4, 5, 1, 2, 6, 7, 8]

 
Think of it as a replacement forArray.prototype.concat.

Rest operator

When used within the signature of a function, where the function’s arguments should be, either replacing the arguments completely or alongside the function’s arguments, the three dots are also called the rest operator.
 
When it is used like that, the rest operator enables the developer to create functions that can take an indefinite number of arguments, also called functions of variable arity or variadic functions.
 
Here’s the simplest example of such a function. Let’s assume you want to create a function that calculates the sum of all its arguments. Note that it’s not the sum of two, three or four numbers but the sum of all the numbers the function would receive as arguments.
 
Here is a naive implementation, using the rest operator  

function sum(...numbers) { 
    return numbers.reduce((accumulator, current) => { 
        return accumulator += current });
    };

sum(1, 2) // 3
sum(1, 2, 3, 4, 5) // 15

 
The simplest explanation would be that the rest operator takes the arguments that a function receives and dumps them into a real array that you could later use.
 
You might argue that you can do this by requesting the user to pass an array of numbers. That’s technically doable but poor UX for your API, since users expect to call a sum function with plain numbers, not a list of numbers.
 
You might also argue that you could use theargumentsarray. That’s also true but be careful,argumentsis not a real array but an array-like object(an object with a length property). It actually looks like this, for the first call of our sum function, in the previous example:  

{ '0': 1, '1': 2, 'length': 2}

 
To manipulate this object and use array methods on it such as reduce, from my previous example, you’d have to do theArray.prototype.slice.call(arguments, 0)thing. That doesn’t perform well, in terms of speed and memory usage and is not elegant. It’s just smartass code that’s prone to confuse your junior colleagues.
 
This should be everything you need to know to be productive with the rest/spread operator in JavaScript.
 
If you have any advice on how to improve the article, or you need some help wrapping your head around the concept, leave a comment, below.
 
Keep an open mind!
 
Adrian.

'C Lang > JS Basic' 카테고리의 다른 글

자바스크립트에서 물음표? 의 활용  (0) 2020.05.25
javascript error문법  (1) 2020.03.04
Arrow function사용법, 주의사항  (0) 2020.02.13
함수형 프로그래밍이란 무엇인가?  (0) 2020.02.03
함수형 프로그래밍?  (0) 2020.02.03

https://www.robinwieruch.de/javascript-variable-question-mark



JavaScript Variable with Question Mark


javascript question mark

If you are new to JavaScript, the question mark after a variable may be confusing to you. Let's shed some light into it. The question mark in JavaScript is commonly used as conditional operator -- called ternary operator when used with a colon (:) and a question mark (?) -- to assign a variable name conditionally.

const isBlack = false;
const text = isBlack ? 'Yes, black!' : 'No, something else.';
console.log(text);
// "No, something else."

Either the expression is true and returns the value after the question mark (?) or the expression is false and returns the value after the colon (:).

This kind of JavaScript variable declaration is used as a shorthand though. You can achieve the same with the "if-else"-statement in JavaScript as conditional operator in contrast to the ternary operator, but it turns out more verbose:

const isBlack = false;
let text;
if (isBlack) {
text = 'Yes, black!';
} else {
text = 'No, something else.';
}
console.log(text);
// "No, something else."

If this is not what you are looking for, then maybe you are searching for JavaScript's optional chaining feature. It is used to assign a variable conditionally:

const person = {
name: 'Robin Wieruch',
pet: {
name: 'Trixi',
},
};
const petName = person.pet?.name;
console.log(petName);
// "Trixi"

If the person has no pet, the output would be undefined without throwing a JavaScript exception.

const person = {
name: 'Robin Wieruch',
};
const petName = person.pet?.name;
console.log(petName);
// undefined

When this feature was not available in JavaScript, it was common to use the AND (&&) operator or the ternary operator (?:) from before to avoid any JavaScript exceptions:

const person = {
name: 'Robin Wieruch',
};
let petName = person.pet && person.pet.name;
console.log(petName);
// undefined
petName = person.pet ? person.pet.name : undefined;
console.log(petName);
// undefined

Most commonly you will find the question mark in JavaScript for these two use cases. Either it is used as shorthand conditional operator instead of the commonly used "if-else"-statement or as optional chaining operator to assign variables conditionally without hitting an exception.



https://ko.javascript.info/try-catch

'try..catch'와 에러 핸들링

아무리 프로그래밍에 능한 사람이더라도 에러가 있는 스크립트를 작성할 수 있습니다. 원인은 아마도 실수, 예상치 못한 사용자 입력, 잘못된 서버 응답 등의 수천만 가지 이유 때문일 겁니다.

에러가 발생하면 스크립트는 ‘죽고’(즉시 중단되고), 콘솔에 에러가 출력됩니다.

그러나 try..catch 문법을 사용하면 스크립트가 죽는 걸 방지하고, 에러를 ‘잡아서(catch)’ 더 합당한 무언가를 할 수 있게 됩니다.

‘try…catch’ 문법

‘try…catch’ 문법은 'try’와 'catch’라는 두 개의 주요 블록으로 구성됩니다.

try {

  // 코드...

} catch (err) {

  // 에러 핸들링

}

동작 알고리즘은 다음과 같습니다.

  1. 먼저, try {...} 안의 코드가 실행됩니다.
  2. 에러가 없다면, try 안의 마지막 줄까지 실행되고, catch 블록은 건너뜁니다.
  3. 에러가 있다면, try의 실행이 중단되고, catch(err) 블록으로 제어 흐름이 넘어갑니다. 변수 err(아무 이름이나 사용 가능)은 무슨 일이 일어났는지에 대한 설명이 담긴 에러 객체를 포함합니다.

따라서 try {…} 블록 안에서 에러가 발생해도 catch에서 에러를 다루기 때문에 스크립트가 죽지 않습니다.

예제를 살펴봅시다.

  • 에러가 없는 예제 – (1)과 (2)를 alert 창에 보여줌

    
    
    
    
    
    
                                     
    
                                     try {
    
      alert('try 블록 시작');  // (1) <--
    
      // ...에러가 없습니다.
    
      alert('try 블록 끝');   // (2) <--
    
    } catch(err) {
    
      alert('에러가 없으므로, catch는 무시됩니다.'); // (3)
    
    }
  • 에러가 있는 예제 – (1)과 (3)을 보여줍니다.

    
    
    
    
    
    
    
    
    
    
                                      
    
    
    
    
    
    
                                     try {
    
      alert('try 블록 시작');  // (1) <--
    
      lalala; // 에러, 변수가 정의되지 않음!
    
      alert('try 블록 끝(절대 도달하지 않음)');  // (2)
    
    } catch(err) {
    
      alert(`에러가 발생했습니다!`); // (3) <--
    
    }
try..catch는 오직 런타임 에러에만 동작합니다.

try..catch는 실행 가능한(runnable) 코드에만 동작합니다. 실행 가능한 코드는 유효한 자바스크립트 코드를 의미합니다.

중괄호 짝이 안 맞는 것처럼 코드가 문법적으로 잘못된 경우엔 try..catch가 동작하지 않습니다.

try {
  {{{{{{{{{{{{
} catch(e) {
  alert("유효하지 않은 코드이기 때문에, 자바스크립트 엔진은 이 코드를 이해할 수 없습니다.");
}

자바스크립트 엔진은 코드를 읽고 난 후 코드를 실행합니다. 코드를 읽는 중에 발생하는 에러는 ‘parse-time’ 에러라고 부르는데, 엔진은 이 코드를 이해할 수 없기 때문에 parse-time 에러는 (코드 안에서) 복구가 불가능합니다.

try..catch는 유효한 코드에서 발생하는 에러만 처리할 수 있습니다. 이런 에러를 ‘런타임 에러(runtime error)’ 혹은 '예외(exception)'라고 부릅니다.

try..catch는 동기적으로 동작합니다.

setTimeout처럼 ‘스케줄 된(scheduled)’ 코드에서 발생한 예외는 try..catch에서 잡아낼 수 없습니다.

try {
  setTimeout(function() {
    noSuchVariable; // 스크립트는 여기서 죽습니다.
  }, 1000);
} catch (e) {
  alert( "작동 멈춤" );
}

setTimeout에 넘겨진 익명 함수는 엔진이 try..catch를 떠난 다음에서야 실행되기 때문입니다.

스케줄 된 함수 내부의 예외를 잡으려면, try..catch를 반드시 함수 내부에 구현해야 합니다.

setTimeout(function() {
  try {
    noSuchVariable; // 이제 try..catch에서 에러를 핸들링 할 수 있습니다!
  } catch {
    alert( "에러를 잡았습니다!" );
  }
}, 1000);

에러 객체

에러가 발생하면 자바스크립트는 에러 상세내용이 담긴 객체를 생성합니다. 그 후, catch 블록에 이 객체를 인수로 전달합니다.

try {
  // ...
} catch(err) { // <-- '에러 객체', err 대신 다른 이름으로도 쓸 수 있음
  // ...
}

모든 내장 에러를 포함한 에러 객체는 두 가지 주요 프로퍼티를 가집니다.

name
에러 이름. 정의되지 않은 변수 때문에 발생한 에러라면 "ReferenceError"가 이름이 됩니다.
message
에러 상세 내용을 담고 있는 문자 메시지

두 프로퍼티 이외에 표준은 아니지만, 대부분의 호스트 환경에서 지원하는 프로퍼티도 있습니다. stack은 가장 널리 사용되는 비표준 프로퍼티 중 하나입니다.

stack
현재 호출 스택. 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열로 디버깅 목적으로 사용됩니다.

예시:



try {
  lalala; // 에러, 변수가 정의되지 않음!
} catch(err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (호출 스택)

  // 에러 전체를 보여줄 수도 있습니다.
  // 이때, 에러 객체는 "name: message" 형태의 문자열로 변환됩니다.
  alert(err); // ReferenceError: lalala is not defined
}

선택적 ‘catch’ 바인딩

최근에 추가됨
스펙에 추가된 지 얼마 안 된 문법입니다. 구식 브라우저는 폴리필이 필요합니다.

에러에 대한 자세한 정보가 필요하지 않으면, catch에서 이를 생략할 수 있습니다.

try {
  // ...
} catch { // <-- (err) 없이 쓰임
  // ...
}

‘try…catch’ 사용하기

try..catch가 실무에서 어떻게 사용되는지 알아봅시다.

앞서 JSON으로 인코딩된 값을 읽을 수 있도록 해주는 JSON.parse(str) 메서드에 대해 배운 바 있습니다.

이 메서드는 주로 서버 등에서 네트워크를 통해 전달받은 데이터를 디코딩하는 데 사용합니다.

전달받은 데이터에 JSON.parse를 호출하는 식으로 사용되죠.




let json = '{"name":"John", "age": 30}'; // 서버로부터 전달받은 데이터

let user = JSON.parse(json); // 전달받은 문자열을 자바스크립트 객체로 변환

// 문자열 형태로 전달받은 user가 프로퍼티를 가진 객체가 됨
alert( user.name ); // John
alert( user.age );  // 30

JSON에 관한 자세한 정보는 JSON과 메서드 챕터에서 찾을 수 있습니다.

json의 형식이 잘못된 경우, JSON.parse가 에러를 만들기 때문에 스크립트가 ‘죽습니다’.

스크립트가 죽는 것에 만족해야 할까요? 당연히 아니죠!

서버에서 전달받은 데이터가 잘못되어 스크립트가 죽는 경우, 사용자는 (개발자 콘솔을 열지 않는 이상) 원인을 절대 알 수 없습니다. 그런데 사람들은 에러의 원인을 알려주는 메시지 없이 무언가가 '그냥 죽는 것’을 정말 싫어합니다.

try..catch를 사용해 이를 처리해 봅시다.


















let json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- 여기서 에러가 발생하므로
  alert( user.name ); // 이 코드는 동작하지 않습니다.

} catch (e) {
  // 에러가 발생하면 제어 흐름이 catch 문으로 넘어옵니다.
  alert( "데이터에 에러가 있어 재요청을 시도합니다." );
  alert( e.name );
  alert( e.message );
}

위 예시에선 에러가 발생했다는 걸 단순히 보여주기 위해 catch 블록을 사용했지만, catch 블록 안에서 새로운 네트워크 요청 보내기, 사용자에게 대안 제안하기, 로깅 장치에 에러 정보 보내기 등과 같은 일을 할 수 있습니다. 스크립트가 죽도록 놔두는 것보다 훨씬 나은 대응이죠.

자체 에러 던지기

json이 문법적으로 잘못되진 않았지만, 필수 프로퍼티 name을 가지고 있지 않다면 무슨 일이 생길까요?

다음처럼 말이죠.







let json = '{ "age": 30 }'; // 불완전한 데이터

try {

  let user = JSON.parse(json); // <-- 에러 없음
  alert( user.name ); // 이름이 없습니다!

} catch (e) {
  alert( "실행되지 않습니다." );
}

위 예시에서 JSON.parse는 정상적으로 실행되었지만 name이 없는 건 에러를 유발하는 상황입니다.

이제 throw 연산자를 사용해 에러 처리를 통합해 보도록 하겠습니다.

‘Throw’ 연산자

throw 연산자는 에러를 생성합니다.

문법은 다음과 같습니다.

throw <error object>

이론적으로는 숫자, 문자열 같은 원시 타입을 포함한 어떤 것이든 에러 객체로 사용할 수 있습니다. 하지만 되도록 (내장 에러와의 호환을 위해) name과 message 프로퍼티가 있는 객체를 에러 객체로 사용하는 것을 권장합니다.

자바스크립트는 ErrorSyntaxErrorReferenceErrorTypeError등의 표준 에러 객체를 만들어주는 내장 생성자를 지원합니다. 이 생성자들을 이용해 에러 객체를 만들 수도 있습니다.

아래와 같이 말이죠.

let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

일반 객체가 아닌 내장 생성자를 사용해 만든 내장 에러 객체의 name 프로퍼티는 생성자의 이름과 동일한 값을 갖습니다. 프로퍼티 message의 값은 인수에서 가져옵니다.

예시:

let error = new Error("이상한 일이 발생했습니다. o_O");

alert(error.name); // Error
alert(error.message); // 이상한 일이 발생했습니다. o_O

잘못된 데이터를 받았을 때, JSON.parse가 어떤 종류의 에러를 만들어내는지 아래 코드를 통해 살펴봅시다.





try {
  JSON.parse("{ 잘못된 형식의 json o_O }");
} catch(e) {
  alert(e.name); // SyntaxError
  alert(e.message); // Unexpected token b in JSON at position 2
}

SyntaxError가 발생하네요.

사용자를 나타내는 객체에 name 프로퍼티는 반드시 있어야 하므로, 우리 예시에선 name이 없으면 에러가 발생한 것으로 간주합시다.

throw 연산자를 사용해 에러를 던져보겠습니다.









let json = '{ "age": 30 }'; // 불완전한 데이터

try {

  let user = JSON.parse(json); // <-- 에러 없음

  if (!user.name) {
    throw new SyntaxError("불완전한 데이터: 이름 없음"); // (*)
  }

  alert( user.name );

} catch(e) {
  alert( "JSON Error: " + e.message ); // JSON Error: 불완전한 데이터: 이름 없음
}

(*)로 표시한 줄에서 throw 연산자는 message를 이용해 자바스크립트가 자체적으로 에러를 생성하는 방식과 동일한 방식으로 SyntaxError를 생성합니다. 에러가 발생했으므로 try의 실행은 즉시 중단되고 제어 흐름은 catch로 넘어갑니다.

이제 JSON.parse에서 에러가 발생한 경우를 포함한 모든 에러를 catch 블록 안에서 처리할 수 있게 되었습니다.

에러 다시 던지기

위 예제에선 try..catch를 사용해 불완전한 데이터를 처리하였습니다. 그런데 또 다른 예기치 않은 에러가 try {...} 블록 안에서 발생 할 수도 있습니다. 정의하지 않은 변수 등의 프로그래밍 에러가 발생할 가능성은 항상 있습니다.

예시:

let json = '{ "age": 30 }'; // 불완전한 데이터

try {
  user = JSON.parse(json); // <-- user 앞에 let을 쓰지 않음

  // ...
} catch(err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (실제론 JSON Error가 아닙니다.)
}

어떤 에러든 발생할 수 있습니다! 끔찍한 해킹으로 이어질 수 있는 엄청난 버그가 몇십 년간 몇백만 명이 사용한 오픈소스 유틸리티에서도 조차 발견됩니다.

위에선 '불완전한 데이터’를 다루려는 목적으로 try..catch를 썼습니다. 그런데 catch는 근본적으로 try 블록에서 발생한 모든 에러들을 잡습니다. 위 코드에서 catch는 예상치 못한 에러를 잡아냈긴 했지만, 에러의 종류와 관계없이 “JSON Error” 메시지를 보여줍니다. 이렇게 에러 종류와 관계없이 동일한 방식으로 에러를 처리하는 방식은 디버깅을 어렵게 만들기 때문에 좋지 않습니다.

다행히도, name 프로퍼티를 사용하면 에러의 종류를 알아낼 수 있습니다.





try {
  user = { /*...*/ };
} catch(e) {
  alert(e.name); // 존재하지 않는 변수에 접근하려 했으므로 "ReferenceError"가 발생
}

에러 처리 규칙은 간단합니다.

catch에선 타입을 알고 있는 에러만 처리하고, 나머지 에러는 모두 ‘다시 던집니다(rethrow)’.

‘다시 던지기’ 기술을 자세히 설명해보겠습니다.

  1. catch에서 모든 에러를 받습니다.
  2. catch(err) {...} 블록에서 에러 객체, err을 분석합니다.
  3. 에러를 어떻게 처리할지 모르는 경우는 throw err로 처리합니다.

다시 던지기를 사용하여 catch 블록에선 SyntaxError만 처리하도록 해보겠습니다.































let json = '{ "age": 30 }'; // 불완전한 데이터
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("불완전한 데이터: 이름 없음");
  }

  blabla(); // 예상치 못한 에러

  alert( user.name );

} catch(e) {

  if (e.name == "SyntaxError") {
    alert( "JSON Error: " + e.message );
  } else {
    throw e; // 에러 다시 던지기 (*)
  }

}

catch 블록 안의 (*)로 표시된 줄에서 다시 던져진(rethrow) 에러는 try..catch ‘밖으로 던져집니다’. 이때 바깥에 try..catch가 있다면 여기서 에러를 잡습니다. 아니라면 스크립트가 죽겠죠.

따라서 catch 블록은 어떻게 다룰지 알고 있는 에러만 처리하고, 알 수 없는 에러는 ‘건너뜁니다’.

아래 예시에선 try..catch를 한 레벨 더 만들어, 예상치 못한 에러를 어떻게 처리하는지 보여줍니다.



































function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
    blabla(); // 에러!
  } catch (e) {
    // ...
    if (e.name != 'SyntaxError') {
      throw e; // 알 수 없는 에러 다시 던지기
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "External catch got: " + e ); // 에러를 잡음
}

readData는 SyntaxError 처리 방법만 알고 있는 반면, 함수 바깥의 try..catch는 예상치 못한 에러 처리 방법도 알고 있습니다.

try…catch…finally

여기서 끝이 아닙니다.

try..catch는 finally라는 코드 절을 하나 더 가질 수 있습니다.

finally안의 코드는 아래 상황에서 실행됩니다.

  • 에러가 없는 경우, try 실행이 끝난 후
  • 에러가 있는 경우, catch 실행이 끝난 후

finally를 사용해 try..catch를 확장하면 다음과 같습니다.





         

          try {
   ... 코드를 실행 ...
} catch(e) {
   ... 에러 핸들링 ...
} finally {
   ... 항상 실행 ...
}

아래 코드를 실행해보세요.

try {
  alert( 'try' );
  if (confirm('에러를 만드시겠습니까?')) 이상한_코드();
} catch (e) {
  alert( 'catch' );
} finally {
  alert( 'finally' );
}

위 코드는 두 가지 경로로 실행됩니다.

  1. "에러를 만드시겠습니까"에 "OK"로 답한 경우: try -> catch -> finally
  2. "No"로 답한 경우: try -> finally

finally 절은 무언가를 실행하고, 결과에 상관없이 실행을 완료하고 싶을 경우 사용됩니다.

피보나치 함수 fib(n)의 연산 시간을 측정하고 싶다고 해 봅시다. 함수 실행 전에 측정을 시작해서 실행이 끝난 후 측정을 종료하면 되겠죠. 그런데 함수 실행 도중 에러가 발생하면 어떻게 될까요? 아래에서 구현한 함수 fib(n)는 음수나 정수가 아닌 수가 입력될 경우 에러가 반환됩니다.

finally 절은 무슨 일이 일어났든 관계없이 연산 시간 측정을 종료하기 적절한 곳입니다.

fib 함수가 에러 없이 정상적으로 실행되든 에러를 발생하든 상관없이, finally를 사용하면 연산 시간을 제대로 측정할 수 있습니다.





















let num = +prompt("양의 정수를 입력해주세요.", 35)

let diff, result;

function fib(n) {
  if (n < 0 || Math.trunc(n) != n) {
    throw new Error("음수나 정수가 아닌 값은 처리할 수 없습니다.");
  }
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

let start = Date.now();

try {
  result = fib(num);
} catch (e) {
  result = 0;
} finally {
  diff = Date.now() - start;
}

alert(result || "에러 발생");

alert( `연산 시간: ${diff}ms` );

코드를 실행하고 35를 프롬프트 대화상자에 입력하면 try 다음에 finally가 정상적으로 실행되면서 연산 시간을 확인할 수 있습니다. -1을 입력하면 에러가 발생하고, 연산 시간은 0ms가 됩니다. 두 경우 모두 연산 시간이 정상적으로 측정되었습니다.

함수는 return 이나 throw를 만나면 종료되는데, finally 절은 이용하면 두 경우를 모두 실행됩니다.

try..catch..finally 안의 변수는 지역 변수입니다.

위 예시에서 변수 diff와 result는 try..catch  에 선언되었다는 점에 주의해 주세요.

try 블록 안에서 변수를 선언했다면, 블록 안에서만 유효한 지역 변수가 됩니다.

finally 와 return

finally 절은 try..catch절을 빠져나가는 어떤 경우에도 실행됩니다. return을 사용해 명시적으로 빠져나가려는 경우도 마찬가지 입니다.

아래 예시의 try 블록 안엔 return이 있습니다. 이 경우엔 값이 바깥 코드로 반환되기 전에 finally가 실행됩니다.














function func() {

  try {
    return 1;

  } catch (e) {
    /* ... */
  } finally {
    alert( 'finally' );
  }
}

alert( func() ); // finally 안의 alert가 실행되고 난 후, 실행됨
try..finally

catch 절이 없는 try..finally 구문도 상황에 따라 유용하게 쓸 수 있습니다. try..finally 안에선 에러를 처리하고 싶지 않지만, 시작한 프로세스가 마무리되었는지 확실히 하고 싶은 경우에 사용합니다.

function func() {
  // 무언가를 측정하는 경우와 같이 끝맺음이 있어야 하는 프로세스
  try {
    // ...
  } finally {
    // 스크립트가 죽더라도 완료됨
  }
}

위 코드엔 catch가 없기 때문에 try 안에서 발생한 에러는 항상 밖으로 떨어져 나옵니다. 그러나 실행 흐름이 함수를 떠나기 전에 finally가 실행됩니다.

전역 catch

호스트 환경을 확인하세요

이 절은 코어 자바스크립트가 아닙니다.

try..catch 바깥에서 치명적인 에러가 발생해 스크립트가 죽었다고 상상해봅시다.

대처 방법은 무엇이 있을까요? 에러를 기록하거나 사용자에게 무언가를 보여주는 것 등을 할 수 있을 겁니다.

자바스크립트 명세에는 이런 치명적인 에러에 대응하는 방법이 적혀있지 않습니다. 하지만 try..catch 밖의 에러를 잡는 것은 아주 중요하기 때문에, 자바스크립트 호스트 환경 대다수는 에러 처리 기능을 제공합니다. Node.js의 process.on("uncaughtException")이 그 예입니다. 브라우저 환경에선 window.onerror를 이용해 에러를 처리합니다. window.onerror프로퍼티에 함수를 할당하면, 예상치 못한 에러가 발생했을 때 이 함수가 실행됩니다.

문법:

window.onerror = function(message, url, line, col, error) {
  // ...
};
message
에러 메시지
url
에러가 발생한 스크립트의 URL
linecol
에러가 발생한 곳의 줄과 열 번호
error
에러 객체

예시:





<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };

  function readData() {
    badFunc(); // 에러가 발생한 장소
  }

  readData();
</script>

전역 핸들러 window.onerror를 죽어버린 스크립트를 복구하려는 목적으론 잘 사용하지 않습니다. 프로그래밍 에러가 발생한 경우 window.onerror만으로 스크립트를 복구하는 건 사실상 불가능 합니다. window.onerror는 개발자에게 에러 메시지를 보내는 용도로 사용합니다.

window.onerror말고 https://errorception.com 나 http://www.muscula.com같은 에러 로깅과 관련된 여러 가지 상용 서비스가 있습니다.

이런 서비스들은 다음과 같은 프로세스로 동작합니다.

  1. 서비스를 가입하면 자바스크립트 파일(혹은 스크립트 url)을 받는데, 개발자는 이 파일을 페이지에 삽입합니다.
  2. 받은 파일은 커스텀 window.onerror 함수를 설정합니다.
  3. 에러가 발생하면, 이 커스텀 함수가 에러에 관한 내용을 담아 서비스에 네트워크 요청을 보냅니다.
  4. 서비스 사이트에 로그인하고 기록된 에러를 봅니다.

요약

try..catch를 이용하면 런타임 에러를 처리할 수 있습니다. try에선 코드를 실행하고, 에러가 발생하면 catch에서 잡아냅니다.

문법은 다음과 같습니다.

try {
  // 이곳의 코드를 실행
} catch(err) {
  // 에러가 발생하면, 여기부터 실행됨
  // err는 에러 객체
} finally {
  // 에러 발생 여부와 상관없이 try/catch 이후에 실행됨
}

catch 나 finally를 따로 다루는 절이 없는 걸 봐서, try..catch와 try..finally도 유효한 문법이란 걸 유추할 수 있습니다.

에러 객체엔 다음 프로퍼티가 있습니다.

  • message – 사람이 읽을 수 있는 형태의 에러 메시지
  • name – 에러 이름을 담은 문자열 (에러 생성자 이름)
  • stack (표준이 아니지만 대부분의 호스트 환경이 지원함) – 에러가 발생한 순간의 스택

에러 객체가 필요 없으면 이를 생략하여 catch(err) { 대신 catch {를 쓸 수 있습니다.

throw 연산자를 사용하면 에러를 직접 만들 수 있습니다. 이론상으론, throw의 인수에 모든 것이 가능하지만, 대게 내장 Error 클래스를 상속받은 에러 객체가 인수에 들어갑니다. 확장 에러에 대해선 다음 챕터에서 다루도록 하겠습니다.

다시 던지기는 에러 처리 시 사용되는 중요한 패턴입니다. catch 블록에선 대게, 예상하였거나 어떻게 다룰지 알고 있는 에러를 다루고, 예상치 못한 에러는 다시 던지기 합니다.

try..catch가 없어도 대부분의 호스트 환경이 ‘전역’ 에러 핸들러를 지원해주기 때문에 ‘떨어져 나온’ 에러를 잡을 수 있습니다. 브라우저의 전역 에러 핸들러는 window.onerror입니다.

과제

중요도: 5

두 코드 조각을 비교해보세요.

  1. 첫 번째 코드 조각은 try..catch 이후에 코드를 실행하기 위해 finally를 사용하였습니다.

    
    
    
    
    
    
    try {
      work work
    } catch (e) {
      handle errors
    } finally {
      작업 내역 삭제
    }
  2. 두 번째 코드 조각에선 try..catch 바로 아래에 작업 내역을 삭제하는 코드를 놓았습니다.

    
    
    
    
    
    
    
    try {
      work work
    } catch (e) {
      handle errors
    }
    
    작업 내역 삭제

에러의 유무와 상관없이, 작업 후에는 초기화가 필요합니다.

finally를 사용하면 이점이 있을까요? 아니면 두 코드 조각은 동일하게 동작할까요? 만약 이점이 있다면, 이점이 드러나는 예시를 제시해 주세요.


Arrow function

사용법

//기존의 function sample
var double = function(x){
    return x*2;
}
//arrow function sample
const double = (x) => {
    return x*2;
}
//매개변수가 1개인 경우 소괄호 생략 가능
const double = x => {return x*2}
//함수가 한줄로 표현가능하면 중괄호 생략 가능하고 자동으로 return됨
const double = x => x * 2
//매개변수가 두 개 이상일 때
const add = (x,y) => x + y
//매개변수가 없을 때
() => {    ... }
//객체변환할 때 중괄호 사용
() => {return {a:1};}
//return 바로할땐 소괄호 사용
() => ({a:1})

function 키워드 대신 화살표(=>)를 사용하여 함수를 선언할 수 있다.

  • 미리 말하자면 arrow function이 기존의 function을 완전히 대신할 수 없다.
  • 콜백 함수에서 사용하면 아주 간결하게 표현이 가능한 장점이 있다.

함수 호출

//const pow = (x) => {return x * x;}
const pow = x => x * x;
console.log(pow(10)); // 100
//콜백에 arrow function 사용해서 간략화 시키기
const arr = [1,2,3];
const pow = arr.map(x => x * x);
console.log(pow);

변경점

  1. 매개변수에 default 값 적용 가능
const f1 = (msg ='jeongpro') => {
    alert(msg);
}
f1(); // 'jeongpro'

기존에는 매개변수를 전달하지 않으면 undefined가 나오지만 ES6에서는 실수로 매개변수를 전달하지 않았을 때 처리를 할 수 있다.

  1. arguments 대신 args 사용
var foo = function(){
    console.log(arguments);
}
foo(1,2); // { '0':1, '1':2 }

매개변수를 지정하지 않아도 arguments 라는 프로퍼티가 함수에 자동으로 생성되어 사용가능 했었으나 화살표 함수에는 arguments가 없어짐. 대신 args가 생겼다.

// ES5
function sum(){
  var arr = Array.prototype.slice.call(arguments);
  return arr.reduce(function(pre, cur){
    return pre + cur;
  });
}
console.log(sum(1,2,3,4));

// ES6
const sum1 = (...args) => {
  return args.reduce((pre, cur) => pre + cur);
}
console.log(sum1(1,2,3,4));

매개변수부분에 rest 파라미터(...)를 사용하여 가변인자 함수 내부에 배열로 전달할 수 있음.

  1. this 변경점 (핵심)
  • arrow function은 자신을 포함하는 외부 scope에서 this를 계승받는다.
    즉, arrow function은 자신만의 this를 생성하지 않는다. (Lexical this)
function Prefixer(prefix) {
  this.prefix = prefix;
}

Prefixer.prototype.prefixArray = function (arr) {
  console.log(this.prefix);// (A)
  return arr.map(function (x) {
    return this.prefix + ' ' + x; // (B)
  });
};

var pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));

this.prefix가 (A)에서는 'Hi' 를 나타내지만 (B)에서는 undefined가 찍힌다.
지난 포스트에서 자바스크립트의 this에서 봤듯이 내부 함수호출에서는 this가 window객체였다.

function Prefixer(prefix) {
  this.prefix = prefix;
}

Prefixer.prototype.prefixArray = function (arr) {
  return arr.map(x => `${this.prefix}  ${x}`);
};

const pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Lee', 'Kim']));

arrow function에서는 내부 함수여도 this가 arrow function이 선언된 곳에서 상위의 context를 가리키므로 Prefixer객체 pre를 가리키므로 this.prefix가 Hi를 가리켜 개발자가 예상한대로 작동한다.

arrow function을 사용하면 안되는 경우1

const obj1 = {
  name : 'Lee',
  sayName : () => {console.log(`hi + ${this.name}`);}
};
obj1.sayName();
Colored by Color Scripter

위와 같이하면 객체의 this를 바인딩하지 않고 전역 객체가 바인딩된다.

즉, 객체의 메소드를 정의할 때는 사용하면 안된다.

const obj = {
  name: 'Lee',
  sayHi() { // === sayHi: function() {
    console.log(`Hi ${this.name}`);
  }
};

obj.sayHi(); // Hi Lee

위 처럼 ES6에서 사용하는 객체의 메소드 정의방법으로 정의하는 것이 옳다. 마찬가지로 prototype에 메소드를 할당할 때도 똑같이 화살표함수를 사용하면 안된다.

일반 함수 function(){ } 을 이용하도록 한다.

arrow function을 사용하면 안되는 경우2

arrow function을 생성자 함수로 사용할 수 없음.

arrow function을 사용하면 안되는 경우3

addEventListener 함수의 콜백 함수에서 사용하면 this가 상위 컨텍스트를 가리킴

var button = document.getElementById('myButton');  
button.addEventListener('click', () => {  
  console.log(this === window); // => true
  this.innerHTML = 'Clicked button';
});

var button = document.getElementById('myButton');  
button.addEventListener('click', function() {  
  console.log(this === button); // => true
  this.innerHTML = 'Clicked button';
});
Colored by Color Scripter

https://medium.com/@jooyunghan/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-fab4e960d263


(번역) 함수형 프로그래밍이란 무엇인가?

Jooyung Han (한주영)
Jan 6, 2016 · 11 min read

이 글은 Kris Jenkins(@krisjenkins)의 “What is Functional Programming?”을 허락을 구해 번역한 것입니다.

Update 2016–01–18 이 글의 2부에 해당하는 “Which Programming Languages Are Functional?”을 번역(“어떤 프로그래밍 언어가 함수형인가?”)하였습니다.


작성일: 2015년 12월 29일

이 글에서 나는 함수형 프로그래밍이 정말로 무엇인지 설명하려고 한다. 일을 제때 끝내야 하는 월급 개발자들 입장에서 수긍할 수 있게 말이다.

다음을 잘 생각해보자. 여러분이 작성하는 모든 함수는 두 종류의 입력과 출력을 가진다.

두 종류라니? 한 종류 뿐이지 않나?

아니, 둘이다. 그것도 확실히 두 종류다. 다음 예제에서 첫번째 종류의 입출력을 살펴보자.

public int square(int x) {
return x * x;
}
// NOTE: 어떤 언어인가는 중요하지 않다. 여기서는
// 강조하기 위해 입출력 타입이 명확한 언어를 골랐을 뿐이다.

여기서 여러분은 int x를 입력으로, int를 출력값의 타입으로 생각할 것이다.

그것이 바로 입력 및 출력의 첫 번째 종류이다. 그냥 일반적인 입출력이라고 불러도 좋다. 이제 입력과 출력의 두 번째 종류를 보여주는 예를 보자.

public void processNext() {
Message message = InboxQueue.popMessage();

if (message != null) {
process(message);
}
}

문법적으로 따지자면, 이 함수는 입력이 없고 어떤 값도 반환하지 않지만, 분명히 무언가 의존성을 가지며, 또 뭔가 하는 일이 있다는 것은 분명하다. 사실은 이 함수가 숨겨진 형태의 입출력을 가진다는 의미이다. 숨겨진 입력은 popMessage()를 호출하기 전의 InboxQueue 상태이고, 숨겨진 출력은 process 호출로 인해 발생하는 모든 것과 모든 일이 끝나고 났을 때의 InboxQueue 상태이다.

실수하지 말자, 분명 InboxQueue의 상태는 이 함수의 입력이다. 그 값을 모르고서는 processNext가 어떻게 동작할지 알 수 없다. 그리고 그것은 진짜 출력이기도 하다. InboxQueue의 바뀐 상태를 고려하지 않고서는 processNext를 호출한 결과를 완전히 이해할 수 없다.

그래서 두 번째 코드 조각에는 숨겨진 입력과 출력이 있다. 무언가를 필요로하고, 또 변경을 초래하기도 하지만 API만 봐서는 추측할 수 없을 것이다.

이 숨겨진 입력과 출력은 공식적인 이름을 가지고 있다. 바로 “부작용(side-effect)”이다. 이 부작용에는 여러가지 종류가 있지만 모두 같은 컨셉으로 아우를 수 있다. “우리가 이 함수를 호출하려면 인수 목록에는 없지만 필요한 것들이 무엇이고, 반환 값에 반영되지 않으면서 하는 일은 무엇인가?”

(사실 나는 용어를 구분해야 한다고 본다. 숨겨진 출력은 “부작용(side-effect)”으로, 숨겨진 입력은 “부원인(side-cause)”으로 말이다. 이 글에서는 간결함을 위해 “부작용”이란 말을 사용하겠지만 분명 부원인도 의미하는 것이다. 나는 모든 숨겨진 입력과 출력에 대해 이야기하고 있다.)

부작용은 복잡성 빙산이다

함수가 부작용(과 부원인)을 가진다면, 여러분은 다음의 함수를 보면…

public boolean processMessage(Channel channel) {...}

… 이 함수가 어떤 일을 할지 안다고 생각하겠지만 그것은 완전히 틀렸다. 함수 내부를 보지 않고 무엇을 필요로 하는지 무슨 일을 하는지 전혀 알 길이 없다. 채널(Channel)에서 메시지를 꺼내어 처리하는 걸까? 아마도. 어떤 조건 하에서는 채널을 닫아버리나? 그럴지도 모른다. 어디 다른 데이터베이스의 특정 카운트를 업데이트하나? 어쩌면. 로깅 디렉토리 경로를 찾을 수 없는 경우에 죽어버리는 건 아닐까? 그럴 수도 있다.

부작용은 복잡성 빙산이다. 여러분은 함수의 Signature와 이름을 보면, 그 함수가 무언인지 알 수 있다고 생각한다. 그러나 함수 Signature의 표면 아래 숨겨진 것은 그 무엇이든 될 수 있다. 어떤 요구 사항이든 숨겨져 있을 수 있고, 또 어떤 숨겨진 변경도 발생할 수 있다. 그 구현을 보지 않고, 여러분은 정말 어떤 것들이 연관되어 있을지 전혀 알 수 없다. API의 표면 아래에는 잠재적으로 엄청나게 큰 복잡성이 숨어 있다. 여러분이 함수를 제대로 파악하려고 할 때 가능한 대안은 세 가지가 있다. 함수 정의를 파고 들거나, 복잡성을 표면 위로 드러내거나, 그냥 무시하고 잘 되길 바라는 것이다. 하지만 결국에는 무시하는 것이 엄청난 실수가 된다.

그래서 캡슐화를 하는 것 아닌가?

아니.

캡슐화는 구현 세부 사항을 숨기는 것에 관한 것이다. 코드의 내부를 숨겨서 호출하는 쪽에서 걱정할 필요가 없게 하는 것이다. 좋은 원칙이기는 하지만 지금 우리가 이야기하는 것과는 다른 이야기다.

부작용은 “구현 세부 사항 숨기기”에 관한 것이 아니다. 코드와 외부 세상과의 관계를 숨기는 것에 대한 것이다. 부원인을 가지는 함수는 그것이 의존하고 있는 외부 요인에 대한 문서화되지 않은 가정을 가진다. 부작용을 가지는 함수는 그것이 바꾸게 될 외부 요인에 대한 문서화되지 않은 가정을 가진다.

부작용이 나쁜가?

부작용이 원래 작성한 프로그래머가 예상한 그대로 정확하게 동작한다면 괜찮을 것이다. 그러나 문제가 있다. 우리는 원래 프로그래머의 숨겨진 예상이 정확하다고, 그리고 시간이 지나도 여전히 정확할 것이라고 신뢰해야만 한다.

이 함수가 작성될 때 기대했던 것과 똑같이 세상의 상태를 셋업했는가? 혹시 어딘가를 바꾸지는 않았던가? 아마 겉으로 봐서는 전혀 연관없어 보이는 코드 조각을 수정했을지 모른다. 아니면 새로운 환경에 소프트웨어를 설치하고 있기 때문일지도 모른다. 세계의 상태에 대한 숨겨진 가정이 있다는 것은 충분히 비슷하니 잘 동작할 것이라는 우리의 숨겨진 희망을 의미한다.

이러한 코드를 테스트 할 수 있나? 이런 코드는 완전히 분리하여 테스트 할 수 없다. 회로 기판처럼 입력을 연결하고 출력만 확인할 수 있는게 아니다. 우리는 코드를 열어보고 숨겨진 원인과 결과를 파악하고, 세상을 그럴듯하게 시뮬레이션해야 한다. 나는 TDD 개발자들이 블랙 박스로 테스트할지 화이트 박스로 테스트할지 헷갈려하는 경우를 여러번 봤다. 그 대답은, 블랙 박스 테스트를 해야 한다이다. 여러분은 구현 세부 사항을 무시 할 수 있어야 한다. 하지만 여러분이 부작용을 허용하게 되면 블랙 박스 테스트를 할 수 없다. 부작용은 블랙 박스 테스트 여지를 없애버린다. 박스를 열고 그 안에 무엇이 들어 있는지 확인하지 않고서는 입력과 출력을 결정할 수 없기 때문이다.

이 효과는 디버깅 시에 증폭된다. 함수가 부작용 (또는 부원인)을 허용하지 않는 경우, 당신은 단지 몇 가지 입력에 대해 출력을 확인하여 올바른지 여부를 알 수 있다. 그러나 부작용이 있는 함수라면? 여러분이 시스템의 다른 부분을 어디까지 고려해야 할지 그 끝을 알 수 없다. 함수가 무엇에든 의존할 수 있고 무엇이든 변경할 수 있다면 버그는 어느 곳에든 있을 수 있다.

우리는 항상 부작용을 표면으로 드러낼 수 있다

이러한 복잡성에 대해 우리가 할 수 있는 일이 있을까? 있다. 사실 시작하기는 매우 간단하다. 함수가 어떤 입력을 가진다면 그렇게 말하면 된다. 출력으로 뭔가를 반환한다면 그렇게 선언하면 된다. 그렇게 단순하다.

예제로 직접 해보자. 아래 함수는 숨겨진 입력을 가진다. 당신이 빨리 찾을 수 있다면 보너스 점수를 주겠다.

public Program getCurrentProgram(TVGuide guide, int channel) {
Schedule schedule = guide.getSchedule(channel);

Program current = schedule.programAt(new Date());

return current;
}

이 함수는 현재 시간(new Date())을 숨겨진 입력으로 가진다. 우리는 이 추가 입력을 정직하게 대함으로서 복잡성을 표면화할 수 있다.

public Program getProgramAt(TVGuide guide, int channel, Date when) {
Schedule schedule = guide.getSchedule(channel);

Program program = schedule.programAt(when);

return program;
}

이 함수는 이제 숨겨진 입력이나 출력이 없다.

이 새로운 버전의 장단점을 살펴 보자.

단점

더 복잡해 보인다. 두 개가 아닌 세 개의 인자를 가진다.

장점

더 복잡하지 않다. 의존성을 숨긴다고 더 간단해지지는 않는다. 의존성을 정직하게 드러낸다고 더 복잡해지지는 않는다.

훨씬 테스트하기 쉽다. 하루 중 어느 때든, 시차 변경이나 윤년을 테스트 하는 경우에도 원하는 시간을 넘겨주기만 하면 되므로 모두 간단하다. 나는 첫번째 버전의 코드가 실제 제품에 사용된 것을 본적이 있는데, 테스트를 위해 시스템 시간을 바꾸느라 별의별 트릭을 동원해야 했다. 인자로 바꿀 수만 있다면 필요한 노력이 얼마나 될지 상상해보라.

추론하기 더 쉽다. 이 함수는 단지 입력과 출력의 관계를 기술하고 있을뿐이다. 여러분이 입력을 알고 있다면 결과가 무엇이어야 하는지 모든 것을 알고 있다. 이것은 정말 대단한 것이다. 우리는 이 코드를 따로 떼어내어 확인할 수 있다. 입력과 출력 사이의 관계만 테스트하면 함수 전체를 테스트한 것이 된다.

(게다가 부가적으로 더 유용한 함수가 되었다. “한 시간 뒤에 시작하는 프로그램이 무엇인가?”를 구하는 코드도 덤으로 얻었다.)

‘순수 함수’는 무엇인가?

두구두구두구…

이제 숨겨진 입력과 출력을 알게 되었으니 “월급 개발자의 순수 함수 정의”를 알려줄 수 있겠다.

모든 입력이 입력으로 선언되고 (숨겨진 것이 없어야 한다) 마찬가지로 모든 출력이 출력으로 선언된 함수를 ‘순수(pure)’하다고 부른다.

반대로 숨겨진 입력이나 출력이 있는 경우는 순수하지 않은 것이며, 함수가 제공한다고 보이는 계약(contract)이 사실은 전체의 절반을 이야기해 줄 뿐이다. 복잡성 빙산이 나타난다. 순수하지 않은 코드를 ‘독립적으로’ 사용할 수는 없다. 독립적으로 테스트 할 수 없다. 테스트하거나 디버그가 필요할 때면 그것이 의존하고 있는 것을 항상 신경써야 한다.

‘함수형 프로그래밍’이란 무엇인가?

순수/비순수 함수를 알게 되었으니 이제 여러분에게 “월급 개발자의 함수형 프로그래밍 정의”를 알려주겠다.

함수형 프로그래밍은 순수 함수를 작성하는 것, 그러니까 숨겨진 입력이나 출력을 최대한 제거하여 가능한한 우리 코드의 대부분이 단지 입력과 출력의 관계를 기술하게끔 하는 것을 말한다.

부작용을 완전히 피할 수는 없다. 대부분의 프로그램은 반환 값을 얻기 위해서가 아니라 어떤 동작을 하기 위해 실행하기 때문이다. 하지만 프로그램 내부에서는 엄격하게 통제하고자 한다. 우리는 가능한 모든 곳에서 부작용(과 부원인)을 제거하고, 또 제거할 수 없는 경우에는 철저하게 통제할 것이다.

다르게 말하자면, 코드 조각이 필요로 하는 것과 유발하게 될 결과를 숨기지 말자. 코드 조각이 제대로 실행하기 위해 뭔가를 필요로 한다면 그렇게 말하자. 뭔가 유용한 일을 한다면 출력 형태로 선언하자. 이렇게 한다면 우리의 코드는 더 명확해 질 것이다. 복잡성이 표면에 드러나고 우리는 그것을 분해하여 처리할 수 있을 것이다.

‘함수형 프로그래밍 언어’는 무엇인가?

모든 언어는 순수 함수를 지원한다. add(x, y)를 순수하지 않게 만들기는 어렵다.(1) 그리고 많은 경우 순수하지 않은 함수를 순수하게 만들때 필요한 일은 모든 입력 및 출력을 함수 Signature에 올리는 것 뿐이다.그럼 모든 프로그래밍 언어가 ‘함수형’인가?

아니다. 만약 그렇다면 굳이 용어를 둘 필요도 없을 것이다.

그럼 “월급 개발자의 함수형 프로그래밍 언어 정의”가 무엇일까?

함수형 프로그래밍 언어는 부작용없는 프로그래밍을 지원하고 장려하는 언어이다.

더 구체적으로 말하자면, 함수형 언어는 여러분이 가능한한 부작용을 제거하고 그렇지 않은 곳에는 철저히 제어 할 수 있도록 적극적으로 도와주는 언어이다.

더 극적으로 표현하자면, 함수형 언어는 더 적극적이고 더 격렬하게 부작용에 적대적인 언어이다. 부작용은 복잡성이고, 복잡성은 버그이며, 버그는 악마이다. 함수형 언어는 여러분들도 부작용에 적대적이 되도록 도와줄 것이다. 여러분과 함께 그들(부작용,복잡성,버그)을 깨부시고 굴복시킬 것이다.

그게 다야?

그렇다. 여러분이 숨겨진 입력이라고는 도저히 생각하지도 못했을 두어가지 미묘한 것들이 있기는 하지만 그것이 본질이다. 그러나 “부작용이 첫 번째 적이다”라는 관점으로 소프트웨어를 개발하기 시작한다면 여러분이 프로그래밍에 대해 알고 있던 모든 것이 달라진다. 이 글의 2부에서는 부작용과 함수형 프로그래밍에 대한 인식을 바탕으로, 프로그래밍이라는 땅 위에 샷건을 쏘아볼 예정이다.

감사의 글

이 포스팅은 함수형 프로그래밍의 본질에 대해 나눴던 논의들에서 출발한 것이다. 특히 “적절한 라이브러리의 도움이 있다면 자바 스크립트를 함수형 프로그래밍 언어로 간주할 수 있는가”에 관한 Sleepyfox과의 대화가 주효했다. 나의 본능적 대답은 ‘아니다’였지만 를 거듭 고민한 끝에 유용한 사고 연결 고리를 따라갈 수 있었다.

제임스 헨더슨에게도 도움을 받았다. 나는 그와 함께 올 한해동안 함수형 프로그래밍과 관련한 여러가지 유익한 아이디어들을 주고 받았다.

말콤 스파크 , 이반 Uemlianin , 조엘 클레르몽 , 케이티 모에, 그리고 내 이름과 발음이 같은 도플 갱어 크리스(Chris) 젠킨스의 교정과 제안에도 감사한다.


함수형 프로그래밍?(Functional Programming)

Functional Programming(함수형 프로그래밍)

지난 토요일에 T 회사 인턴 면접을 보고 왔다. 해당 회사 모바일 개발 직군에 인턴으로 지원하기도 했고, 전 회사에서 하던 것이 Android라서 최근에 구글이 선택한 Kotlin을 공부하기 시작했다. 면접에서 요즘에 공부하는 것이 무엇이냐는 질문에, Kotlin을 막 공부하기 시작했다고 대답했다. 그러자 면접관께서 Kotlin, Swift 등 최근 함수형 언어(Functional Language)가 뜨고 있는데 그 이유를 알고 있느냐고 질문하셨다. Kotlin 공부를 하게 된 이유는 함수형 언어이기 때문이 아니라, 단순히 구글이 Android 언어로 Kotlin을 선택했기 때문이다.

그래서 질문에 대답하지 못했다. 하지만 아는 것이 힘이니 오늘 그 이유에 대해 알아보고자 한다.

함수형 프로그래밍이란(About Functional Programming)

함수형 프로그래밍 은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다. 명령형 프로그래밍(Java 등)에서는 상태를 바꾸는 것을 강조하는 것과 달리, 함수형 프로그래밍은 함수의 응용을 강조한다. 대부분 람다 대수에 근간을 두고 있으며, 다수의 함수형 언어는 람다 연산을 발전시킨 것으로 볼 수 있다.

PURE FUNCTION(순수 함수)

순수 함수 란, Side Effect가 없는 함수를 뜻한다. 즉, 함수 실행이 외부에 영향을 끼치지 않는 함수를 뜻한다. 이러한 성질 덕분에 함수형 언어는 Thread-Safe하고 Concurrency를 지원한다.

ANONYMOUS FUNCTION(익명 함수)

익명 함수 란, 이름이 없는 함수를 뜻한다. 전통적인 명령형 언어에서는 모든 함수에 이름이 부여되어야만 했다. 하지만 람다식을 통해 이름없는 함수를 수행할 수 있다.

유의할 점은 익명 함수의 경우 최신 명령형 언어에서도 지원한다. Java의 경우 Java8부터, C++의 경우 C++11 부터, .NET Framework의 경우 LINQ가 추가된 3.5부터 가능하게 되었다.

HIGHER-ORDER FUNCTION(고계 함수)

고계 함수 란, 함수를 다루는 함수를 뜻한다. 함수형 언어에서는 함수 자체도 ‘값(value)’ 으로 취급한다. 정수를 함수의 인자로 전달할 수 있듯이 어떤 함수도 다른 함수의 인자로 전달할 수 있다. 마찬가지로 함수의 결과 값으로 정수를 반환할 수 있듯이 함수를 반환할 수 있다.

참고 링크


함수형 프로그래밍이 뜨는 이유(The Reason Why Functional Programming is On The Rise)

그렇다면 함수형 프로그래밍이 뜨는 이유는 뭘까? 또 왜 여태까지 뜨지 않았던 것일까?

뜨는 이유

함수형 프로그래밍이 뜨는 이유는 세 가지로 볼 수 있다고 한다.

  • Multi-Core & Concurrency
    • Multi-Core CPU를 장착한 컴퓨터가 대세가 되면서 Multi-Threading을 지원하는 소프트웨어가 필요하게 되었다.
    • 무어의 법칙을 따라 발전하던 CPU가 현실적인 한계에 봉착하자, 수직적 전략을 포기하고 여러 개의 칩이 병렬적으로 동작하도록 만드는 수평적 전략을 선택함.
    • Concurrency를 지원하게 되면서 변경 가능한 데이터(Mutable Data)는 개발자에게 어려움을 가져다 주었다.
    • 함수형 언어의 특징은 데이터가 변경 불가능(Immutable Data)이다. 때문에 Concurrency를 지원하기에 적합하다.
  • 코드의 간결함
    • 객체지향(OOP)를 오랜 시간 사용해온 프로그래머라면 각종 디자인 패턴에 친숙할 것이지만, 함수형 프로그래밍의 관점에서는 디자인 패턴은 우격다짐이다.
    • 필요한 논리 부분과 추상 부분을 구별해내고 패턴에 맞게 구성하는 것은 부수적이고 장식적인 코드가 많아지게 한다.
    • 함수형 프로그래밍의 중요 개념인 Curry, Partial Application, Monad와 같은 기법이 간결하고 우아한 함수의 구성(Composition)을 가능하게 해준다.
  • C#과 Java 같은 기존 언어의 관리모드
    • 기존 언어들이 발전모드에서 관리모드로 접어듦에 따라 프로그래밍 언어와 관련한 새로운 패러다임이 요구되는 현실과 관련이 있다.
    • JVM이나 .NET 플랫폼을 대상으로 여러 언어들이 실험되고 있으며, 실제로 많은 지원이 이루어지고 있다. 때문에 함수형 패러다임은 객체지향 패러다임의 뒤를 이을 새로운 패러다임을 발견하기 위한 전반적인 실험과 암중모색의 와중에 하나의 강력한 대안으로 주목받고 있다.

참고 링크

뜨지 못한 이유

뜨지 못한 이유는 함수형 프로그래밍이 가지는 장점 자체가 단점이기 떄문이라고 한다.

  • Multi-Core & Concurrency
    • Immutable Data Structure는 Thread-Safe를 쉽게 보장하는 대신 ‘오래된’ 데이터를 사용하게 될 수도 있다.
    • Mutable Data Structure는 항상 최신 데이터를 다룬다는 장점이 있지만, Data Consistency를 보장하기 위해 복잡함을 수반한다.
    • 둘 중 어느 것이 낫다고 볼 수 없다.
  • Non-State, No-Side Effect
    • 함수형 프로그래밍은 상태(State)를 배제하여 Side Effect가 없게 동작한다.
    • 하지만 사용자(User)와의 상호작용(Interaction)은 대부분 상태 변화로 모델링된다.
    • 즉, 상태가 없는 함수형 프로그래밍의 한계다.
  • 코드의 간결함
    • 코드가 간결해질 수 있지만, 간결한 코드를 읽기 위해서 학습이 필요하다.
  • 생산성이 올라간다.
    • 함수형 프로그래밍 스타일로 작성할 수 있는 프로그래머를 채용하는데 드는 비용을 상쇄시킬만큼 생산성이 올라야만 한다.
  • 해결할 문제들
    • 세상에는 함수형 프로그래밍 스타일에 적합한 문제도 많지만, 적합하지 않은 문제들도 충분히 많다.

참고 링크


How About Kotlin

Kotlin은 JVM 호환 언어라고 한다. 즉, Kotlin으로 작성된 소스 코드를 컴파일하면 Java Bytecode(.class file)가 생성된다. 아마 Java를 활용하는 모든 곳에 Kotlin을 사용할 수 있을 것이라고 생각된다.

아마 이런 이유 때문에 구글이 Kotlin을 Android 공식 언어로 선택하지 않았나 싶다.

다만 Java의 경우 명령형 언어를 대표하고 Kotlin은 함수형 언어인 만큼, 프로그래밍 패러다임적인 문제가 없을지 궁금하다. 단순히 JVM 위에서 동작한다고 Kotlin을 선택했을까? JVM을 지원하는 언어는 무수히 많은데…


Function Programming 을 공부 하다보면 1급 객체란 말을 많이 언급됩니다. 그래서 1급 객체 (First-class citizen)란 무엇인가를 공부도하고 정리해 공유 하고자 합니다.

1급 객체란?

아래 3 가지조건을 충족한다면 1급 객체라고 할수 있습니다.  

  1. 변수나 데이타에 할당 할 수 있어야 한다.
  2. 객체의 인자로 넘길 수 있어야 한다.
  3. 객체의 리턴값으로 리턴 할수 있어야 한다.
     
    Java와 Kotlin의 비교를 통해 Kotlin 의 함수는 왜 1급 객체고 Java의 함수는 1급 객체가 아닌지 알아보겠습니다.

변수나 데이타에 할당 할 수 있어야 한다.

object Main {
    @JvmStatic
    fun main(args: Array<String>) {
        val a = test
    }

    val test: () -> Unit = { println("kotlin") }
}
public class java {

    public static void test(){
        System.out.println("java");
    }

    public static void main(String[] args) {
        System.out.println("java");
//        Object a = test;
    }
}

kotlin은 a 에 type이 () -> Unit 인 test 함수 할당이 가능하지만, Java는 불가능 합니다.

객체의 인자로 넘길 수 있어야 한다.

object Main {
    @JvmStatic
    fun main(args: Array<String>) {
        function(test)
    }

    fun function(f: () -> Unit) {
       f.invoke()
    }

    val test: () -> Unit = { println("kotlin") }
}

kotlin 은 function 함수의 인자로 함수타입을 전달 할 수 있습니다. 하지만 Java에서는 불가능 합니다.

객체의 리턴값으로 리턴 할 수 있어야 한다.


object Main {
    @JvmStatic
    fun main(args: Array<String>) {
        function()
    }

    fun function(): () -> Unit {
        return { println("kotlin") }
    }

}

function함수는 { println(“kotlin”) }, 즉 함수타입을 반환 합니다.
kotlin에서 함수는 변수나 data에 할당이 가능하며, 함수의 인자로 전달가능 하고, 함수의 리턴값으로도 사용 할 수 있습니다. 그렇기 때문에 kotlin의 함수는 1급 객체라고 할 수 있습니다. 반면 Java의 함수는 위 조건들을 만족하지 못하기 때문에 1급 객체라고 할수 없습니다.

javascript : call, apply, bind 차이 - this의 사용

자바스크립트에서는 일반적인 방법 외에도, 함수를 어디서 어떻게 호출했느냐와 관계없이 this가 무엇인지 지정할 수 있다.

call

call 메서드는 모든 함수에서 사용할 수 있으며, this를 특정 값으로 지정할 수 있다.

<script type="text/javascript">  
const bruce = {  
     name : 'Bruce'  
};  

const madeline = {  
    name : 'Madeline'  
};  

//이 함수는 어떤 객체에도 연결되지 않았지만 this를 사용함.  
function greet(){  
    return `Hello, I'm ${this.name}`;  
};  

greet(); // 'Hello, I'm undefined' - this는 어디에도 묶이지 않음  
greet.call(bruce) // 'Hello, I'm Bruce' - this는 bruce  
greet.call(madeline) // 'Hello, I'm Madeline' - this는 madeline  
</script>

함수를 호출하면서 call을 사용하고 this로 사용할 객체를 넘기면 해당 함수가 주어진 객체의 메서드인 것처럼 사용할 수 있다. call의 첫 번째 매개변수는 this로 사용할 값이고, 매개변수가 더 있으면 그 매개변수는 호출하는 함수로 전달된다.

<script type="text/javascript"\>  
function update(birthYear, occupation){  
    this.birthYear = birthYear;  
    this.occupation = occupation;  
};  

update.call(bruce, 1949, 'singer'); // bruce 변경  
/*  
bruce는 이제  
{  
name : 'Bruce',  
birthYear : 1949,  
occupation : 'singer'  
}  
로 변경됨  
*/  

update.call(madeline, 1942, 'actress'); // madeline 변경  
/*  
madeline은 이제  
{  
name : 'Madeline',  
birthYear : 1942,  
occupation : 'actress'  
}  
로 변경됨  
*/  
</script>

apply

apply는 함수 매개변수를 처리하는 방법을 제외하면 call과 완전히 같다. call은 일반적인 함수와 마찬가지로 매개변수를 직접 받지만, apply는 매개변수를 배열로 받는다.

<script type="text/javascript">  
update.apply(bruce, [1955, 'actor']);  
/*  
bruce는 이제  
{  
name : 'Bruce',  
birthYear : 1955,  
occupation : 'actor'  
}  
로 변경됨  
*/

update.apply(madeline, [1918, 'writer']);  
/*
madeline은 이제  
{  
name : 'Madeline',  
birthYear : 1918,  
occupation : 'writer'  
}  
로 변경됨  
*/  
</script>

apply는 배열 요소를 함수 매개변수로 사용해야 할 때 유용하다. apply를 설명할 때 흔히 사용하는 예제는 배열의 최소값과 최대값을 구하는 것이다. 자바스크립트의 내장 함수인 Math.min 과 Math.max 는 매개변수를 받아 그중 최소값과 최대값을 각각 반환한다. apply를 사용하면 기존 배열을 이들 함수에 바로 넘길 수 있다.

<script type="text/javascript">  
const arr = [2,3,-5,15,7];  

Math.min.apply(null, arr); // -5  
Math.max.apply(null, arr); // 15  
</script>

this의 값에 null을 쓴 이유는 Math.min과 Math.max가 this와 관계 없이 동작하기 때문이다. 즉, 무엇을 넘기든 관계없다.

ES6의 확산 연산자(...)를 사용해도 apply와 같은 결과를 얻을 수 있다. update 메서드는 this 값이 중요하므로 call을 사용해야 하지만, Math.min과 Math.max는 this 값이 무엇이든 관계없으므로 확산 연산자를 그대로 사용할 수 있다.

<script type="text/javascript">  
const newBruce = [1940, "martial artist"];  

update.call(bruce, ...newBruce); // apply(bruce, newBruce)와 같음  
Math.min(...arr); // -5  
Math.max(...arr); // 15  
</script>

bind

this의 값을 바꿀 수 있는 마지막 함수는 bind이다. bind를 사용하면 함수의 this 값을 영구히 바꿀 수 있다. update 메서드를 이리저리 옮기면서 호출할 때 this 값은 항상 bruce가 되게끔, call이나 apply, 다른 bind와 함께 호출하더라도 this 값이 bruce가 되도록 하려면 bind를 사용한다.

<script type="text/javascript">  
const updateBruce = update.bind(bruce);  

updateBruce(1904, "actor");  
// bruce 는 이제 { name: "Bruce", birthYear: 1904, occupation: "actor" }  

updateBruce.call(madeline, 1274, "king");  
// bruce 는 이제 { name: "Bruce", birthYear: 1274, occupation: "king" };  
// madeline은 변하지 않음  
</script>

bind는 함수의 동작을 영구적으로 바꾸므로 찾기 어려운 버그의 원인이 될 수 있다. bind를 사용한 함수는 call이나 apply, 다른 bind와 함께 사용할 수 없는 거나 마찬가지다. 함수를 여기저기서 call이나 apply로 호출해야 하는데, this 값이 그에 맞춰 바뀌어야 하는 경우를 상상해보라. 이럴 때 bind를 사용하면 문제가 생긴다. bind를 쓰지 말라고 권하는 것은 아니다. bind는 매우 유용하지만, 함수의 this가 어디에 묶이는지 정확히 파악하고 사용해야 한다.

bind에 매개변수를 넘기면 항상 그 매개변수를 받으면서 호출되는 새 함수를 만드는 효과가 있다. 예를 들어 bruce가 태어난 해를 항상 1949로 고정하지만, 직업은 자유롭게 바꿀 수 있는 업데이트 함수를 만들고 싶다면 다음과 같이 하면 된다.

<script type="text/javascript"\>  
const updateBruce1949 = update.bind(bruce, 1949); updateBruce1949("singer, songwriter");  
/*  
bruce 는 이제 {  
name: "Bruce",  
birthYear: 1949,  
occupation: "singer, songwriter"  
}  
*/  
</script>

출처:https://ibrahimovic.tistory.com/29[Web Standard]

+ Recent posts