고차 함수(Higher Order Function)에 대하여

taeheeyoon·2022년 6월 10일
15

JavaScript

목록 보기
7/7

시작하며

안녕하세요.
자바스크립트를 학습함에 있어서 고차함수는 프로그래밍의 단계를 한 단계 높일 수 있는 방법 중 하나입니다. 산 정상에서 내려다보면 더 넓은 시야를 가질 수 있습니다. 프로그래밍에서도 마찬가지로 높은 수준에서 생각하면, 높은 수준의 결과물이 나올 수 있습니다. 고차함수를 통해 높은 수준에서 사고하는 방식을 생각해봅시다.

특별한 대우를 받는 함수

비행기에는 First class가 있습니다. 이코노미 클래스와는 탑승수속부터 기내식, 수하물이 나오는 순서까지 항공사와 관련된 모든 부분에서 혜택이 다릅니다. First class 좌석을 구매한 사람은 비싼 가격을 치르고, 항공사로부터 특별한 대우를 받습니다.

자바스크립트에도 특별한 대우를 받는 일급 객체(first-class citizen)가 있습니다. 대표적인 일급 객체 중 하나가 함수입니다.

일급 객체(first-class citizen)의 세 가지 특징

자바스크립트에서 함수는 아래와 같이 특별하게 취급됩니다.

  • 변수에 할당(assignment) 할 수 있다.
  • 다른 함수의 인자(argument)로 전달될 수 있다.
  • 다른 함수의 결과로서 리턴될 수 있다.

함수를 변수에 할당할 수 있기 때문에, 함수를 배열의 요소나 객체의 속성값으로 저장할 수 있습니다. 이는 함수를 데이터(string, number, boolean, array, object)를 다루듯이 다룰 수 있다는 걸 의미합니다.

  1. 변수에 함수를 할당하는 경우
/*
 * 아래는 변수 square에 함수를 할당하는 함수 표현식입니다.
 * 자바스크립트에서 함수는 일급 객체이기 때문에 변수에 저장할 수 있습니다.
 *
 * 함수 표현식은 할당 전에 사용할 수 없습니다.
 * square(7); // --> ReferenceError: Can't find variable: square
 */

const square = function (num) {
  return num * num;
};

// square에는 함수가 저장되어 있으므로 (일급 객체), 함수 호출 연산자 '()'를 사용할 수 있습니다.
output = square(7);
console.log(output); // --> 49

위의 함수 표현식(function expression)은 함수 선언식(function declaration)과 다르게 호이스팅(Hoisting)이 적용되지 않습니다.

  • 호이스팅은 선언된 위치에 관계없이 어디서든 함수를 사용할 수 있도록 합니다.
  • 코드가 실행되는 과정에서 함수 선언부를 코드의 최상단으로 끌어올리는 것처럼 보이게 합니다.

함수 선언식의 호이스팅에 지나치게 의존하면, 코드의 유지 보수가 쉽지 않습니다. 코드 리뷰나 디버깅을 할 때, 코드를 위아래로 왔다 갔다 하게 될 수 있습니다. 함수 선언식은 어느 위치에나 함수를 선언할 수 있고, 함수의 실행 위치도 중요하지 않습니다. 반면에 함수 표현식은 함수의 할당과 실행의 위치에 따라 결과가 달라지기 때문에, 코드의 위치를 어느 정도 예측할 수 있습니다. 호이스팅을 제외하면, 함수 선언식과 함수 표현식은 크게 차이가 없습니다. 다만, 함수 표현식의 경우는 함수가 변수에 저장될 수 있다는 사실을 보다 분명하게 보여 줍니다.

그리고 함수는 변수에 저장된 데이터를 인자로 받거나, 리턴 값으로 사용할 수 있습니다. 함수도 변수에 저장될 수 있기 때문에 함수를 인자로 받거나, 리턴 값으로 사용할 수 있습니다.

고차 함수(higher-order function)란?

