Core Javascript - 클로저

김규빈·2021년 2월 24일
0
post-thumbnail

이 글은 Core Javascript를 읽고 내용을 정리하였습니다. 2회독 하였지만 잘못된 내용의 지적을 감사히 받겠습니다.

클로저란?

클로저는 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다. 자바스크립트의 고유의 개념이 아니라서 ECMAScript 명세에서도 클로저의 정의를 다루지 않고 있고, 그것 때문이라고 할 수는 없지만 어쨌든 다양한 문헌에서 제각각 클로저를 다르게 정의 또는 설명하고 있다. 결과적으로 말하면 내가 생각하는 클로저는 다음과 같다.
가비지 컬렉터의 속성(참조하는 변수가 하나라도 있다면 수집대상에서 제외한다)으로 인해 외부 함수에서의 변수가 내부함수로 인해 실행컨텍스트가 종료됬음에도 불구하고 변수가 사라지지 않고 참조할 수 있게 되는것

본질을 깨닫고 나면 의외로 쉬운 개념인데도 어딘가 갈증이 해소되지 않는 기분을 느끼기도 한다.

예시를 통해서 알아보자

let outer = function (){
	let a = 1;
	var inner = function (){
    	console.log(++a)			// 2
    }
    inner();
}
outer()

outer 함수에서 변수 a를 선언하고 outer 내부함수인 inner 함수에서 a의 값을 1만큼 증가시킨 다음 출력한다. inner함수 내부에서는 a를 선언하지 않았기 때문에 실행컨텍스트(environmentReocrd)에서 값을 찾지 못하므로 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 Lexical에 접근해서 a를 찾게 될 것이다(스코프 체인).그렇기 때문에 콘솔에는 2가 찍히게 되는데 의아한 점을 느끼게 된다. outer함수의 실행컨텍스트가 종료되면 Lexical환경에 저장된 식별자들(a, inner)에 대한 참조를 지우게 되는데 그러면 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터에 의해 수집이 되어 변수 a는 사라져야 하는데 어떻게 inner함수에서 변수 a를 참조하게 됬을까? 다시 리마인드 시켜 주겠다. 가비지 컬렉터의 속성(참조하는 변수가 하나라도 있다면 수집대상에서 제외한다)으로 인해 외부 함수에서의 변수가 내부함수로 인해 실행컨텍스트가 종료됬음에도 불구하고 변수가 사라지지 않고 참조할 수 있게 되는것 즉 inner함수에서 콘솔로그을 통해 a를 참조하고 있기 때문에 가비지컬렉팅이 되지 않았던 것. 우리는 항상 클로저의 개념을 사용하고 있었던 것이다. 클로저의 작동을 알게 되면 아마 메모리 누수에 대해 다시 생각하게 되는데..

클로저와 메모리 관리

클로저는 객체지향과 함수형을 모두를 아우르는 매우 중요한 개념이다. 메모리 누수의 위험을 이유로 클로저 사용을 조심해야한다거나 심지어 지양해야 한다고 주장하는 사람들도 있지만 메모리 소모는 클로저의 본질적인 특성일 뿐이다. 오히려 이러한 특성을 정확히 이해하고 잘 활용한다면 오히려 이점을 가질 수 있다. 메모리 누수라는 표현은 개발자가 의도적으로 참조 카운트를 0이 되지 않게 설계한 경우엔 누수라고 표현 할 수 없다고 생각한다. 하지만 이 또한 의도적으로 메모리 할당을 하지 않음으로써 해결 할 수 있다.

let outer = (function (){
	let a = 1;
  	let inner = function(){
    	return ++a;
    }
    return inner;
})();
console.log(outer())
console.log(outer())
outer = null;		// outer 식별자의 inner 함수 참조를 의도적으로 끊음

이런 방식으로 식별자에 참조형이 아닌 기본형 데이터(null, undefined)를 할당 함으로써 메모리 할당을 없애는 방법이다.

커링함수

갑자기 커링함수가 나온 이유는 커링함수가 대표적인 클로저 함수이기 때문이다. 우리가 흔히 사용하는 redux의 미들웨어는 커링함수를 기반으로 만들어 졌다. 커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다. 커링함수의 특징은 한 번에 하나의 인자만 전달하는 것을 원칙으로 하고, 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행 되지 않는다.

let curry3 = function(func){
	return function(a){
    	return function(b){
        	return func(a,b)
        };
    };
};

let getMaxWith10 = curry3(Math.max)(10)
console.log(getMaxWith10(8))	// 10
console.log(getMaxWith10(25))	// 25

let getMinWith10 = curry3(Math.min)(10)
console.log(getMinWith10(8))	// 8
console.log(getMinWith10(25))	// 10

커링 함수는 필요한 상황에 직접 만들어 쓰기 용이하다. 필요한 인자 개수만큼 함수를 만들어 계속 리턴해 주다가 마지막에 조합하여 리턴해주면 끝. 다만 인자가 많아지면 가독성이 떨어진다는 단점이 있는데,

let curry3 = func => a => b => return func(a,b)

이와같이 화살표 함수로 가독성 좋게 표현할 수 있다.

redux 미들웨어 Logger

const logger = store => next => action => {
	console.log('dispatching', action)
  	console.log('next state', store.getState())
  	return next(action)
};

redux 미들웨어 Thunk

const thunk = store => next => action => {
  return typeof action === 'function'
  	? action(dispatch, store.getState)
  	: next(action)
};

뒤 미들웨어는 공통적으로 store, next, action 순서로 인자를 받는다. 이중 store는 프로젝트 내에서 한 번 생성된 이후로는 바뀌지 않는 속성이고 dispatch의 의미를 가지는 next역시 마찬가지지만, action의 경우에는 매번 달라진다. 즉 store와 next값이 결정되면 Redux 내부에서 logger 또는 thunk에 store, next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게 된다는 것!

profile
FrontEnd Developer

0개의 댓글