함수와 추상적 사고
서브루틴으로서의 함수
- 서브루틴은 복잡한 코드를 간단하게 만드는 기초적인 수단
- 서브루틴은 반복되는 작업의 일부를 떼어내서 이름을 붙이고, 그 이름을 부르기만 하면 실행
- 서브루틴은 어떤 알고리즘을 나타내는 형태
- 윤년(leap year) 판단 알고리즘
const year = new Date().getFullYear(); if(year % 4 !== 0) console.log(`${year} is NOT a leap year.`); else if(year % 100 != 0) console.log(`${year} IS a leap year.`); else if(year % 400 != 0) console.log(`${year} is NOT a leap year.`); else console.log(`${year} IS a leap year.`); - 프로그램안에서 이 코드를 여러번 실행하야 한다면? 그 후에 특정 부분을 추가하거나 수정해야 한다면?
- 자바스크립트에서는 함수를 사용하여 해당 문제 해결
function printLeapYearStatus() { const year = new Date().getFullYear(); if(year % 4 !== 0) console.log(`${year} is NOT a leap year.`); else if(year % 100 != 0) console.log(`${year} IS a leap year.`); else if(year % 400 != 0) console.log(`${year} is NOT a leap year.`); else console.log(`${year} IS a leap year.`); } - 함수의 이름은 다른 사람, 또는 나중에 이 코드를 다시 볼 당신을 위해 정하는 것
- 너무 길지 않게, 그렇다고 의미를 알아채기 어렵도록 너무 짧게 하지 않는 수준에서 이름을 정해야 함
값을 반환하는 서브루틴으로서의 함수
printLeapYearStatus함수는 잘 동작하지만, 프로그램이 커지면 콘솔에 기록하는 것은 곧 쓸모 없어짐- HTML에 출력하거나, 파일에 저장하거나, 다른 계산에 사용해야 할 수도 있는데 지금 가진 함수는 그렇게 활용할 수 없음
- 값을 반환하도록 변경해야 함
function isCurrentYearLeapYear() { const year = new Date().getFullYear(); if(year % 4 !== 0) return false; else if(year % 100 != 0) return true; else if(year % 400 != 0) return false; else return true; } - 함수의 반환값을 활용하는 예
const daysInMonth = [31, isCurrentYearLeapYear() ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; if(isCurrentYearLeapYear()) console.log('It is a leap year.');
함수로서의 함수
- 입력은 모두 어떤 결과와 관계되어 있음
- 이렇게 함수의 수학적인 정의에 충실한 함수를 순수한 함수(pure function)라 부름
- 순수한 함수에서는 입력이 같으면 결과도 반드시 같음
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']; let colorIndex = -1; function getNextRainbowColor() { if(++colorIndex >= colors.length) colorIndex = 0; return colors[colorIndex]; } getNextRainbowColor함수는 호출할 때마다 무지개의 일곱 가지 샊갈을 하나씩 반환- 이 함수는 순수한 함수의 두 가지 정의를 모두 어김
- 입력이 같아도 결과가 항상 다르고, 변수
colorIndex를 바꾸는 부수 효과도 있음 - 윤년 문제로 돌아가서 이 함수를 순수한 함수로 고치려면?
function isLeapYear(year) { if(year % 4 !== 0) return false; else if(year % 100 != 0) return true; else if(year % 400 != 0) return false; else return true; } getNextRainbowColor함수를 순수한 함수로 고치려면?const getNextRainbowColor = (function() { const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']; let colorIndex = -1; return function() { if(++colorIndex >= colors.length) colorIndex = 0; return colors[colorIndex]; }; })();- 부수 효과는 없어졌지만, 아직은 입력이 같아도 결과가 다를 수 있음
- 이터레이터를 사용한다면 해당 결과값이 이터레이터로 반환되므로 항상 같은 것을 반환함
function getRainbowIterator() { onst colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']; let colorIndex = -1; return { next() { if(++colorIndex >= colors.length) colorIndex = 0; return { value: colors[colorIndex], done: false }; } }; } - 결국
next()메서드는 매번 다른 값을 반환할 테니, 문제를 뒤로 미뤘을 뿐 아니냐고 생각할 수 있지만next()는 함수가 아니라 메서드임. 메서드는 자신이 속한 객체라는 컨텍스트 안에서 동작하므로 메서드의 동작은 그 객체에 의해 좌우됨 - 프로그램의 다른 부분에서
getRainbowIterator를 호출하더라도 독립적인 이터레이터가 생성되므로 다른 이터레이터를 간섭하지 않음
그래서?
- 왜 함수를 사용할까? 반복을 없애기 위해..
- 자주 사용하는 동작을 하나로 묶을 수 있다
- 순수한 함수를 권장
함수도 객체다
- typeof를 사용하면 “function”이 반환
IIFE와 비동기적 코드
- IIFE (즉시 호출하는 함수 표현식)
- IIFE를 이용해서 클로저를 만들 수 있음
- IIFE를 사용하는 사례 중 하나는 비동기적 코드가 정확히 동작할 수 있도록 새 변수를 새 스코프에 만드는 것
- 5초에서 시작하고 카운트다운이 끝나면 “go”를 표시하는 타이머 예제
var i; for(i=5; i>=0 i--) { setTimeout(function() { console.log(i===0 ? "go" : i); }, (5-i)*1000); } let대신var를 쓴 이유는 IIFE가 중요하던 시점으로 돌아가서 왜 중요했는지 이해하기 위함5,4,3,2,1, go가 출력될 거라 예상했다면 틀렸음.-1만 6번 출력됨setTimeout에 전달된 함수가 루프 안에서 실행되지 않고 루프가 종료된 뒤에 실행됐기 때문let을 사용해 블록 수준 스코프를 만들면 해결되지만, 비동기적 프로그래밍에 익숙하지 않다면 정확히 이해해야 함- 블록 스코프 변수가 도입되기 전에는 이런 문제를 해결하기 위해 함수 하나 더 썼음
- 함수를 하나 더 쓰면 스코프가 새로 만들어지고 각 단계에서 i의 값이 클로저에 캡처됨
function loopBody(i) { setTimeout(function() { console.log(i===0 ? "go" : i); }, (5-i)*1000); } var i; for(i=5; i>=0; i--) { loopBody(i); } - 루프의 각 단계에서
loopBody함수가 호출됨 - 자바스크립트는 매개변수를 값으로 넘김
- 따라서 루프의 각 단계에서 함수에 전달되는 것은 변수 i가 아니라 i의 값
- 스코프가 7개 만들어졌고, 변수도 7개 만들어졌다는 부분이 중요
- 루프에 한 번 쓰고 말 함수에 일일이 이름을 붙이는 건 성가신 일
- 익명 함수를 만들어 즉시 호출하는 IIFE를 사용하는 게 더 나음
var i; for(i=5; i>=0; i--) { (function() { setTimeout(function() { console.log(i===0 ? "go" : i); }, (5-i)*1000); })(i); } - 블록 스코프 변수를 사용하면 스코프 하나 때문에 함수를 새로 만드는 번거로운 일을 하지 않아도 됨
for(let i=5; i>=0; i--) { setTimeout(function() { console.log(i===0 ? "go" : i); }, (5-i)*1000); } let키워드를 for루프 바깥에 썼다면 똑같은 문제가 발생했을 것let키워드를 이렇게 사용하면 루프의 단계마다 변수 i의 복사본을 새로 만듬- 따라서
setTimeout에 전달된 함수가 실행될 때는 독립 스코프에서 변수를 받음
변수로서의 함수
- 함수를 가리키는 변수를 만들어 별명을 정할 수 있음
- 배열에 함수를 넣을 수 있음(다른 타입의 데이터와 섞일 수 있음)
- 함수를 객체의 프로퍼티로 사용할 수 있음
- 함수를 함수에 전달할 수 있음
- 함수를 매개변수로 받는 함수를 반환하는 것도 가능 ```javascript function addThreeSquareAddFiveTakeSquareRoot(x) { return Math.sqrt(Math.pow(x+3, 2)+5); }
// 별명 쓰기 전 const answer = (addThreeSquareAddFiveTakeSquareRoot(5) + addThreeSquareAddFiveTakeSquareRoot(2)) / addThreeSquareAddFiveTakeSquareRoot(7);
// 별명 사용 const f = addThreeSquareAddFiveTakeSquareRoot; const answer = (f(5) + f(2)) / f(7);
- 별명 붙일 때 `addThreeSquareAddFiveTakeSquareRoot`뒤에 괄호를 붙이지 않았음
- 괄호를 붙이면 함수를 호출하고, f에 함수 결과가 저장됨
```javascript
const Money = require('math-money'); // require는 라이브러리를 불러오는 노드 함수
const oneDollar = Money.Dollar(1);
// Money.Dollar도 길게 느껴지면
const Dollar = Money.Dollar;
const twoDollars = Dollar(2);
// Dollar와 twoDollars는 같은 타입의 인스턴스
배열 안의 함수
- 자주하는 일을 한 셋으로 묶는 파이프라인 예제
- 배열을 사용하면 작업 단계를 언제든 쉽게 바꿀 수 있다는 장점
- 그래픽 변형을 예로 들어보면 시각화 소프트웨어를 만들 때는 변형을 파이프라인으로 묶어서 적용할 때가 많음 ```javascript const sin = Math.sin; const cos = Math.cos; const theta = Math.PI/4; const zoom = 2; const offset = [1, -3];
const pipeline = [ function rotate(p) { return { x: p.x * cos(theta) - p.y * sin(theta), y: p.x * sin(theta) - p.y * cos(theta), }; }, function scale(p) { return { x: p.x * zoom, y: p.y * zoom }; }, function translate(p) { return { x: p.x + offset[0], y: p.y + offset[1]; }; }, ];
// pipeline은 2D변형에 필요한 함수의 배열 // 점 하나를 변형해 보면 const p = { x: 1, y: 1 }; let p2 = p; for(let i=0; i<pipeline.length; i++) { p2 = pipeline[i][p2]; }
// p2는 이제 P1을 좌표 원점 기준으로 45도 회전하고 // 원점에서 2 단위만큼 떨어뜨린 후 // 1단위 오른쪽, 3단위 아래쪽으로 움직인 점입니다.
- 파이프라인의 각 함수를 호출할 때 사용한 문법을 보면 pipeline[i]는 파이프라인의 i번째 요소에 접근하고, 그 요소는 함수로 평가
- 각 함수에 점을 전달하고, 반환값을 다시 그 점에 할당
#### 함수에 함수 전달
- 함수에 함수를 전달하는 다른 용도는 비동기적 프로그래밍
- 이런 용도로 전달하는 함수를 보통 콜백이라 부르며 약자로 cb라 씀
- 콜백 함수는 자신을 감싼 함수가 실행을 마쳤을 때 호출됨
- 함수는 동작이고, 함수를 받은 함수는 그 동작을 활용할 수 있음
```javascript
function sum(arr, f) {
// 함수가 전달되지 않았으면 매개변수를 그대로 반환하는 null 함수를 씀
if(typeof f != 'function') f = x => x;
return arr.reduce((a, x) => a += f(x), 0);
}
sum([1, 2, 3]); // (6)
sum([1, 2, 3], x => x*x); // 제곱을 합해서 반환하는 함수 (14)
sum([1, 2, 3], x => Math.pow(x, 3)); // 세제곱을 합해서 반환하는 함수 (36)
- 함수를 넘기지 않고
sum을 호출했을 때if문이 없다면 매개변수f의 값은 undefined이므로 에러가 발생 - 에러를 방지하기 위해 위의 예제처럼
if문을 사용하면 함수가 아닌 것은null 함수. 즉, 아무 일도 하지 않는 것으로 변경 함(null함수 f에 5를 넘기면 그대로 5를 반환)
함수를 반환하는 함수
sum함수를 다시 보면 이 함수는 각 요소를 더하기 전에 해당 요소를 바꾸는 함수를 받음- 배열과 함수를 받는 함수로는 만족스런 결과를 얻을 수 없고, 배열 하나만 받아서 제곱의 합을 반환하는 함수가 필요하다고 하면
- 이미 만들어 둔
sum함수를 활용하여 아래와 같은 함수를 생성function sumOfSquares(arr) { return sum(arr, x => x*x); } - 필요한 것이 함수 하나라면 위와 같이 해도 됨
- 하지만 제곱근의 합을 구하는 함수, 세제곱의 합을 구하는 함수, 이런식으로 패턴이 반복 된다면 필요한 함수를 반환하는 함수를 만들어 문제를 해결할 수 있음
function newSummer(f) { return arr => sum(arr, f); }const sumOfSquares = newSummer(x => x*x); const sumOfCubes = newSummer(x => Math.pow(x, 3)); sumOfSquares([1, 2, 3]); // 14 sumOfCubes([1, 2, 3]); // 36 - 위 예제처럼 매개변수 여러 개를 받는 함수를 매개변수 하나만 받는 함수로 바꾸는 것을
커링이라 부름
재귀
- 재귀란 자기 자신을 호출하는 함수
- 같은 일을 반복하면서 그 대상이 점차 줄어드는 상황에서 유용하게 쓰임
- 건초 더미에서 바늘을 찾는 예제를 살펴보면
- 건초 더미에서 바늘이 보이면 3단계로 이동한다.
- 건초 더미에서 건초를 하나 덜어낸다. 1단계로 이동한다.
- 찾았다.
- 바늘을 찾을 때까지 건초 더미에서 건초를 하나씩 제외하는 소거법이며, 재귀이다. ```javascript function findNeedle(haystack) { if(haystack.length == 0) return “no haystack here!”; if(haystack.shift() === ‘needle’) return “found it!”; return findNeedle(haystack); // 건초더미에 들어있는 건초가 하나 줄어듬 }
findNeedle([‘hay’, ‘hay’, ‘hay’, ‘hay’, ‘needle’, ‘hay’, ‘hay’]);
- 이 함수는 모든 가능성을 전부 고려
- 경우의 수는 haystack이 비어있거나, 배열의 첫 번째 요소가 바늘이거나, 바늘이 아닌 경우
- 마지막은 배열의 어딘가에 바늘이 들어있을 테니 `Array.prototype.shift`로 배열의 첫 번째 요소를 제거하고 함수를 반복
- 재귀 함수에는 종료 조건이 있어야 함
- `findNeedle`에는 두가지 종료 조건이 있음
- 바늘을 찾거나
- 배열이 비어 있거나
- 호출할 때마다 배열의 길이가 줄어들므로 언젠가는 두 조건 중 하나를 만족하게 됨
- 숫자의 계승(factorial)을 찾는 예제
```javascript
function fact(n) {
if(n === 1) return 1;
return n * fact(n-1);
}
- 이 함수의 종료 조건은
n === 1이고, 재귀 호출할 때마다 숫자 n은 1씩 줄어 들다가 결국 1이 됨