고차 함수(higher order function)는 함수의 형태로 리턴할 수 있는 함수입니다. 위에서 확인했듯이, 함수는 변수에 저장할 수 있습니다. 그리고 함수는, 함수를 담은 변수를 인자로 전달받을 수 있습니다. 마찬가지로, 함수 내부에서 변수에 함수를 할당할 수 있습니다. 그리고 함수는 이 변수를 리턴할 수 있습니다. 여기서 변수에 할당하지 않고 함수를 바로 이용할 수 있습니다. 어떤 고차 함수에 함수를 인자로 전달하고, 고차 함수는 함수 자체를 리턴합니다. 변수가 빠졌을 뿐, 동일하게 동작합니다.

이때 다른 함수(caller)의 인자(argument)로 전달되는 함수를 콜백 함수(callback function)라고 합니다. 콜백 함수의 이름은, 어떤 작업이 완료되었을 때 호출하는 경우가 많아서, 답신 전화를 뜻하는 콜백이라는 이름이 붙여졌습니다.

콜백 함수를 전달받은 고차 함수는, 함수 내부에서 이 콜백 함수를 호출(invoke) 할 수 있습니다. caller는 조건에 따라 콜백 함수의 실행 여부를 결정할 수 있습니다. 아예 호출하지 않을 수도 있고, 여러 번 실행할 수도 있습니다. 특정 작업의 완료 후에 호출하는 경우는 이후에 충분히 접할 수 있습니다.

'함수를 리턴하는 함수'는 모양새가 특이한 만큼, 부르는 용어가 따로 있습니다. '함수를 리턴하는 함수'를 고안해 낸 논리학자 하스켈 커리(Haskell Curry)의 이름을 따, 커리 함수라고 합니다. 따로 커리 함수라는 용어를 사용하는 경우에는, 고차 함수란 용어를 '함수를 인자로 받는 함수'에만 한정해 사용하기도 합니다. 그러나 정확하게 구분하자면, 고차 함수가 커리 함수를 포함합니다. 이번 유닛부터는 '함수를 리턴하는 함수'와 '함수를 인자로 받는 함수' 모두, 용어를 고차 함수로 사용합니다.

고차 함수를 자바스크립트로 작성하기

  1. 다른 함수를 인자로 받는 경우
function double(num) {
  return num * 2;
}

function doubleNum(func, num) {
  return func(num);
}

/*
 * 함수 doubleNum은 다른 함수를 인자로 받는 고차 함수입니다.
 * 함수 doubleNum의 첫 번째 인자 func에 함수가 들어올 경우
 * 함수 func는 함수 doubleNum의 콜백 함수입니다.
 * 아래와 같은 경우, 함수 double은 함수 doubleNum의 콜백 함수입니다.
 */
let output = doubleNum(double, 4);
console.log(output); // -> 8
  1. 함수를 리턴하는 경우
function adder(added) {
  return function (num) {
    return num + added;
  };
}

/*
 * 함수 adder는 다른 함수를 리턴하는 고차 함수입니다.
 * adder는 인자 한 개를 입력받아서 함수(익명 함수)를 리턴합니다.
 * 리턴되는 익명 함수는 인자 한 개를 받아서 added와 더한 값을 리턴합니다.
 */

// adder(5)는 함수이므로 함수 호출 연산자 '()'를 사용할 수 있습니다.
let output = adder(5)(3); // -> 8
console.log(output); // -> 8

// adder가 리턴하는 함수를 변수에 저장할 수 있습니다.
// javascript에서 함수는 일급 객체이기 때문입니다.
const add3 = adder(3);
output = add3(2);
console.log(output); // -> 5
  1. 함수를 인자로 받고, 함수를 리턴하는 경우
function double(num) {
  return num * 2;
}

function doubleAdder(added, func) {
  const doubled = func(added);
  return function (num) {
    return num + doubled;
  };
}

/*
 * 함수 doubleAdder는 고차 함수입니다.
 * 함수 doubleAdder의 인자 func는 함수 doubleAdder의 콜백 함수입니다.
 * 함수 double은 함수 doubleAdder의 콜백으로 전달되었습니다.
 */

