함수형 프로그래밍, 콜백과 클로저

함수를 값처럼 사용하고 함수의 응용을 강조하는 프로그래밍 패러다임에 대해 알아보고, 콜백(Callback), 클로저(Closure)에 대해서 알아봅니다.
콜백, 클로저, 함수형 프로그래밍


함수도 객체(값)다

합성 함수합성 함수

함수를 값으로 다루기

var sum = function(a,b){ return a+b; };
func1(1);
func2("abc", 123);
func3(sum);
func4("abc", sum);
func5("xxx", func3, function(){ ... });
func6(123, function(){ ... }, function(){ ... });

JavaScript에서는 함수 역시 객체, Function 타입의 인스턴스입니다. 변수에 할당을 할 수도 있고, 다른 함수를 실행할 때 인자로 넘길 수도 있습니다. 함수형 프로그래밍은 이런식으로 함수의 응용을 강조하는 일종의 패러다임입니다.

좀 더 이론적으로 말하자면, 함수형 프로그래밍은 입력 값을 목적 값을 향해서 순차적으로 변화시키기(명령형 프로그래밍) 보다는, 함수의 합성을 응용하여 입력 값으로 부터 목적 값을 생성하는 프로그래밍 패러다임을 말합니다.

함수형 프로그래밍은 수학적인 사고방식을 표현하기에 적합하며, 복잡한 로직을 단순하게 표현 할 수 있고, 또 일반적으로 입력 값에 대한 변화를 주지 않고(Immutable), 함수를 통해 새로운 결과를 생성하기 때문에 프로그램의 안정성을 향상 시킬 수 있다는 장점이있습니다. 현실적으로는 이러한 함수형 스타일과 명령형, 객체지향적 스타일이 뒤섞여 쓰이는 것이 일반적입니다.

함수의 축약 문법

var func1 = function(x, y){
  console.log(x, y);
};

var func2 = (x, y) => {
  console.log(x, y);
};

var func3 = x => { // 인자가 하나면 주머니를 생략 가능
  console.log(x);
};

var func4 = x => console.log(x);
// 함수내 코드가 한줄이면 블록을 생략 가능
// 이때는 그 한줄의 코드를 계산한 값이 리턴됩니다.

var func5 = x => x + 1;

var func6 = function(x) {
  return x + 1;
};
// 즉 func5와 func6의 기능은 동일합니다.

JavaScript의 최신 버전인 ES6에서는 Arrow Function이라고 하는 함수를 정의하는 새로운 방법이 도입되었습니다. Arrow Function 객체는 Function 객체와 기능적으로 거의 동일합니다. 그 약간의 차이에 대해서는 추후 알아보겠습니다. 또한 Node.js 파트에서는 Arrow Function을 사용해도 문제가 없습니다만, 추후 JavaScript 파트에서는 웹 브라우저 호환성을 위해서 Arrow Function을 지양합니다.

말이 되는 코드

class Obj {
  constructor(name){
    this.name = name;
  }
}
var objs = [new Obj("a"), new Obj("b"), new Obj("c")];

// 사람보다는 기계에게 친숙한 표현
for(var i=0; i < objs.length; i++) {
  var obj = objs[i];
  console.log(obj.name);
}

// 조금 더 사람에게 말이되는 코드?
objs.forEach(obj => {
  console.log(obj.name);
});

Array의 메소드

우리가 앞으로 프로그램을 작성하는 데 Collection을 다루는 경우는 비일비재합니다. 아래에서 Array의 forEach 같은 주요한 메소드에 대해 알아보면서 functional 프로그래밍에 대한 감을 익혀보도록 하겠습니다.

var arr = [
  {x:1, y:'hello1'},
  {x:2, y:'hello2'},
  {x:5, y:'hello5'},
  {x:3, y:'hello3'},
  {x:4, y:'hello4'},
];

forEach

// forEach: arr의 원소들에 반복적으로 같은 작업하기
for(var i=0; i<arr.length; i++) console.log(arr[i].x);
arr.forEach(obj => console.log(obj.x));

map

// map: arr의 원소들에 반복적으로 같은 변화를 준 새로운 arr
var arr2 = [];
for(var i=0; i<arr.length; i++) {
  var obj = arr[i];
  var str = obj.x + obj.y;
  arr2.push(str);
}
var arr2 = arr.map(obj => obj.x + obj.y);

filter

// filter: arr의 원소들 중에 특정 로직을 만족하는 원소만 걸러낸 새로운 arr
var arr2 = [];
for(var i=0; i<arr.length; i++) {
  var obj = arr[i];
  if (obj.x > 3) arr2.push(obj);
}
var arr2 = arr.filter(obj => obj.x > 3);

