서브루틴으로서의 함수

Subroutin은 아주 오래 된 개념이며 복잡한 코드를 간단하게 만드는 기초적인 수단이다.

서브루틴은 프로시저, 루틴, 서브프로그램, 매크로 등 다양한 이름으로 불립니다. 이들은 모두 매우 단순하고 범용적인, 호출할 수 있는 한 단위를 일컫는 말입니다. 자바스크립트에서는 서브루틴이라는 용어를 쓰지 않고 함수는 함수(메서드) 라고 부를 뿐이죠. 여기서 서브루틴이라는 용어를 쓰는 이유는 함수의 가장 간단한 사용 형태를 강조하기 위해서입니다.

서브루틴은 대개 어떤 알고리즘을 나타내는 형태이다.

함수의 이름을 정하는 건 아주 중요한 일이다. 함수의 이름은 자바스크립트를 위한 일이 아니라 다른 사람, 나중에 이 코드를 다시 볼 누군가를 위해 정하는 것이다. 함수의 이름을 정할 때는 다른 사람이 함수의 이름만 봐도 함수에 대해 알 수 있도록 주의 깊게 작성해야 한다.

함수로서의 함수

입력은 모두 어떤 결과와 관계되어 있고, 프로그래머들은 이렇게 함수의 수학적인 정의에 충실한 함수를 순수한 함수라고 부른다.

순수한 함수의 특징

  • 순수한 함수에서는 입력이 같으면 결과도 반드시 같다
  • 순수한 함수에는 Side Effect가 없다
  • 순수한 함수를 쓰면 코드를 테스트하기 쉽고, 이해하기 쉽고, 재사용하기 쉽다.

IIFE와 비동기적 코드

IIFE (즉시 호출하는 함수 표현식)를 사용하는 사례 중 하나는 비동기적 코드가 정확히 동작할 수 있도록 새 변수를 새 스코프에 만드는 것이다.

5초에서 시작하고 카운트다운이 끝나면 "Go"를 표시하는 예제

var i;
for(i = 5; i >= 0; i--) {
    setTimeout(function(){
        console.log(i === 0 ? "Go" : i);
    }, (5-i)*1000);
}

5, 4, 3, 2, 1, Go 가 출력될 거라 예상했지만 -1 이 여섯 번 출력됐다.
setTimeout에 전달된 함수가 루프 안에서 실행되지 않고 종료된 뒤에 실행됐기 때문이다. 따라서 콜백 함수가 호출되는 시점에서 i의 값은 -1이다.

let을 사용해 블록 수준 스코프를 만들면 이 문제는 해결되지만, 비동기적 프로그램에 익숙하지 않다면 이 예제를 정확하게 이해해야 한다.

변수로서의 함수

이번 파트는 프로그래밍을 처음 공부하는 입장에서 복잡하고 여렵지만 꼭 이해해야 하는 중요한 개념이다.

숫자나 문자열, 배열을 변수라고 생각하면 별 거부감이 들지 않는다.

변수 = 데이터
배열, 객체 = 데이터의 모임

이렇게 생각하는 것이 익숙하기 때문이다. 하지만 함수도 다른 변수와 마찬가지로 이리저리 전달할 수 있다.
함수는 능동적인 것이므로, 보통 수동적이라고 생각하는 데이터와 연결이 잘 되지 않을 수 있지만, 함수 역시 호출하기 전에는 다른 변수와 마찬가지로 수동적이다.

변수로서의 함수가 어떤 일을 할 수 있는지 알아보자.

함수를 가리키는 변수를 만들어 별명을 정할 수 있다.

함수에 별명을 붙인다는 것은 만약 짧은 코드 안에서 여러 번 호출해야 하는 함수가 있는 경우,
이 함수의 이름이 너무 길어서 타이핑하기 번거롭고, 코드의 가독성을 해치게 된다.

이런 경우에 함수도 데이터이므로 짧은 이름의 데이터에 저장할 수 있다.

function addThreeSquareAddFiveTackSquareRoot(x){
    // 엄청난 길이의 이름을 갖는 함수
    return Math.sqrt(Math.pow(x+3, 2) + 5);
  }

  // 별명을 쓰기 전
  const answer = (addThreeSquareAddFiveTackSquareRoot(5) +
    addThreeSquareAddFiveTackSquareRoot(2)) /
    addThreeSquareAddFiveTackSquareRoot(7);

  // 별명 사용 후
  const f = addThreeSquareAddFiveTackSquareRoot;
  const answer = (f(5)+f(2)) / f(7);

배열 안의 함수