// doubleAdder(5, double)는 함수이므로 함수 호출 기호 '()'를 사용할 수 있습니다.
doubleAdder(5, double)(3); // -> 13

// doubleAdder가 리턴하는 함수를 변수에 저장할 수 있습니다. (일급 객체)
const addTwice3 = doubleAdder(3, double);
addTwice3(2); // --> 8

내장 고차함수(Built-in higher order functions)

자바스크립트에는 기본적으로 내장된 고차함수가 여럿 있습니다. 그중에서 배열 메소드들 중 일부가 대표적인 고차함수에 해당합니다. 여기에서는 배열 메소드 중 하나인 filter를 대표적으로 설명합니다.

Array.filter()

배열의 filter 메소드는, 모든 배열의 요소 중에서 특정 조건을 만족하는 요소를 걸러내는 메소드입니다. 예를 들어 number 타입을 요소로 갖는 배열에서 짝수만을 걸러내거나, 18 보다 작은 수만을 걸러냅니다. string 타입을 요소로 갖는 배열에서, 길이가 10 이하인 문자열만 걸러내거나, 'korea'만 걸러낼 수도 있습니다.

// 아래 코드에서 '짝수'와 '길이 5 이하'는 문법 오류(syntax error)에 해당합니다.
// 의미만 이해해도 충분합니다.
let arr = [1, 2, 3, 4];
let output = arr.filter(짝수);
console.log(output); // ->> [2, 4]

arr = ['hello', 'code', 'states', 'happy', 'hacking'];
output = arr.filter(길이 5 이하)
console.log(output); // ->> ['hello', 'code', 'happy']

여기서 걸러내는 기준이 되는 특정 조건은 filter 메소드의 인자로 전달됩니다. 이때 전달되는 조건은 함수의 형태입니다. filter 메소드는, 걸러내기 위한 조건을 명시한 함수를 인자로 받기 때문에 고차함수입니다. filter 메소드가 동작하는 방식을 조금 더 자세히 살펴보면 다음과 같습니다.

// 아래 코드는 정확한 표현 방식은 아닙니다.
// 의미만 이해해도 충분합니다.

let arr = [1, 2, 3];
// 배열의 filter 메소드는 함수를 인자로 받는 고차함수입니다.
// arr.filter를 실행하면 내부적으로 arr에 접근할 수 있다고 생각해도 됩니다.
arr.filter = function (arr, func) {
  const newArr = [];
  for (let i = 0; i < arr.length; i++) {
    // filter에 인자로 전달된 콜백 함수는 arr의 각 요소를 전달받아 호출됩니다.
    // 콜백 함수가 true를 리턴하는 경우에만 새로운 배열에 추가됩니다.
    if (func(arr[i]) === true) {
      newArr.push(this[i]);
    }
  }
  // 콜백 함수의 결과가 true인 요소들만 저장된 배열을 리턴합니다.
  return newArr;
};


// 아래 코드가 보다 정확한 정의의 코드 입니다.

 Array.prototype.filter = function(func) {
    const arr = this;
    const newArr = []
    for(let i = 0; i < arr.length; i++) {
      if (func(arr[i]) === true) {
        newArr.push(this[i])
      }
    }
    return newArr;
  }
 

filter 메소드는 배열의 요소를, 인자로 전달되는 콜백 함수에 다시 전달합니다. 콜백 함수는 전달받은 배열의 요소를 받아 함수를 실행하고, 콜백 함수 내부의 조건에 따라 참(true) 또는 거짓(false)을 리턴해야 합니다. 적어도 filter 메소드는 이런 함수를 기대하고 있습니다. 처음 본 코드에 이 점을 반영하여 다시 코드를 작성하면, 다음과 같습니다.

// 함수 표현식
const isEven = function (num) {
  return num % 2 === 0;
};

