티스토리 뷰
클로저(Closure)?
클로저는 함수가 생성되는 시점에 접근 가능했던 변수들을 생성 이후에도 계속해서 접근할 수 있게 해주는 기능입니다. 일반적으로 함수 내부에서 정의된 지역 변수는 그 함수가 처리되는 동안에만 존재합니다.
하지만 Javascript는 함수를 리턴시킬 수 있으므로 이를 잘 응용하면 계속해서 지역 변수에 접근 가능하게 할 수 있는 것입니다. 이것이 가능한 이유는 리턴하는 함수가 클로저를 형성하기 때문인데. 이때 접근 가능한 변수 환경은 클로저가 형성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성됩니다.
다음 예를 살펴보겠습니다
function sayHello(name) {
const saying = `${name} 안녕!`;
return function() {
return saying;
}
}
const sayHelloToMike = sayHello("Mike");
console.log(sayHelloToMike()); // Mike 안녕!
sayHello() 함수는 name을 인자로 받아 함수 내의 지역변수 saying에 할당하고, 다시 saying을 리턴하는 함수를 리턴합니다. sayHello("Mike") 함수의 실행이 끝나면 saying 변수에 더이상 접근할 수 없게 되는 것이 일반적이지만, 리턴값을 전달받은 sayHelloToMike() 함수를 호출해보면 saying 변수에 접근하는게 보입니다.
function changeNumber() {
let privateNumber = 0;
function changedBy(value) {
privateNumber += value;
}
return {
increment: () => {
changedBy(1);
},
decrement: () => {
changedBy(-1);
},
value: () => {
return privateNumber;
}
}
}
changeNumber() 함수 내부의 privateNumber 변수와 changedBy() 함수는 함수가 실행되었을 때 생성되었다가 종료되면서 사라지게 됩니다. 따라서 외부에서 함수 내 지역 변수인 privateNumber와 changedBy() 함수에 직접적으로 접근할 수 없게 됩니다.
하지만 함수가 리턴한 객체 내의 메서드를 통해 간접적으로 접근할 수 있는데, 이를 이용해서 자바와 같은 몇몇 언어들에 있는 프라이빗 메서드를 흉내낼 수도 있습니다.
클로저는 독립적인 공간을 가진다
let data1 = changeNumber();
let data2 = changeNumber();
console.log(data1.value()); // 0
console.log(data2.value()); // 0
data1.increment();
console.log(data1.value()); // 1
console.log(data2.value()); // 0
data2.decrement();
console.log(data1.value()); // 1
console.log(data2.value()); // -1
//
makeData = null;
data1 = null;
data2 = null;
changeNumber() 함수로부터 생성된 data1과 data2 객체는 각자의 독립성을 갖습니다. data1 클로저 환경에서의 privateNumber와 data2 클로저 환경의 privateNumber는 각각 고유의 클로저를 통한 privateCounter 변수의 다른 버전을 참조하기 때문입니다. 따라서 하나의 클로저에서 변수 값을 변경해도 다른 클로저의 값에는 영향을 주지 않는 것입니다.
사용이 끝난 함수는 null을 대입해 해제해주면 좋습니다.
Currying?
커링은 함수를 반환하는 함수라고 볼 수 있습니다.
함수의 인자를 다시 구성하여 필요한 함수를 만드는 패턴인 것이지요.
커링을 사용해야 하는 이유는 무엇일까요? 그것은 바로 "함수의 재활용"때문입니다.
우선 커링을 알아보기 전에 함수의 재활용이 무엇인지 다음 예제를 통해 한번 알아보도록 하겠습니다.
function multiply(a, b) {
return a * b;
}
function multiplyX(x) {
return function (a) {
return multiply(a, x);
}
}
const multiplyTwo = multiplyX(2);
const multiplyThree = multiplyX(3);
console.log(multiplyTwo(3)) // 3 * 2 = 6
console.log(multiplyThree(4)) // 4 * 3 = 12
multiply() 함수를 재활용하여 전해진 인자 X를 곱해주는 함수 multiplyX() 함수를 만들었습니다.
약간 의문이 듭니다. 굳이 커링을 이용하는 이유는 무엇일까요? 커링을 이용하지 않고 multiply(a, b) 함수에 원하는 인자를 전달해도 같은 결과가 나올텐데 말입니다.
그 이유는 바로 인자를 나누어서 전달할 수 있기 때문인데요.
다음 실습을 통해 이 특징의 장점에 대해 한번 더 생각해보겠습니다.
const equation = (a, b, c) => x => ((x * a) * b) + c;
const formula = equation(2, 3, 4);
const result = formula(10);
console.log(result) // ((10 * 2) * 3) + 4 = 64
equation() 함수는 커링을 사용하여 a, b, c에 해당하는 값을 인자로 받아 x => ((x * a) * b) + c라는 함수를 반환합니다. 그 다음 이것을 변수 formula에 대입합니다.
이와 같이 인자를 나중에 받아 실행할 함수를 생성해주는 equation()과 같은 함수를 "커링 함수"라고 부릅니다.
이것 또한 equation() 함수가 클로저를 형성하기 때문에 가능한 것입니다. equation() 함수를 리턴할 당시 환경에 속해있던 변수 a, b, c에 접근할 수 있도록 말입니다.
커링을 응용한 함수 조합 기법
커링 개념은 하이어오더 컴포넌트의 개념에서 사용되므로 React에서도 중요하다고 볼 수 있습니다. 또한 커링을 잘 이용하면 +, *와 같은 연산자를 최소화하여 함수들의 조합만으로 이를 구현할 수 있습니다.
const multiply = (a, b) => a * b;
const add = (a, b) => a + b;
const multiplyX = x => a => multiply(a, x);
const addX = x => a => add(a, x);
// 커링 함수 multiplyX(), addX()는 다음과 같이 응용될 수 있습니다.
const addFour = addX(4);
const multiplyTwo = multiplyX(2);
const multiplyThree = multiplyX(3);
const formula = x => addFour(multiplyThree(multiplyTwo(x)));
// formula(x) == (x * 2) * 3) + 4
위처럼 커링은 여러 번 겹쳐서 재사용할 수 있습니다. 함수 formula()는 위에서 구현했던 equation() 함수를 커링 함수들의 조합으로 구현한 것입니다.
하지만 여기에는 한가지 단점이 있습니다. 함수가 실행되는 순서는 안쪽 multiplyTwo()부터 마지막 addFour까지 오른쪽에서 왼쪽 방향으로 실행되므로, 코드 가독성이 떨어지고 깊어질 수록 점점 이해하기 어려워진다는 것입니다.
그렇다면 커링 함수를 우리가 알아보기 쉽도록 왼쪽부터 오른쪽으로 실행되도록 조합해주는 함수를 만들어보면 어떨까요?
배열 API 중 다음과 같이 reduce()를 사용하면 함수를 조합해주는 함수를 만들 수 있습니다.
// 구조
funcArray.reduce(<func>, function(k) {return k});
앞서 만들어본 formula() 함수를 reduce를 이용해 재조합 해보겠습니다.
prevFunc(value)를 nextFunc() 함수의 인자로 넘겨 조합하는 점에 주목하여 살펴보시길 바랍니다.
[multiplyTwo, multiplyThree, addFour].reduce(
function (prevFunc, nextFunc) {
return function(value) {
return nextFunc(prevFunc(value));
}
},
function(k) {return k;}
)
[1단계: 초깃값과 multiplyTwo() 함수의 조합]
function(value) {
return multiplyTwo((k => k)(value));
}
prevFunc는 function(k) {return k;}이고 nextFunc는 multiplyTwo입니다. 첫 조합 연산의 결과로 위와 같은 함수를 얻게 됩니다.
[2단계: 1단계 결괏값과 multiplyThree() 함수의 조합]
function(value) {
return multiplyThree(
function(value) {
return multiplyTwo(
(k => k)(value)
);
}(value)
)
}
prevFunc는 1단계에서 얻은 함수이고 nextFunc는 multiplyThree입니다. 두 조합 연산 결과로 위와 같은 함수를 얻게 됩니다.
[3단계: 2단계 결괏값과 addFour() 함수의 조합]
function(value) {
return addFour(
function(value) {
return multiplyThree(
function(value) {
return multiplyTwo(
(k => k)(value)
);
}(value)
);
}(value)
);
}
3번째 조합 연산 결과로 위와 같은 함수를 얻게 됩니다. 최종 변환된 함수의 구조가 복잡하지만 함수를 실행해보면 정상적으로 ((x * 2) * 3) + 4를 계산하는 것을 알 수 있습니다.
이제 배열 안에 지정된 함수만 조합하는 코드를 임의의 함수로 조합할 수 있도록 바꿔보겠습니다.
function compose(...funcArr) {
return funcArr.reduce(
function (prevFunc, nextFunc) {
return function(...args) {
return nextFunc(prevFunc(...args));
}
},
function(k) {return k;}
)
}
const formulaWithCompose = compose(
multiplyTwo,
multiplyThree,
addFour
);
console.log(formulaWithCompose(10)); // 64
이제 compose() 함수를 이용하여 formulaWithCompose() 함수와 같이 왼쪽에서 오른쪽 (혹은 위에서 아래) 방식으로 조합할 수 있게 되었습니다.
참조: MDN
'JS 기본' 카테고리의 다른 글
#3. XMLHttpRequest 객체를 활용한 Ajax (0) | 2021.12.22 |
---|---|
#1. HTTP, Cookie, Web Storage (0) | 2021.12.22 |
jQuery Ajax (0) | 2021.12.21 |
크로스 도메인 (0) | 2021.12.21 |