some

// some: arr의 원소들 중에 특정 로직을 만족하는 원소가 하나라도 있는지?
var bool = false;
for(var i=0; i<arr.length; i++) {
  var obj = arr[i];
  if (obj.x > 3) {
    bool = true;
    break;
  }
}
var bool = arr.some(obj => obj.x > 3);

every

// every: arr의 원소들이 모두 특정 로직을 만족하는지?
var bool = true;
for(var i=0; i<arr.length; i++) {
   var obj = arr[i];
   if (!(obj.x > 3)) {
    bool = false;
    break;
   }
}
var bool = arr.every(obj => obj.x > 3);

find

// find: arr의 원소들 중에 특정 로직을 만족하는 첫번째 원소
var obj = null;
for(var i=0; i<arr.length; i++) {
   var o = arr[i];
   if (o.x > 3) {
    obj = o;
    break;
   }
}
var obj = arr.find(o => o.x > 3);

findIndex

// findIndex: arr의 원소들 중에 특정 로직을 만족하는 첫번째 원소의 인덱스
var objIndex = -1;
for(var i=0; i<arr.length; i++) {
   var o = arr[i];
   if (o.x > 3) {
    objIndex = i;
    break;
   }
}
var objIndex = arr.findIndex(o => o.x > 30);
console.log(objIndex);

sort

// sort: arr의 원소들을 특정 로직에 따라 정렬한 새로운 arr
var arr2 = [];
for(var i=0; i<arr.length; i++)
  arr2.push(arr[i]);

for(var i=0; i<arr2.length; i++) {
  for(var j=i+1; j<arr2.length; j++) {
    if (arr2[i].x < arr2[j].x) {
      var temp = arr2[i];
      arr2[i] = arr2[j];
      arr2[j] = temp;
    }
  }
}

var arr2 = arr.sort((a,b) => a.x < b.x ? 1 : (a.x == b.x ? 0 : -1));
console.log(arr2);

indexOf

// indexOf: 단순한 비교로 Array의 인덱스를 찾는 메소드
var arr = [1,2,3,4,5];
var index = arr.findIndex(o => o == 3); // 2
var index = arr.indexOf(3); // 2

concat

// concat: Array를 이어 붙히기
var arr1 = [1,2,3];
var arr2 = arr1.concat(arr1, [1,2,3], [4,5,6]);
console.log(arr2);

slice

// slice: Array를 특정 구간만 잘라내기
var arr1 = [1,2,3,4,5];
var arr2 = arr1.slice(0,-2);
console.log(arr2);

splice

// splice: Array의 특정 구간을 바꿔치우기
var arr1 = [1,2,3,4,5];
var arr2 = arr1.splice(0, 4, 1,2,3,4,5,6,7,8,8);
console.log(arr1, arr2);

join

// join: Array를 String으로 합성하기
var arr1 = ['abc', 'gmail.com', 'xxx'];
console.log(arr1.join('_'));

split

// split: String을 분리해 Array로 만들기
var arr2 = '010-1234-1234'.split('-').join('.');
console.log(arr2);

reduce

// reduce: arr의 원소들을 특정 로직에 따라 누적하며 계산해 나간 결과 값
var numbers = [1,2,3,4,5,6,7,8,9];

var result = 0;
for(var i=0; i<numbers.length; i++)
  result += numbers[i];

console.log(result);
// 0 1 => 1
// 1 2 => 3
// 3 3 => 6
// ...

var result = numbers.reduce((result, number) => {
  return result + number;
}, 0);
console.log(result);


var result = numbers
  .map(num => num*num)
  .reduce((result, num) => result * num, 1);
console.log(result);


var numbers2 = [
  [1,2,3],
  [6,5,4],
  [7,8,9],
];

// [9,8,7,6,5,4,3,2,1]
// "9-8-7-6-5-4-3-2-1"
// reduce, concat, sort, join

var numbers3 = numbers2
  .reduce((result, arr) => result.concat(arr), [])
  .sort()
  .reverse()
  .join('-');
console.log(numbers3);

연습

var orders = [
    {
      id:1,
      paid: true,
      amount: 3000,
    },
    {
      id:4,
      paid: false,
      amount: 100,
    },
    {
      id:3,
      paid: true,
      amount: 800,
    },
    {
      id:2,
      paid: false,
      amount: 3000,
    },
    {
      id:5,
      paid: true,
      amount: 2500,
    },
  ];