let arr = [1, 2, 3, 4];
// let output = arr.filter(짝수);
// '짝수'를 판별하는 함수가 조건으로서 filter 메소드의 인자로 전달됩니다.
let output = arr.filter(isEven);
console.log(output); // ->> [2, 4]

const isLteFive = function (str) {
  // Lte = less then equal
  return str.length <= 5;
};

arr = ['hello', 'code', 'states', 'happy', 'hacking'];
// output = arr.filter(길이 5 이하)
// '길이 5 이하'를 판별하는 함수가 조건으로서 filter 메소드의 인자로 전달됩니다.
let output = arr.filter(isLteFive);
console.log(output); // ->> ['hello', 'code', 'happy']

filter는 이렇게 조건에 맞는 데이터만 분류(filtering) 할 때 사용합니다.
filter 이외에도 자주 쓰이는 메소드는 map,reduce등이 있습니다.
MDN Array 인스턴스 메서드

Array.map()

map은 하나의 데이터를 다른 데이터로 맵핑(mapping) 할 때 사용합니다.

const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
]; // 만화책의 모음

const findSubtitle = function (cartoon) {
  return cartoon.subtitle;
}; // 만화책 한 권의 제목을 리턴하는 로직(함수)

const subtitles = cartoons.map(findSubtitle); // 각 책의 부제 모음

Array.reduce()

reduce는 여러 데이터를, 하나의 데이터로 응축(reduce)할 때 사용합니다.

const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
]; // 단행본의 모음

const scoreReducer = function (sum, cartoon) {
  return sum + cartoon.averageScore;
}; // 단행본 한 권의 평점을 누적값에 더한다.

let initialValue = 0 // 숫자의 형태로 평점을 누적한다.
const cartoonsAvgScore = cartoons.reduce(scoreReducer, initialValue) / cartoons.length;
// 모든 책의 평점을 누적한 평균을 구한다.

Array.reduce()의 색다른 사용법

배열을 문자열로

function joinName(resultStr, user) {
  resultStr = resultStr + user.name + ', ';
  return resultStr;
}

let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];

users.reduce(joinName, '');

배열을 객체로

function makeAddressBook(addressBook, user) {
  let firstLetter = user.name[0];

  if(firstLetter in addressBook) {
    addressBook[firstLetter].push(user);
  } else {
    addressBook[firstLetter] = [];
    addressBook[firstLetter].push(user);
  }

  return addressBook;
}

let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];

users.reduce(makeAddressBook, {});

왜 고차함수를 써야할까?

높은 수준에서 생각하기

컴퓨터 공학의 근간을 이루는 개념은 여러 가지가 있지만, 여기에서는 추상화(abstraction)를 설명합니다. 추상화는 복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것이 추상화입니다. 우리가 살아가는 이 세상은, 추상화로 가득 차 있습니다. '-1'을 표현하는 현실의 방법은 존재하지 않습니다. 그러나 우리는 '-1'이라는 문자를 보고, -1은 0보다 1만큼 작은 수라고 설명할 수 있습니다. 이렇듯, 인간은 추상화를 통해 생각하고 표현합니다. 추상화를 이용하면, 효율적이고 편하게 생각할 수 있기 때문입니다.

브라우저 창에 주소를 입력했을 때, 어떤 일이 일어나는지 정확하게 알고 있나요? 입력한 내용을 전파하고, 어디 서버로 갔다가 다른 서버로 가는 등 그런 복잡한 내용을, 일상생활에서는 몰라도 됩니다. 우리는 그저 주소창에 올바른 주소를 입력하면, 브라우저가 해당 사이트를 보여 준다는 것만 알고 있습니다. 스마트폰으로 카카오톡이나 페이스북 메신저를 통해 친구에게 'ㅇㅇ'이란 메세지를 보내면, 그 순간 여러분들의 스마트폰은 기지국과 약 20개의 메세지를 주고받습니다. 하지만 우린 이런 것들을 전부 알지 못하고, 알 필요도 없습니다. 그러나 입력창에 메세지를 입력하고 전송 버튼을 누르면, 내 친구가 메세지를 받는다는 사실은 알고 있습니다. 자동차의 시동 버튼, 자료를 정리하는 엑셀, 지하철/버스를 타기 위한 교통 카드도 추상화의 결과입니다. 일상생활에서 추상화가 아닌 것을 찾아보기 힘들 정도입니다.

