함수형 자바스크립트의 기초

2017년 03월 15일


명령형과 조합하기

모든 상황에 함수형 프로그래밍이 적합한 것은 아닙니다.

함수형으로 작성하는 것이 오히려 불편하거나, 반복과 같은 최적화가 필요한 부분에는 명령형을 섞어 사용할 수 있습니다.

명령형 프로그래밍은 기본적으로 값을 변경하는 사이드 이펙트에 의존하기 때문에 항상 함수형 프로그래밍과 조합할 수 있는 것은 아닙니다.

하지만 사이드 이펙트가 함수형 프로그래밍을 망치지 않는 상황에선 이 둘을 적절하게 조합해서 사용할 수 있습니다.

함수 안에서 일어나는 일

지역변수는 함수가 실행되는 동안 발생했다가 호출이 종료되면 반환되거나 제거됩니다. 이러한 값들은 함수가 실행되는 동안은 값을 변경해도 함수 밖에 영향을 끼치지 않습니다.

함수형 프로그래밍에서는 함수 안의 동작을 크게 신경쓰지 않기 때문에, 사이드 이펙트만 함수 밖으로 노출되지 않는다면 아무런 문제가 없습니다.

function keys(object) {
  let z = [];

  for (const key in object)
    z.push(key);

  return z;
}

const object = { a: 10, b: { c: 20 } };
keys(object); // ['a', 'b'];

위 코드는 함수 안에서 바라보면 z를 변경됐기 때문에 사이드 이펙트가 맞지만, 함수 밖에서는 호출이 일어나는 동안은 z가 무엇인지 알 수 없습니다.

z는 사이드 이펙트가 있고 최종적으론 함수 밖으로 반환되지만, 함수가 실행되는 동안은 함수 밖에 어떠한 영향도 주지않기 때문에 함수형 프로그래밍을 망치지 않습니다.

트랜션트

함수가 실행되는 동안은 지역변수의 값을 변경해도 외부에 영향이 없습니다. 이러한 지역변수의 성질을 이용하는 것을 트랜션트라 합니다.

트랜션트는 자바스크립트가 순수 항수형 언어가 아니기 때문에 갖는 독특한 성질입니다.

함수형 프로그래밍이 갖는 장점을 누리다가도, 명령형 방식이 필요하다면 함수로 장벽을 새우고 명령형 프로그래밍을 조합할 수 있습니다.

이 방법은 편리하지만 사이드 이펙트가 함수 밖으로 노출되는 순간 함수형 프로그래밍을 망치기 시작합니다.

다음과 같은 경우가 대표적입니다.

const concat = (xs, ys) => ys.reduce((zs, y) => {
  zs.push(y);
  return zs;
}, xs);

const as = [1, 2];
const bs = [3, 4];
const cs = concat(as, bs);

as; // [1, 2, 3, 4]
bs; // [3, 4]
cs; // [1, 2, 3, 4]

zs는 지역변수가 아닌 인자고, 함수 외부에서 전달된 파라미터 xs를 직접 참조하고 있습니다.

참조의 참조를 추적해보면 zs는 xs를 거쳐 as와 연결돼있는 것을 알 수 있습니다. 즉, 함수 내부에서 zs에 사이드 이펙트를 주면 함수의 외부(as)에 영향을 줍니다.

사이드 이펙트가 없는 함수를 호출하고 있다 착각하게 만들기 때문에 버그의 원인을 찾기도 어렵습니다.

캐싱

함수형 프로그래밍에서는 함수는 인자에 대한 값에만 신경씁니다. 항상 같은 인자에 대해 같은 값을 반환하고, 사이드 이펙트만 밖으로 노출되지 않는다면 함수가 어떻게 구현되던 상관이 없습니다.

이러한 특징 덕분에 설계를 함수형으로 하면 캐싱 기능을 추가하기 쉽습니다.

function memoize(f) {
  let cache = {};

  return (x) => {
    const y = cache[x];

    if (y !== undefined) {
      console.debug('cache hit');
      return y;
    } else {
      console.debug('cache miss');
      return cache[x] = f(x);
    }

  }
}

function fact(n) {
  let z = 1;
  for (let i = 1; i <= n; ++i) 
    z *= i;
  return z;
}

const myFact = memoize(fact);

myFact(5); // 120 cache miss
myFact(5); // 120 cache hit

웹 환경에서 이 아이디어는 네트워크에서 데이터를 가져올 때 유용합니다.

비동기 처리와 에러 핸들링 등 구현과 관련된 것은 잠시 잊고, 서버에서 데이터를 가져오는 것을 단순히 함수 호출로 생각해봅시다.

사용자가 머무르는 동안 서버의 상태가 변하지 않는다면, 이 함수 호출은 사이드 이펙트가 없는 함수로 간주할 수 있습니다.

이 블로그 같은 사이트가 대표적입니다. 게시글은 자주 갱신되지 않기 때문에, 서버를 상태에 의존하지 않는 함수로 생각할 수 있습니다.

실제로 블로그에서 페이지를 처음 읽으면 서버에서 데이터를 가져오지만, 이후에는 캐싱된 데이터를 사용합니다. 새로고침을 하기 전까진 한 페이지는 한 번만 로딩됩니다.