배열안에 함수를 쓰는 패턴은 오래되지 않았지만 점점 늘어난다고 한다, 특정 상황에서 대단히 유용하고
자주 하는 일을 한 셋으로 묶는 파이프라인이 좋은 예이다.
배열을 사용하면 작업 단계를 언제든 쉽게 바꿀 수 있다는 장점이 있고, 어떤 작업을 빼야 한다면 배열에서 제거하기만 하면 되고, 추가할 작업은 추가하기만 하면 된다.

파이프라인은 그래픽 애플리케이션에서만 쓰이는 건 아니며 오디오 처리와 과학 및 공학 애플리케이션에서도 자주 사용한다.
일정한 순서에 따라 함수를 실행해야 한다면 파이프라인을 써서 효율적으로 일할 수 있다.

함수에 함수 전달

함수에 함수를 전달하는 예제는 setTimeoutforEach에서 이미 사용했고,
다른 용도로는 비동기적 프로그래밍이다. 이런 용도로 전달하는 함수를 보통 콜백이라 부르며 콜백 함수는 자신을 감싼 함수가 실행을 마쳤을 때 호출된다.

함수는 동작이고, 함수를 받은 함수는 그 동작을 활용할 수 있다.
배열에 들어있는 숫자를 모두 더하는 단순한 함수 sum이 필요하다면 쉽게 만들 수 있다. 그런데 숫자의 제곱을 합해서 반환하는 함수가 필요하다면? 제곱근을 반환하는 새 함수를 만들어도 되지만, 함수에 함수를 전달한다는 발상은 임의의 함수를 sum에 전달하는 것이다.

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에 전달하면 원하는 일을 거의 모두 할 수 있게된다.
함수를 넘기지 않고 sum을 호출하면 매개변수 f의 값은 undefined이므로 에러가 일어난다. 에러를 방지하기 위해 함수가 아닌 것은 모두 null 함수로 바꾼다. 이런식으로 안전한 함수를 만들 수 있다.

함수를 반환하는 함수

함수를 반환하는 함수는 아마 가장 난해한 사용법일 수 있지만, 그만큼 유용하기도 하다.

sum 함수를 다시 생각해서 제곱근의 합을 구하는 함수, 세제곱의 합, 이런 패턴이 계속 반복되는 함수가 필요한 경우에는
필요한 함수를 반환하는 함수를 만들어 문제를 해결할 수 있다.


  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

이 예제처럼 매개변수 여러 개를 받는 함수를 매개변수 하나만 받는 함수로 바꾸는 것을 커링 (currying) 이라 부르며,
커링이라는 이름은 이 패턴을 만든 미국의 수학자 하스켈 커리의 이름을 딴 것이다.

함수를 반환하는 함수의 예제로는 자바스크립트 웹 프레임워크인 익스프레스 와 Koa 같은 미들웨어 패키지를 살펴보면 확인할 수 있다. 미들웨어는 대개 함수를 반환하는 함수 형태로 만들어진다.

재귀

재귀란 자기 자신을 호출하는 함수로, 같은 일을 반복하면서 그 대상이 점차 줄어드는 상황에서 재귀를 유용하게 활용할 수 있
다.

만약 건초 더미에서 바늘을 찾아야 하는 상황이라고 생각해보자

  1. 건초 더미에서 바늘이 보이면 3단계로 이동
  2. 건초 더미에서 건초를 하나 덜어낸다. 1단계로 이동
  3. 찾았다!

바늘을 찾을 때까지 건초 더미에서 건초를 하나씩 제외하는 소거법이며, 이것이 재귀이다.
코드로 바꿔 보자.

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']);

중요한 점은 모든 가능성을 전부 고려한다는 것이다.

  • if (haystack이 비어 있다)
  • if (배열의 첫 번째 요소가 바늘이거나 바늘이 아닌 경우)
  • if (배열 어딘가에 바늘이 있을 테니 push()를 사용해 첫 번째 요소를 제거하고 함수 반복)

재귀 함수에는 종료 조건이 있어야 한다. 종료 조건이 없다면 자바스크립트 인터프리터에서 스택이 너무 깊다고 판단할 때까지 재귀 호출을 계속 하다가 프로그램이 멈추기 때문이다.

마무리

이번 파트 공부를 통해서 함수를 유용하게 사용할 수 있는 새로운 사고방식을 알게되었고, 역시 어려움을 느꼈다.
함수형 프로그래밍을 통해 할 수 있는일이 굉장히 많고 앞으로 상황에 맞는 '좋은 방법'을 찾는데 시간을 갖고 노력해야겠다.