자바스크립트를 비롯한 많은 프로그래밍 언어 역시, 추상화의 결과입니다. 컴퓨터를 구성하는 장치(중앙처리장치, CPU; Central Processing Unit)는 0과 1만 이해합니다. 크롬 개발자 도구의 콘솔(console) 탭에서 다음의 코드를 입력했을 때, 어떤 과정을 거쳐 10이 출력되는지 몰라도 10을 출력할 수 있습니다. 그런 복잡한 것들은 크롬의 자바스크립트 해석기(엔진)가 대신해 주기 때문입니다.

//개발자 도구의 콘솔에서 실행하면, 계산 결과를 출력합니다.
function sum(num1, num2) {
  return num1 + num2;
}
const output = sum(3, 7);
console.log(output); // --> 10

컴퓨터의 내부 구조에 대한 고민이 추상화로 해결되었습니다. 우리는 자바스크립의 문법(syntax)을 올바르게 사용하는 것만으로, 다양한 프로그램을 (자바스크립트가 없었을 때) 보다 쉽게 작성할 수 있습니다. 이처럼 고민거리가 줄어들고, 그래서 문제의 해결이 더 쉬워지는 것이 추상화의 이점입니다.

  • 추상화 = 생산성(productivity)의 향상

한편 프로그램을 작성할 때, 자주 반복해서 사용하는 로직은 별도의 함수로 작성하기도 합니다. 이 역시 추상화의 좋은 사례입니다. 추상화의 관점에서 함수를 바라보면, 함수는 사고(thought) 또는 논리(logic)의 묶음입니다.

아래의 getAverage 함수는 number 타입을 요소로 갖는 배열을 입력받아, 모든 요소의 평균값을 리턴합니다. 앞으로는 number 타입을 요소로 갖는 배열을 인자로 전달하기만 하면, 복잡한 로직은 신경 쓰지 않아도 평균값을 얻을 수 있습니다.

//getAverage 함수는 배열을 인자로 받아, 평균값을 리턴합니다.
function getAverage(data) {
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum = sum + data[i];
  }
  return sum / data.length;
}

let output = getAverage([1, 2, 3]);
console.log(output); // --> 2

output = getAverage([4, 2, 3, 6, 5, 4]);
console.log(output); // --> 4

함수를 통해 얻은 추상화를, 한 단계 더 높인 것이 고차 함수입니다. getAverage 함수는 값(배열)을 전달받아, 이 값을 가지고 복잡한 작업을 수행합니다. 이는 값 수준에서의 추상화입니다.

  • 함수 = 값을 전달받아 값을 리턴한다 = 값에 대한 복잡한 로직은 감추어져 있다 = 값 수준에서의 추상화

고차 함수는 이 추상화의 수준을 사고의 추상화 수준으로 끌어올립니다.

  • 값 수준의 추상화: 단순히 값(value)을 전달받아 처리하는 수준
  • 사고의 추상화: 함수(사고의 묶음)를 전달받아 처리하는 수준

다시 말해 고차 함수를 통해, 보다 높은 수준(higher order)에서 생각할 수 있습니다.

  • 고차함수 = 함수를 전달받거나 함수를 리턴한다 = 사고(함수)에 대한 복잡한 로직은 감추어져 있다 = 사고 수준에서의 추상화

추상화의 수준이 높아지는 만큼, 생산성도 비약적으로 상승합니다.

사고 수준의 추상화의 예시

//예시 데이터
const data = [
  {
    gender: 'male',
    age: 24,
  },
  {
    gender: 'male',
    age: 25,
  },
  {
    gender: 'female',
    age: 27,
  },
  {
    gender: 'female',
    age: 22,
  },
  {
    gender: 'male',
    age: 29,
  },
];

