12-1. 함수
객체와 더불어 함수는 자바스크립트의 핵심이자, 스코프, 실행 컨텍스트, 클로저, 프로토타입 등 모두 함수와 관련이 있다.
우리가 알고 있는 수학에서의 함수는 결국 입력(input) 을 받아 출력(output) 을 내보내는 과정이다.
function add(x, y) {
return x + y;
}
add(2,5); // 7
x, y 를 입력받아 내부 과정을 통해 x+y 를 출력한다. 위 표현방법은 자바스크립트에서의 함수 표현방법이다.
자바스크립트에서 함수는 일련의 과정을 문(statement)으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것이다.
위 그림을 참고하여, 함수는 내부로 입력을 전달받는 매개변수(parameter), 입력을 인수(argument), 출력을 반환값(return value) 라고 한다. 또한 함수는 값이다. 그리고 함수는 정의와 호출로 나눌수 있는데, 정의만 하였다고 함수가 실행되는 것은 아니다. 함수를 실행시키기 위해서는 함수 호출을 해주어야 한다.
함수는 반복되는 과정을 수월하게 해준다. 위 덧셈 함수는 어떠한 수가 들어와도 합을 반환한다. 몇번이든 재사용이 가능하기에 코드의 재사용이라는 부분에서 매우 유용하다. 또한 만일 덧셈을 뺄셈으로 변경하게 된다면? 만일 덧셈의 로직을 그냥 하드코딩으로 반복하여 사용한다면 수정할 때 역시 모든 로직을 변경해주어야 한다. 허나 함수를 호출한 경우라면 그 함수만 변경해주면, 쉽게 변경이 가능하다. 유지보수의 편의성을 가지게 된다.
12-2. 함수 리터럴
자바스크립트의 함수는 객체 타입의 값이다. 따라서 함수도 리터럴로 생성할 수 있는데 아래처럼 리터럴이 구성된다.
var f = function add(x, y){
return x + y;
};
함수는 크게 함수 이름, 매개변수, 함수 몸체 로 구성된다. 함수이름은 식별자이기에 식별자 네이밍 규칙을 따라야한다. 신기하게도 함수의 이름은 생략이 가능한데 이름이 없는 함수를 익명함수(anonymous function) 이라고 한다.
매개변수는 쉼표로 구별하며, 역시나 식별자 네이밍 규칙을 따라야 한다.
함수는 객체지만, 객체와 다르다. 객체는 호출할 수 없지만, 함수는 호출할 수 있다. 허나 함수가 객체라는 사실은 자바스크립트의 중요한 특징이다. 일단 기억해놓자..
12-3. 함수 정의
함수를 정의하는 방법에는 4가지가 있다.
- 함수 선언문
- 함수 표현식
- Function 생성자 함수
- 화살표 함수(ES6)
함수를 정의한다는 면에서는 동일하지만, 차이점들이 있다. 조금 더 알아보도록 하자
1) 함수 선언문
function add(x, y) {
return x + y;
}
// 함수 선언문은 이름을 생략할 수 없다.
function (x, y){
return x + y;
} // SyntaxError: Function statement require a function name
주의할 점은 함수 선언문은 이름을 생략할 수 없다. 또한 함수 선언문은 표현식이 아니라 문이다. 개발자도구에서 함수 선언문을 실행하면 undefined 가 출력된다. 만일 선언문이 문이 아니라 표현식이였다면 표현식이 평가되어 생성된 함수가 출력되어야 한다.
함수 선언문은 문이기에 값이 아니다. 따라서 변수에 할당이 되어서는 안된다. 아래 코드처럼 말이다.
var add = function add(x, y){
return x + y;
};
// 함수 호출
console.log(add(2, 5)); // 7
그런데 이상하게도 분명 선언문같은데 변수에 할당되는듯이 작동을 한다. 무엇인가 이상하다.
문은 분명 변수에 할당되지 않는다. 그렇다면 표현식으로서 값을 가진다는 의미인데, 함수가 객체라는 사실은 이를 가능하게 해줄 것 같다.
앞서서 함수 선언문의 형식은 함수 리터럴과 차이가 없다고 하였다. 그렇다면 자바스크립트 엔진이 선언문이 아닌 리터럴로 착각한 것이라면 위 코드가 성립할 수 있게 된다. 지금 함수 add 가 들어갈 자리(우항) 은 분명 값으로서 평가받는 표현식이 들어가야 하는 자리다. 자바스크립트 입장에서 함수 선언문인지 리터럴인지 햇갈린다면 문맥으로 파악할 수 밖에 없을 것이다. 즉, 저 자리는 문맥상 리터럴이 들어가야하는게 맞다. 함수 선언문이 단독으로 사용되었다면 말그대로 선언문으로 인지할 것이고, 문맥상 리터럴이 들어갈 자리라면 함수 리터럴로 해석을 할 것이다.
그래도 뭔가 어색한 점이 변수 이름이 add 이며, 함수 이름도 add 이다. 이래도 되는 것일까?
function foo(){ console.log('aa'); }
foo(); // aa
(function bar(){ console.log('aa') });
bar(); // ReferenceError: bar is not defined
이를 이해하려면 우선 위 코드부터 이해를 해야한다. 단독으로 쓰인 함수 리터럴 foo 는 함수 선언문으로 해석된다. 우리가 흔히 자바스크립트를 사용하면서 자연스럽게 함수 foo 를 불러왔듯이, 전혀 문제되지 않는다.
반면 아래 () 안 함수 bar 의 경우 그룹 연산자 내부에 있기에 값으로 평가되어야 하기에 문맥상 함수 리터럴로 해석이 되어야 한다.
여기까지는 뭐 이렇게도 저렇게도 해석되는구나로 생각이 들었지만, 밑에 bar() 를 실행할 시 bar 가 선언되지 않았다고 나온다. 함수 리터럴 표현식으로 생성된 bar 는 호출할 수 없다니.. 이유가 무엇일까.
위에서 언급하지 않았던 부분 중 하나가 함수 리터럴로 해석될 때 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자라는 것이었다. 그러니깐 함수 블록 밖에서는 (위에서 bar() 와 같이) 호출을 할 수 없다는 것이다. 얼핏 bar 라는 이름으로 메모리에 접근하면 된다고 생각할 수 있으나, 사실은 그렇지 않다. 우리가 특정 메모리 공간에 접근하려고 하면 반드시 식별자가 있어야 한다고 저번 포스팅들에서 배울 수 있었다. 지금까지 함수의 이름이 식별자라고 판단하였고, 그게 약간의 고정관념처럼 자리잡았었는데 사실은 그렇지 않았던 것이다. 함수의 이름은 식별자가 아니다. 그렇기에 함수를 호출 할 수 없는것이다.
그럼 함수 선언문도 함수 리터럴이 아닌가? 왜 foo 는 호출이 되는 것인가?
여기서 자바스크립트의 성질이 나오는데, 자바스크립트는 문맥상 함수가 선언된 것이라면, 암묵적으로 식별자를 생성하게 된다. 그리고 이 식별자에 함수 객체를 할당한다. 즉, 함수 이름이 add 라면, 그리고 우리가 호출로서 add 를 작성하였다면, 이때의 add 는 함수의 이름이 아니라 암묵적으로 생성된 식별자의 이름 add 인것이다.
var add = function add(x,y){
return x + y;
};
console.log(add(2, 5));
즉 여기서 호출 add 는 식별자 add 인것이다. 함수는 이름으로 호출되는 것이 아니라 식별자로 호출된다는 점을 다시 한번 상기하자.
2) 함수 표현식
자바스크립트의 함수는 객체 타입의 값이며, 변수에 할당도 가능하고 객체의 프로퍼티값이 될 수도 있으며, 배열의 요소가 될 수도 있다. 이러한 성질을 갖는 객체를 일급 객체라 하는데, 자바스크립트 함수는 일급 객체다. 즉, 함수를 값처럼 자유롭게 사용이 가능하다는 의미이다.
var add = function (x, y) {
return x + y;
};
var foo = function bar(x, y){
return x - y;
};
console.log(foo(5,2)); // 3
console.log(bar(5,2)); // ReferenceError: bar is not defined
값처럼 변수에 선언이 가능하니 보통 첫번째 함수처럼 익명함수를 할당하는 식으로 사용한다. 함수 표현식에서 함수 리터럴은 함수 이름을 생략하는 것이 일반적이다. 그리고 함수 선언식에서 설명하였듯이, 함수를 호출할 때는 식별자를 호출하는것을 잊지 말자. 이렇게 되면 사실 함수 선언문도 암묵적으로 식별자를 생성하고, 함수 표현식에는 이미 식별자가 있음으로 둘 다 같은 방식이 아닌가 할 수 있다. 허나 함수 선언문은 '표현식이 아닌 문' 이고 함수 표현식은 '표현식인 문'이다. 미묘한 차이가 있다.
3) 호이스팅
console.log(add(2, 5)); // 7
console.log(sub(5, 2)); // TypeError: sub is not a function
// 함수 선언문
function add(x, y){
return x + y;
}
// 함수 표현식
var sub = function (x, y){
return x - y;
};
콘솔창에서 함수를 호출하는 것은 모두 함수 선언보다 위쪽에 있다. 허나 결과는 상이하다. 함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출할 수 있다. 반면 표현식으로 선언한 함수는 이전에 호출할 수 없다. 이는 함수 선언문으로 정의한 함수와 함수 표현식으로 정의한 함수의 생성시점이 다르기 때문이다.
함수 호출은 런타임 시점에서 호출이 되게된다. 함수를 선언할 때는 런타임 시점 이전에 함수 이름과 동일한 식별자가 생성되고 거기에 함수 객체가 할당이 끝난 시점이 된다. 따라서 밑에서 선언을 하였다고 한들 함수를 호출할때는 전혀 문제가 발생하지 않는다. 이처럼 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유 특징을 함수 호이스팅(function hoisting) 이라고 한다.
함수 호이스팅은 변수 호이스팅과는 차이가 있는데, 변수 호이스팅은 초기값에 undefined 를 할당하게 되는데 비해, 함수 호이스팅은 함수 객체를 할당하게 된다. 위에서 보게 되면 변수 var sub 의 경우 런타임 이전 초기 할당값은 undefined 다. 따라서 함수 표현식으로는 이보다 위에 호출된 함수를 참조할 수 없게 되는 것이다. 즉, 함수 표현식의 경우 함수 호이스팅이 발생하는 것이 아니라 변수 호이스팅이 발생하게 되는 것이다.
이 외 ES6 에서 도입된 화살표 함수(arrow function)이 있는데, 좀 더 간략하게 함수표현이 가능하다.
const add = (x, y) => x + y;
사실 화살표함수가 추가된 이유는 간단하게 기입하기 위해서가 아니라, 기존 함수와 this 의 바인딩 방식이 다르고, prototype 프로퍼티가 없는 등 여러가지 성질들이 있다. 추후 화살표 함수에서 다루어보자.
12-5. 함수 호출
함수를 호출하게 되면 현재의 실행 흐름을 중단하고 호출된 함수로 실행 흐름을 옮긴다는 점에서 비동기적인 부분을 가지고 있다. 어찌 되었던 매개변수의 인수가 순서대로 할당되고 함수 몸체의 문들이 실행되기 시작한다.
위 사진처럼 함수의 실행은 매개변수를 통해 인수를 전달한다.
function add(x, y){
return x + y;
}
add(2, 5); // 7
console.log(x, y); // ReferenceError: x is not defined
// 매개변수보다 적은 인수가 들어온다면
console.log(add(2)); // NaN. (2 + undefined = NaN)
// 매개변수보다 초과된다면 무시
console.log(add(2,5,10)); // 7
매개변수는 함수의 몸체에서 변수처럼 작용한다고 하였다. 따라서 몸체를 벗어난(함수 스코프에서 벗어난) 경우 매개변수를 참조할 수 없다.
매개변수는 변수처럼 작용한다고 하였으니, 초기 할당값은 undefined 다. 이후 인수를 통해 하나하나 값이 할당이 되는 것이다. 그렇기 때문에 매개변수보다 적은 인수가 들어오게 된다면 인수가 할당되지 않은 부분은 undefined 로 남게 된다. 반대로 더 많은 인수가 들어오게 된다면 그냥 무시된다. (추후 argument 객체에 대해 다뤄보겠다.)
인수를 받을 때 역시 자바스크립트 내 함수의 특징이 드러나게 되는데, 자바스크립트는 동적 타입 언어이기에 단순히 매개변수 x, y 를 받겠다고 한다면, 이 매개변수의 타입이 어떤지는 정의하지 않는다. 문자가 들어갈 수도 있고 undefined 가 들어갈 수도 있다. 이를 방지하려면 조건부 타입을 걸어주거나 타입스크립트를 사용하는 방법, ES6에서 도입된 매개변수 기본값 설정을 활용하면 가능하다.
// 어떤 타입이 들어올지 정해지지 않는다
function add(x, y){
return x + y;
}
// 따라서 미리 타입을 정의하는 방법
function add(x, y){
if(typeof x !== 'number' || typeof y !== 'number'){
throw new TypeError('인수는 모두 숫자 값이어야 합니다');
}
return x + y;
}
console.log(add('a','b')); // TypeError: 인수는 모두 숫자 값이어야 합니다.
// 매개변수의 초기값을 설정하기
function tripleAdd(a = 0, b = 0, c = 0){
return a + b + c;
}
console.log(tripleAdd()); // 0
자바스크립트 내 함수의 특징은 매개변수의 개수에는 제한이 없다는 점이다. 그렇다고 무작정 많이 쓸수도 없는 입장이기에, 이상적인 함수로 만들기 위해서는 매개변수가 없는것이 가장 좋다.
이상적인 함수는 한 가지 일만 해야하며 가급적 작게 만들어야 한다. (유지보수및 관리의 용이성, 가독성 등)
다음은, 뭔가 함수내에서 암묵적으로 계속 쓰고있었던 return 에 대해 살펴보자. 함수는 return 키워드와 표현식(반환값)으로 이뤄진 반환문을 사용해 실행 결과를 함수 외부로 반환(return) 할 수 있다.
반환문은 크게 2가지 역할을 한다.
- 반환문은 함수의 실행을 중단하고 함수 몸체에서 빠져나간다
- 반환문은 return 키워드 뒤에 오는 표현식을 평가해 반환한다. 지정하지 않는다면 undefined 가 반환된다.
function multiply(x, y){
return x * y;
// 이후 다른 문 존재 시 무시된다.
console.log('실행안됨');
}
console.log(multiply(3,5)); // 15
function foo() {
}
console.log(foo()); // undefined
12-6. 참조에 의한 전달과 외부 상태의 변경
매개변수가 함수 몸체 내에서 변수와 동일하게 취급된다고 하였다. 그렇기에 매개변수 역시 값에 의한 전달, 참조에 의한 전달 방식을 그대로 따르게 된다.
function changeVal(primitive, obj){
primitive += 100;
obj.name = 'Kim';
}
// 외부 상태
var num = 100;
var person = { name: 'Lee' };
changeVal(num, person);
// 원시값은 원본이 훼손되지 않는다.
console.log(num); // 100
// 객체값은 원본이 훼손된다.
console.log(person) = { name: "Kim" }
매개변수로 num 을 인수로서 대입할 때, num 자체가 전달되어 num 이 변경되지 않을 까 생각하였는데, 원본이 변하지 않았다. 그렇다면 매개변수로 전달된 것은 무엇일까. 표시대로라면 num 이 primative 로 바로 대입될 것 같지만, 사실 매개변수에 전달된 것은 복사된 값이다. 변수 num 이 아닌 num의 값 100 이 전달된 것이다. 그렇기에 함수가 실행되어도 num 은 그대로 변화가 없다. 관계가 없기 때문이다.
허나 아래 객체의 경우 원본이 변경되었다. 예상하겠지만, 객체가 매개변수에 전달하는 것은 값이 아닌 주소의 참조값이다. 즉, 원본 객체의 주소값을 전달하기에, 당연하게도 함수는 원본 객체를 변화시킨다.
12-7. 다양한 함수의 형태
1) 즉시 실행 함수
함수 정의와 동시에 즉시 호출되는 함수를 즉시 실행 함수(IIFE: Immediately Invoked Function Expression) 이라고 한다. 즉시 실행 함수는 단 한번 호출할 수 있고, 다시 호출할 수 없다.
(function() {
var a = 3;
var b = 5;
return a * b;
}());
function () {
...
}(); // SyntaxError: Function statement require a fucntion name;
function foo(){
...
}(); // SyntaxError: Unexpected token ')'
// function foo() {};(); 이렇게 되기 때문에 (); 에 대한 오류 발생
보통은 이처럼 익명함수를 사용하는 편이다. 기명함수를 사용할 수도 있는데 어차피 의미가 없다. () 안에서는 함수 리터럴로 평가되기 때문에, 이름이 있다고 한들 외부에서 다시 호출할 수 없기 때문이다. 함수호출을 식별자를 통해 이루어진다.
또한 전체를 () 로 덮어줘서 사용하는 방식이 가장 일반적이다. 이렇게 하지 않으면 오류가 발생하는데, 첫번째는 익명함수로서 함수 선언문에서는 이름이 있어야하는데 그렇지 않아서 오류다. 아 그렇다면 이름을 넣어주면 되겠지 할 수 있는데, 그래도 오류의 발생이다. 자바스크립트는 암묵적이자 자동적으로 함수 선언문이 끝나는 지점에 세미콜론을 대입한다. 그렇기 때문에 이후 ();가 남게 되어 오류가 발생하는 것이다.
즉시 실행 함수는 일반 함수처럼 값을 반환할 수 있고, 인수를 전달할 수도 있다.
var res = (function(){
var a = 3;
var b = 5;
return a * b;
}());
console.log(res); // 15
res= (function(a, b){
return a * b;
}(3, 5));
console.log(res); // 15
함수가 선언과 동시에 실행되어 버린다면, 외부에서 접근할 수 없어진다. 이러한 특징으로 인해 보통 라이브러리 등에서 활용되곤 한다.
2) 재귀 함수
함수가 자기 자신을 호출하는 것을 재귀 호출(recursive call) 이라고 한다. 재귀함수(recursive function)는 자기 자신을 호출하는 행위, 즉 재귀 호출을 수행하는 함수다.
반복문과 동일하게 재귀함수는 반복적인 처리를 해주는 데 용이하다.
function factorial(n){
if(n <= 1) return 1;
return n * factorial(n-1);
}
console.log(factorial(0)); // 1
console.log(factorial(1)); // 1
console.log(factorial(2)); // 2
console.log(factorial(3)); // 6 = 3 * factorial(2)
...
위 예제는 n factorial 을 구하는 재귀함수다. 반복문을 사용해도 되지만, 이렇게 재귀함수로도 표현할 수 있다.
그리고 위 함수는 함수 선언문이다. 즉 암묵적으로 factorial 이 식별자로 생성되는데, 이 식별자를 함수 내부에서 호출할 수 있고, 나아가 함수 이름 역시 호출할 수 있다. 위 예제는 함수 식별자와 이름이 같기 때문에 다음 예제를 다시 보자
var factorial = function foo(n){
if(n <= 1) return 1;
return n * factorial(n - 1);
// return n * foo(n - 1); 역시 가능하다.
}
// 허나 외부에서 호출할때는 반드시 식별자로 호출해야 한다.
console.log(factorial(5));
이런식으로 재귀함수는 함수 내에서 자기 자신을 호출하는 것이니, 식별자나 이름이나 관계없다. 다만 외부에서 호출할때는 반드시 식별자로 호출해야한다.
이제 if(n <= 1) 을 주목해보자. 재귀함수는 함수 몸체에서 끊임없이 자기 자신을 호출한다. 이렇게 계속 호출하다보면 무한대로 호출이 이루어질거고 당연히 개발자가 의도한 상황이 아니다. 그렇기에 재귀함수에 정말로 중요한 부분은 바로 기저조건을 걸어줘야 한다는 점이다. 즉, 탈출 조건을 만들어야 한다는 걸로 이해하면 될 것이다.
만일 n 이 수없이 작아지다가 1이 되는 순간 기저조건인 조건문이 발동하게 된다. 따라서 함수를 재귀적으로 호출하는것이 아니라 1을 return 한다. 이런식으로 탈출하는 조건이 꼭 있어야 한다.
3) 중첩함수
함수 내부에 정의된 함수를 중첩함수(nested function) 이라고 한다. 중첩함수를 포함하는 외부함수를 외부함수(outer function) 이라고 한다. 중첩함수는 외부 함수 내부에서만 호출할 수 있다.
function outer(){
var x = 1;
// 중첩함수
function inner(){
var y = 2;
// 외부 함수의 변수를 참조할 수 있다.
console.log(x + y); // 3
}
inner();
}
outer();
4) 콜백 함수
예를 들어 인수 n 을 넣으면 n 번만큼 숫자를 콘솔창에 출력하는 함수가 있다고 가정해보자. 반복문을 사용한다면 쉽게 구현할 수 있을 것이다. 그러다 n개의 수 중 홀수만 출력하는 로직을 변경하려면, 함수 전체를 다시 선언해주어야 한다. 바뀌는 로직은 함수 몸체 일부분이지만 말이다.
이렇게 매번 조금의 변화가 있을때마다 함수를 새로 선언하는 방식보다, 함수를 합성하는 것으로 해결할 수 있다. 함수의 변하지 않는 공통 로직은 미리 정의하고, 변경되는 로직은 함수 외부에서 내부로 전달하는 것이다.
function repeat(n, f){
for(var i = 0; i < n; i++){
f(i);
}
}
var logAll = function(i){
console.log(i);
};
repeat(5, logAll); //반복 호출할 함수를 인수로 전달한다.
var logOdds = function (i) {
if(i % 2) console.log(i);
};
repeat(5, logOdds); // 역시나 반복 호출할 함수를 인수로 전달한다.
이처럼 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수(callback function)라고 하며, 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차함수(Higher-Order Function, HOF) 라고 한다.
고차함수는 매개변수를 통해 전달받은 콜백 함수의 호출 시점을 결정해서 호출한다. 외부에서 호출하는 것이 아니라 고차함수에 의해 호출이 된다는 사실을 주목하자. 그렇기 때문에 콜백 함수를 전달할 때 콜백 함수를 호출하지 않고 함수 자체를 전달해야 한다.
보통은 콜백함수가 반드시 고차함수에서만 쓰이는 것이 아니고 다른곳에서도 호출될 수 있기 때문에, 콜백함수를 외부에서 한번 정의를 한 후 함수 참조를 고차 함수에 전달하는 것이 효율적이다. 즉, 함수 자체를 전달한다는 것이다.
var logOdds = function(i){
if(i % 2) console.log(i);
};
// 고차 함수에 함수 참조를 전달한다.
repeat(5, logOdds); // 1 3
콜백 함수는 함수형 프로그래밍 패러다임뿐 아니라 비동기 처리에 활용되는 중요한 패턴이다. 어떠한 이벤트가 발생하였을 때 특수한 기능을 수행하는 콜백함수를 실행시킨다. 이 말은 곧 이벤트의 발생시점이 곧 콜백함수의 호출 시점이고, 그렇기에 위 고차함수로서 콜백 함수의 호출 시점을 결정한다는 말과 같다. 원하는 시점에 함수를 호출한다는 개념이기에 중요할 수 밖에 없다.
// 버튼을 클릭하면 콜백 함수를 실행시킨다.
document.getElementById('myButton').addEventListener('click', function(){
console.log('클릭되었습니다.');
});
// 콜백 함수를 사용한 비동기 처리
// 1초 후 메세지 출력
setTimeout(function(){
console.log('1초 경과');
}, 1000);
이 외에도 배열 고차 함수(reduce, map, filter) 에서도 콜백함수는 사용되는, 이는 추후에 다시 자세히 알아보자.
5) 순수함수와 비순수함수
어떠한 외부 상태에 의존하지도 않고 변경하지도 않는, 즉 부수 효과가 없는 함수를 순수 함수(pure function)라 하고, 외부 상태에 의존하거나 외부 상태를 변경하는, 즉 부수 효과가 있는 함수를 비순수 함수(impure function) 이라고 한다.
순수 함수는 동일한 인수가 전달되면 언제나 동일한 값을 반환하는 함수다. 오직 매개변수를 통해 함수 내부로 전달된 인수에게만 의존해 값을 생성해 반환한다.
반면 함수의 외부 상태에 의존하는 함수는 외부 상태에 따라 반환값이 달라진다. 외부 상태에는 전역 변수, 서버 데이터, 파일, Console 등이 있다. 만일 외부 상태에는 의존하지 않고 함수 내부 상태에만 의존한다 하더라도 그 내부 상태가 호출될 때마다 변화하는 값(예를 들어 현재시간)이라면 순수 함수가 아니다.
순수함수는 최소 한개 이상의 인수를 전달받는데, 만일 인수를 전달받지 않는다면 외부에 의존하지 않는 순수함수는 언제나 동일한 반환값을 반환할테니 그냥 상수와 다를 게 없다. 그리고 순수함수는 전달받은 인수의 불변성을 유지한다.
function add(x, y){
return x + y;
}
add(1, 2); // 3
위 함수는 순수함수다. 외부의 상태를 변경하지도 않고, 외부에 의해 반환값이 결정되지도 않는다. 오직 매개변수 x, y를 통해서만 반환값이 결정되기 때문에 순수함수다. 외부에 의해 반환값이 결정된다는 것은 어떤 의미일까.
var number = 30;
function add(x, y){
return x + y + number;
}
// 외부 number 에 따라 return 값이 변화한다.
add(5, 5); // 40
number = 40;
add(5, 5); // 50
같은 함수같지만 마지막에 number 를 참조하여 반환값을 생성한다. 이렇게 되면 만일 number 값이 변하게 된다면 같은 인수 5,5 를 전달하여 함수를 호출한다고 하지만 그 반환값이 다르게 생성이 된다. 따라서 이는 순수함수가 아니다.
또한 만일 반환값은 일정한다 하더라도 외부 값을 변경한다면 그것 역시 순수함수가 아니다.
var outNum = 30;
function add(x,y){
outNum = y; // 외부상태에 영향
return x + y;
}
반환값은 같지만 외부 변수에 영향을 주는 함수이기에 순수함수가 아니다.
조건이 뭔가 까다롭다. 이렇게 까다로운 조건이라면 순수함수가 가지는 장점이 있지 않을까? 어떠한 점 때문에 순수함수를 함수형 프로그래밍은 추구하려고 하는것일까. 순수함수가 가지는 장점은 크게 2가지가 있는데,
- 실행 시점이 중요해지지 않고, 언제 호출되어도 항상 동일한 결과를 반환한다.
- 순수함수 끼리 조합하여 사용하기가 용이해진다. 따라서 재사용성이 늘어난다.
이러한 장점을 살펴보면, 결국 하나의 함수가 외부에 영향을 끼치지도, 받지도 않기 때문에 사용하기 안전하다는 점이다. 위 덧셈 함수는 명확하게 덧셈을 반환한다. 즉, 인수를 전달하는 입장에서 결과를 손쉽게 예측한다는 점이다. 허나 만약 아래처럼 외부 상황에 따라 반환값이 달라지게 된다면, 어느 환경에서 함수를 호출할지에 따라 결과값이 달라지니 예측이 어려워진다.
예측은 개발자에게 오류를 예방하는 중요한 기점이다. 인간은 누구나 실수를 할 수 있고, 개발자 역시 코드를 작성하다보면 당연히 실수를 하게 된다. 최선은 실수가 일어날 수 있는 가능성을 차단시키는 방향으로 코드를 작성해 나가는 것이다. 이렇게 오류가 날 만한 상황을 최소화 하여 안전성을 높이려는 패러다임을 함수형 프로그래밍이라 한다. 가독성을 해치는 조건문과 반복문을 제거하고, 변수 사용을 최소화 하면서 상태 변경에 따른 오류를 최소화 하려 노력한다. 이러한 패러다임에서 부수 효과가 없는 순수 함수가 가지는 의의가 크다.
함수는 참 친숙하면서도 보면 볼수록 이해가 쉽지 않은 부분인것 같다. 하나의 챕터지만 몇번씩 반복해서 읽으면서 포스팅을 했던 것 같다. 지금도 아리까리한 부분도 많아서 계속해서 접해서 익숙해져야 할 듯 싶다. 누가 그랬는지 모르겠지만 개발은 익숙해지는 과정이라고 했는데... 맞는말 같다.
'Programing > Javascript' 카테고리의 다른 글
[Deep Dive] 전역 변수의 문제점 (0) | 2022.11.08 |
---|---|
[Deep Dive] 스코프 (0) | 2022.11.07 |
[Deep Dive] 원시값과 객체 (0) | 2022.10.25 |
[Deep Dive] 객체 리터럴 (1) | 2022.10.23 |
[Deep Dive] 타입 변환과 단축 평가 (0) | 2022.10.22 |