// 주문을 id 순서로 정렬하여 그 금액만을 나열하기
var amounts = orders
  .sort((a,b) => {
    if (a.id > b.id) return 1;
    return -1;
  })
  .map(order => order.amount);
console.log(amounts);

// 금액이 1000 이상이고 paid가 참인 주문의 금액의 합계
var total = orders
  .filter(order => order.paid && order.amount > 1000)
  .reduce((result, order) => result + order.amount, 0);
console.log(total);

콜백

콜백(Callback)이란 인자로 전달되는 실행 가능한 코드, 즉 함수를 뜻하는 별명입니다. 콜백을 넘겨 받은 함수는 콜백을 바로 실행할 수도 있고, 아니면 필요에 따라 나중에 실행할 수도, 실행하지 않을 수도 있습니다.

콜백 f를 x번 실행하는 함수

function repeatFunc(f, x) {
  for(var i=0; i < x; i++)
    f();
}

// 사용 예시1
function printHelloWorld() {
   console.log("Hello world!");
}
repeatFunc(printHelloWorld, 3);

/*
Hello world!
Hello world!
Hello world!
*/

// 사용 예시2
repeatFunc(function(){
   console.log("Hey Hey!");
}, 2);

/*
Hey Hey!
Hey Hey!
*/

특히 본질적인 의미의 콜백은 인자로 넘어간 함수 중에서도 콜백을 받은 함수가 복잡한, 오래 걸리는 로직을 처리한 뒤에 계산 결과를 콜백에 넘기면서 호출하는 경우를 일컫습니다. 콜백 함수와 오래 걸리는 로직(?)에 대해서는 이후에 다루도록 하겠습니다.

function downloadImage(url, callback){
  var image = ...;
  
  // 다운로드 로직...

  callback(image);
}

downloadImage('http://..', function(image) {
  // 이미지를 가지고 처리하는 로직...
});

콜백이 또 콜백을 받고, 그 콜백이 또 콜백을 받고… 함수가 여러번 중첩되면 가독성이 떨어지는 코드가 됩니다. JavaScript에서는 콜백의 간결한 표현과 제어를 위해서 Promise라는 객체를 제공합니다. Promise는 추후에 다룹니다.

클로저

위의 콜백 fx번 실행하는 함수는 repeatFunc(printHelloWorld, 3)와 같이 실행시 콜백을 3번 반복해서 실행하며 리턴 값은 없습니다. 이때 콜백 fx번 실행하는 것이 아니라, 콜백 fx번 실행하는 함수 자체를 리턴하는 함수를 만들 수 있을까요?

콜백 f를 x번 실행하는 함수를 생성하는 함수

function makeRepeatFunc(f, x) {
  return function() {
    for(var i=0; i < x; i++)
      f();
  };
}

// 사용 예시1
function printHelloWorld() {
   console.log("Hello world!");
}
var printHelloWorld3 = makeRepeatFunc(printHelloWorld, 3);
printHelloWorld3();

/*
Hello world!
Hello world!
Hello world!
*/

var printHelloWorld5 = makeRepeatFunc(printHelloWorld, 5);
printHelloWorld5();

/*
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
*/

위처럼 합성 함수의 개념으로 함수 자체를 만들어내는 함수를 만들 수 있습니다. 이런 함수를 팩토리 함수(Factory Function)라는 별명으로 부르기도 합니다. 또한 이 때 생성된 함수를 클로저(Closure)라고 부릅니다.

클로저의 스코프클로저의 스코프

생성된 함수 printHelloWorld3를 자세히 보면 함수 내에서 makeRepeatFunc의 스코프 내에 있는 인자 fx에 접근하고 있습니다. 이렇게 내부에서 생성된 함수(printHelloWorld3)가 외부 스코프의 변수를 참조하는 경우에는, 외부 함수(makeRepeatFunc)가 종료되어도 외부 스코프의 변수가 해제되지 않습니다. 이렇게 외부의 닫힌 스코프(makeRepeatFunc)의 변수를 참조하는 함수(printHelloWorld3)를 클로저라고 합니다.

여기서의 핵심은 클로저라는 명칭이나 개념보다는, 런타임에서 코드(함수)를 생산해 낼 수 있는 팩토리 함수라는 개념입니다. JavaScript같은 고수준 스크립트 언어에서는 클로저 같은 방식으로 런타임에 동적으로 코드를 생성 할 수 있습니다. 이 개념은 앞으로 반복적으로 다루도록 하겠습니다.

이 강의를 포함한 커리큘럼
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 06월 13일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.