위와 같이 주어진 데이터를 순차적으로 처리하려고 할 때, 모든 작업을 하나의 함수로 작성할 수 있습니다. 예를 들어 남성들의 평균 나이를 구한다고 할 때에는, 다음과 같이 함수를 작성할 수 있습니다.

//남성들의 평균 나이를 구하는 하나의 함수 getAverageAgeOfMaleAtOnce
function getAverageAgeOfMaleAtOnce(data) {
  const onlyMales = data.filter(function (d) {
    // data.filter는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
    // 그 결과가 true인 요소만을 갖는 배열을 리턴합니다.
    return d.gender === 'male';
  });

  const numOfMales = onlyMales.length;

  const onlyMaleAges = onlyMales.map(function (d) {
    // onlyMales.map는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
    // 각 결과를 요소로 갖는 배열을 리턴합니다.
    return d.age;
  });

  const sumOfAges = onlyMaleAges.reduce(function (acc, cur) {
    // onlyMaleAges.reduce는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
    // 각 결과를 두 번째 인자로 전달받은 초기값(0)에 누적한 결과를 리턴합니다.
    return acc + cur;
  }, 0);

  return sumOfAges / numOfMales;
}

위에 제시된 getAverageAgeOfMaleAtOnce 함수는 배열 메소드를 적절하게 사용하여 순차적으로 원하는 작업을 수행합니다. 이 코드는 꽤 괜찮은 코드이지만, '남성'의 '평균 나이'만 구하는 작업에만 사용할 수 있습니다. 개선할 점을 찾아보면, 'male'을 매개변수화(parameterization) 하여 조금 더 일반적인(generic) 함수로 변경할 수 있습니다. 이렇게 수정하더라도, 어디까지나 '남성' 또는 '여성'의 '평균 나이'를 구하는 작업만 수행할 수 있습니다.

한편, filter, map, reduce 등의 배열 메소드는 다른 목적을 위해서 사용될 수도 있습니다. 예를 들어 '남성' 중 '최연소 나이'를 구하거나, '여성' 중 '최연소 나이와 최연장 나이의 차이'를 구할 때, 이미 작성된 로직을 그대로 쓸 수 있습니다.

추상화는 고차 함수를 통해, 보다 쉽게 달성할 수 있습니다. 아래의 compose 함수는 입력받은 함수를 순서대로 결합하는 고차 함수입니다. 각각의 작업(filter, map, reduce)은 별도의 함수로 분리되어, compose의 인자로 전달되는 콜백 함수가 됩니다.

//입력된 함수를 순차적으로 실행하는 고차함수 compose
function getOnlyMales(data) {
  return data.filter(function (d) {
    return d.gender === 'male';
  });
}

function getOnlyAges(data) {
  return data.map(function (d) {
    return d.age;
  });
}

function getAverage(data) {
  const sum = data.reduce(function (acc, cur) {
    return acc + cur;
  }, 0);
  return sum / data.length;
}

function compose(...funcArgs) {
  // compose는 여러 개의 함수를 인자로 전달받아 함수를 리턴하는 고차 함수입니다.
  // compose가 리턴하는 함수(익명 함수)는 임의의 타입의 data를 입력받아,
  return function (data) {
    // funcArgs의 요소인 함수들을 차례대로 적용(apply)시킨 결과를 리턴합니다.
    let result = data;
    for (let i = 0; i < funcArgs.length; i++) {
      result = funcArgs[i](result);
    }
    return result;
  };
}

// compose를 통해 함수들이 순서대로 적용된다는 것이 직관적으로 드러납니다.
// 각각의 함수는 다른 목적을 위해 재사용(reuse) 될 수 있습니다.
const getAverageAgeOfMale = compose(
  getOnlyMales, // 배열을 입력받아 배열을 리턴하는 함수
  getOnlyAges, // 배열을 입력받아 배열을 리턴하는 함수
  getAverage // 배열을 입력받아 `number` 타입을 리턴하는 함수
);

