Eric Elliott의 Master the JavaScript Interview: What is a Pure Function?을 위주로 정리하였습니다.
순수함수(Pure Function)은 함수형 프로그래밍에서 빼놓을 수 없는 중요한 개념이다. 그렇다면 "순수 함수"란 무엇일까?
순수 함수가 무엇인지 알아보기 전에, 함수라는 것은 무엇인지 다시 한번 명확하게 짚을 필요가 있다.
함수의 목적은 다음과 같다.
함수는 결국 1) 입력한 값을 가공한 결과값이 필요할 때 2) 이 결과값으로 여러 과정을 거쳐야 할 때 3) 다른 시스템과 정보를 주고받기 위해 사용한다.
순수 함수에 대해 알아보기 전에, 순수하지 않은 함수에는 어떤 문제가 있는지 알아보자.
함수 foo에서 전역변수 x를 함수 내에서 사용한다고 가정하자. 변수 x를 사용하는 것 자체는 큰 문제가 되지 않는다. 전역변수도 변수고, 변수는 사용하라고 있는 거니까..
// file_001.js
function foo() {
bar에서 조작된 x를 사용
}
(...)
foo();
그런데, 다른 파일에 있는 함수 bar도 전역변수 x를 사용한다. 이 함수 bar는 전역변수 x를 직접 변경한다.
// file_002.js
function bar() {
x를 사용하고 조작
}
(...)
bar();
프로그램의 규모가 작다면 괜찮다. 언제 함수 bar가 호출되는지 알 수 있고, 함수 bar를 찾을 수도 있으니까. 그런데, 프로그램의 규모가 매우 크다면? 함수 bar 외에 다른 함수에서도 전역변수 x의 값을 변경한다면? 그것도 매우 불규칙적인 조건에 따라 x를 변경한다면?
//file_273.js
function bar25() {
스피커가 연결되어 있으면 x를 사용하고 조작
}
function bar238() {
랜덤한 값에 따라 x를 사용하고 조작
}
우리가 함수 foo를 호출할 때 쯤엔 전역변수 x가 어떤 값을 가지고 있을지 예측할 수가 없다. 전역변수 x를 사용하는 여러 함수를 거치면서 x가 어떻게 변했을 지 알 수 없기 때문이다.
한마디로, 함수 foo의 입장에서 전역변수 x는 일관적이지 않은 요소(indeterministic factor)이다. 입력값(=인자)이 일관적이지 않으니, 당연히 함수 foo의 결과값도 일관적이지 않다. 즉, 함수의 결과값을 예측할 수 없다. 정말 끔찍하기 그지없다.
일관적이지 않은 요소는 우리가 상정한 의도에 따른 결과값을 예측할 수 없게 만든다. 결과값을 예측할 수 없는 함수는 우리가 만든 함수임에도 함수를 실행하면 어떤 값이 나올 지 예측할 수 없다. 우리는 이 함수를 테스트하거나, 온전히 이해할 수 없는 것이다.
스칼라의 창시자 Martin Odersky는 다음과 같이 말했다.
비결정성(non-determinism) = 병렬 처리(parallel processing) + 변이할 수 있는 상태(mutable state)
자바스크립트가 싱글 스레드로 동작하기 때문에 병렬 처리 관련 이슈는 없을 거라고 생각할 수도 있겠지만... 자바스크립트는 생각보다 많은 동시성 자원(parallel resources)을 가지고 있다. AJAX, API I/O, 이벤트 리스너, iframes, timeout 등은 우리가 만드는 프로그램에 비결정성 이슈를 만들어낼 수 있다.
동시적으로 작동하는 여러 시스템들이 변수를 공유하고 조작하면 어떻게 될까? 함수 A가 사용하는 전역변수의 값이 우리가 예상한 것과 전혀 다른 값이 되어도 어디서 그 값이 바뀌었는지 예측할수조차 없을 것이다. 또한 우리가 짠 코드임에도 이 함수 A가 우리의 의도대로 작동하지도 않을 것이다.
이러한 문제를 순수 함수를 사용하여 해결할 수 있다.
그렇다면, 순수한 함수는 무엇일까? 다음과 같은 함수를 우리는 순수함수라고 부른다.
상태(state)는 일단은 '변수' 정도로 이해하면 좋다.
아직까지는 아리송하다. 함수라면 당연히 같은 입력값이 주어질 때 같은 결과값을 리턴하는 것 아닌가? 사이드 이펙트는 뭐지? 이런 생각이 들 것이다. (지금 내가 그런 상태...)
순수함수의 첫번째 조건은 같은 입력값에 대해 항상 같은 결과값을 리턴하는 것이다. (앞서 본 예제는 늘 같은 입력값을 넣더라도 전역변수 x 때문에 늘 같은 결과값을 리턴하지 못했다.)
자바스크립트 Math 라이브러리는 Math.random()
이라는 메서드를 지원한다. Math.random()
은 호출될 때마다 0에서 1 사이의 랜덤한 실수를 리턴한다.
Math.random(); //0.9345865343952391
Math.random(); //0.8050833478700568
Math.random(); //0.44961082505037786
Math.random()
함수를 호출했을 때 아무런 인자를 전달하지 않았지만, 모두 다른 값을 출력했다. 이 말인 즉슨, Math.random()
함수는 순수하지 않다. 순수 함수라면 동일한 입력값을 주었을 때 항상 동일한 결과값을 리턴해야 하는데, Math.random()
을 다시 호출했을 때 그 값이 0.44961082505037786
가 나올 거라고 확신할 수 없다.
Math.sqrt()
는 인자로 주어진 숫자의 제곱근을 리턴한다.
Math.random(9); //3
Math.random(4); //2
Math.random(9); //3
Math.sqrt()
함수를 호출했을 때, 주어진 인자에 대해 늘 동일한 결과값(인자의 제곱근)을 리턴한다. 즉, Math.sqrt()
는 순수함수이다. 예제에서도 Math.random(9)
를 실행했을 때, 실행횟수와 관계없이 동일한 인자 9
에 대해 늘 동일한 리턴값 3
을 리턴한 것을 볼 수 있다.
순수 함수는 사이드 이펙트를 만들지 않는다. 다시 말해, 순수 함수는 외부 상태를 바꿀 수 없다.
자바스크립트의 객체, 배열, 함수는 참조 타입(reference type)이다. 참조 타입은 그 값이 저장되는 것이 아니라, 그 값을 저장하고 있는 주소를 담고 있다.
만약 어떤 객체(또는 다른 참조타입)를 함수의 인자로 보내고, 함수 안에서 인자로 주어진 객체의 내용을 변경하면, 원본 객체도 그 값이 변하게 된다.
다시 말해, 외부에서 정의된 참조 타입을 함수 내부에서 변경시키는 것이다.
다음의 예제를 보자.
function addFemale(book, customer, quantity) {
book.push({
customer,
quantity,
});
return book;
}
const bookingObj = {};
const obj = addFemale(bookingObj, { name: "youjin", age: 25 }, 1);
console.log(obj === bookingObj); // true
bookingObj
는 전역에서 정의된 객체이다. 그리고 addFemale()
함수에 해당 객체를 인자로 넘겨, 객체 내에 값을 추가하고 그대로 리턴해주었다. 함수가 인자로 받은 객체는 원본 객체이기 때문에, 해당 객체를 조작하는 것은 외부에서 선언된 객체를 조작하는 것이 된다.
순수 함수의 두번째 조건은 외부 함수를 바꾸지 않는 것(= 사이드 이펙트를 만들지 않는 것)이다. 그러나 bookingObj
는 외부에서 선언된 객체를 조작하였으므로 순수하지 않은 함수라고 볼 수 있다.
bookingObj
함수를 순수하게 만들려면 어떻게 해야 할까? 일단, 함수에 인자로 주어진 객체를 직접 조작하면 안된다!
function addFemale(book, customer, quantity) {
const newBook = { ...book }; // 원본 객체를 얕게 복사
newBook.push({
// 복사된 객체를 조작
customer,
quantity,
});
return newBook; // 복사된 객체를 리턴
}
const bookingObj = {};
const obj = addFemale(bookingObj, { name: "youjin", age: 25 }, 1);
console.log(obj === bookingObj); // true
위의 예제에서는 함수의 인자로 받은 객체를 얕게 복사하고, 원본 객체를 조작하는 대신 복사한 객체를 조작하고 리턴하였다.
수정한 예제에서는 함수 내에서 원본 객체가 변하지 않았기 때문에 사이드 이펙트가 발생하지 않았다. 동시에, 우리가 의도한 대로 함수의 인자에 대한 결과값을 얻어냈다. (위의 코드를 195798번 실행해도 똑같은 결과값이 나올 것이다.) 따라서 수정한 addFemale
함수는 순수 함수의 조건을 만족한다.
결론적으로, 순수 함수를 사용하면 특정 함수가 다른 함수에 미치는 예기치 못한 영향을 최소화할 수 있다. 또한 함수를 만들고 실행할 때 어떤 결과값을 리턴할지 예측할 수 있다는 장점이 있다.