const result = getAverageAgeOfMale(data);
console.log(result); // --> 26

이처럼 고차 함수를 통해 사고 수준에서의 추상화를 달성할 수 있습니다. 각각의 작업은 다른 목적을 위해 재사용될 수 있습니다. 여러 함수를 작성하고, 새로운 작업의 조합을 만들어 보세요.

Closure 와 Currying의 차이

추가적으로 고차함수에서 자주 쓰이는 클로저(Closure)와 커링(Currying)은 어떤 차이가 있는지 알아보겠습니다.

Closure

클로저는 내부함수(클로저함수)에서 외부함수의 값을 사용하는 기법,또는 이러한 작동 원리를 일컫는 용어입니다.

function outerFunc() {
  let outerVal = 2;
  function innerFunc() {
    let innerVal = 1;
    return globalVal + outerVal + innnerVal;
  }
  return innerVal;
}
let globalVal = 3;
let innerF = outerFunc();
innerF();

innerFunc()가 바로 클로저 함수입니다. 클로저 함수에서는 지역변수(innerVal), 외부 함수의 변수(outerVal), 전역 변수(globalVal)에 모두 접근할 수 있습니다.

Currying

함수에 n개의 인자를 받는 대신, n개의 클로저 함수를 만들어 각각 인자를 받게 하는 방법입니다.

function multiply(x, y, z){
  return x, y, z;
}

console.log(multiply(3, 5, 7));  // 3*5*7
console.log(multiply(3, 5, 8));  // 3*5*8
console.log(multiply(3, 2, 1));  // 3*2*1

multiply()의 첫번째 인자는 모두 3이며, 3*5*73*5*8은 두번째 인자가 5입니다. 그러나 이 함수에서는 그때그때 모든 인자를 직접 지정해주어야 합니다.

이럴 때 커링을 이용해 특정 인자를 재사용할 수 있게 만들 수 있습니다.

function multiply(x) {
  return function(y) {
    return function(z){
      return x*y*z;
    }
  }
}

let multiply3 = multiply(3);   // 3이 고정됨
let multiply3And5 = multiply3(5);  // 3과 5가 고정됨
let multiply3And2 = multiply3(2);  // 3과 2가 고정됨
console.log(multiply3And5(7));
console.log(multiply3And5(8));
console.log(multiply3And2(1));

클로저 함수의 외부 함수 변수의 값을 고정시킨 함수를 리턴합니다.

함수 표현식

const greeting = (a) => (b) => a + ' ' + b;

커링은 클로저와 완전히 별개인 무언가가 아니라, 클로저의 특징을 이용한 기법으로 볼 수 있습니다. 특히, 클로저의 외부 함수 변수의 값을 고정할 수 있다는 점에서 유용합니다.

마치며

고차함수에 대하여 알아보았습니다. 모던 자바스크립트 문법을 익히면서 필수로 익히게되는 내장 고차함수의 경험이 있으신 분들은 이해하기 쉬웠을 것이라고 생각됩니다. 추가로 선언형 프로그래밍(declarative programming)과 절차형 프로그래밍(imperative programming)에 대하여 알아보시면 좋을 것 같습니다. 간단하게 설명하자면 명령형 프로그래밍은 무엇을 어떻게 할 것인가에 가깝고, 선언형 프로그래밍은 무엇을 할 것인가와 가깝다고 할 수 있습니다.

고차함수를 더욱 잘 활용할 수 있는 함수형 자바스크립트에 관련된 영상으로 글을 마치겠습니다. 감사합니다.

Reference

MDN Array
MDN 클로저
[JavaScript] 클로저(Closure)와 커링(Currying) 간단하게 정리
JavaScript로 함수형 프로그래밍 배우기 - Anjana Vakil - JSUnconf
추상화(abstraction) wikipedia

profile
생각하는 대로 살지 않으면, 사는 대로 생각하게 된다.

1개의 댓글

comment-user-thumbnail
2024년 1월 31일

좋은글 잘읽고 갑니당

답글 달기