Front End/JavaScript

클로저

클로저

JavaScript에서는 다른 컴퓨터 언어와 조금 다른 특성이 몇 가지 있는데, 그 중 종종 사용되는 것이 클로저라는 개념이다. JavaScript는 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경, 즉 어휘적 환경을 기준으로 변수를 조회하려 한다. 이와 같은 이유로 “외부 함수의 변수에 접근할 수 있는 내부 함수”를 클로저 함수라 일컫는다.

첫 번째 특징 : 함수를 리턴하는 함수

// 화살표 함수를 이용한 간단한 덧셈 함수
const add = (x, y) => x + y;
add(5, 7); // 12

// 함수의 호출이 두 번 발생하는 화살표 함수
const adder = x => y => x + y;
adder(5)(7); // 12

// 리턴 값이 함수의 형태
typeof adder(5); // 'function'
adder(5) // y => x(5) + y

위의 예시는 결과값은 같지만 아래의 함수는 화살표를 두 번 사용함으로써 함수의 호출(invocation)이 두 번 발생하게 된다. 이 말은 곧 adder(5) 의 리턴 값이 함수의 형태임을 의미한다. 이 화살표 함수를 function 함수 선언식으로 표현하면 다음과 같다.

const adder = function(x){
	return function(y){
		return x + y;
	}
}

클로저 함수는 위와 비슷한 모양을 가지고 있으며 이를 통해 “함수를 리턴하는 함수” 라는 첫 번째 특징을 알 수 있다.

두 번째 특징 : 리턴하는 함수에 의해 스코프 구분

클로저의 핵심은 스코프를 이용해서 변수의 접근 범위를 닫는 것(closure, 폐쇄)하는 데에 있다. 따라서 함수를 리턴 하는 것 만큼 변수가 선언된 곳이 중요하다.

// 외부 함수 시작
const adder = function(x){ // 외부 함수의 변수 x
	// 내부 함수 시작
	return function(y){ // 내부 함수의 변수 y
		return x + y;
	}
	// 내부 함수 끝
}
// 외부 함수 끝

위의 주석처럼 이 클로저 함수는 외부 함수와 내부 함수에 따라 스코프가 분리되어 있다. 이 스코프에 따르면 외부 함수에서는 내부 함수의 변수 y에 접근 할 수 없지만 내부 함수에서는 외부 함수의 변수 x에 접근 할 수 있다. 이를 통해 스코프 함수는 “내부 함수는 외부 함수에 선언된 변수에 접근이 가능하다” 는 두 번째 특징을 할 수 있다.

클로저의 활용

데이터를 보존하는 함수

일반적으로 함수는 함수 실행이 끝나면 함수 내부의 변수를 사용 할 수 없다. 하지만 클로저 함수는 외부 함수의 실행이 끝나도 외부 함수 내 변수가 메모리 상에 저장된다.

const adder = function(x){
	return function(y){
		return x + y;
	}
}

const add5 = adder(5);
add5(7); // 12
add5(10); // 15

변수 add5에는 클로저를 통해 리턴한 함수가 담겨있고, 외부의 실행이 끝났음에도 adder 함수에서 인자로 5라는 값을 x 변수에 담은채로 남아있다. 이를 이용해 add5 함수를 이용하여 위와 같이 실행이 가능하다.

const tagMaker = tag => content => `<${tag}>${content}</${tag}>`

const divMaker = tagMaker('div');
divMaker('hello'); // '<div>hello</div>'
divMaker('ninefloor'); // '<div>ninefloor</div>'

const anchorMaker = tagMaker('a');
anchorMaker('go'); // '<a>go</a>'
anchorMaker('main'); // '<a>main</a>'

보다 실용적으로 사용한 예제이다. divmaker 함수는 ‘div’ 라는 문자열을 tag 라는 변수에 담아두고 있으며, anchorMaker 함수는 ‘a’라는 문자열을 tag에 담아두고 있다. 이처럼 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게 해준다.

정보의 접근 제한 (캡슐화)

다음은 ‘클로저 모듈 패턴’ 이라 불리는 유용한 패턴이다.

const makeCounter = () => {
	let value = 0;

	return { // 객체를 리턴하게 되며 key는 method가 된다
		increase: () => { // increase를 key로 가진 함수
			value = value + 1;
		}, 
		decrease: () => { // decrease를 key로 가진 함수
			value = value - 1;
		}, 
		getValue: () => value // getValue를 key로 가진 함수
	}
}

const counter1 = makeCounter();
counter1; // { increase: f, decrease: f, getValue: f }
counter1.increase(); // value++ 실행
counter1.getValue(); // 1

클로저를 이용해 내부 함수를 단 하나만 리턴하는 것이 아닌 객체에 담아 여러 개의 내부 함수를 리턴하도록 되어있다. 따라서 counter1 변수에 makeCounter 함수를 실행해 담으면 increase, decrease, getValue메서드를 포함한 객체가 된다.

이 때, 함수 외부에서 value 변수에 새로운 값을 재할당 할 수 없다. 내부 스코프 밖에서는 변수에 접근 할 수 없기 때문이다. 대신 리턴하는 객체가 제공하는 메서드를 통해 value 값을 간접적으로 컨트롤 할 수 있다. 이를 정보의 접근 제한(캡슐화)라 한다.

왜 캡슐화를 하는가?

만약 스코프로 value 값을 감싸지 않았다면 value는 전역 변수여야만 했다. 하지만 makeCounter 라는 함수가 value 값을 임의로 재할당 할 수 없도록 보존하고 있기 때문에 전역 변수로 따로 만들 필요가 없다.

전역 변수가 좋지 않은 이유는 다른 함수나 로직에 의해 의도치 않은 변경을 초래하는 side effect를 발생시키기 때문이다. 따라서 클로저를 이용해 불필요한 전역 변수 사용을 줄이고 스코프를 이용해 값을 안전하게 다룰 수 있다.

모듈화

앞선 예제는 단 한 개의 카운터만 만들 수 있는 것이 아니다. 위의 코드를 그대로 활용하여 counter1counter2를 만들고 각기 다르게 실행한다.

const counter1 = makeCounter();
counter1.increase(); // value++ 실행
counter1.increase(); // value++ 실행
counter1.decrease(); // value-- 실행
counter1.getValue(); // 1

const counter2 = makeCounter();
counter2.decrease(); // value-- 실행
counter2.decrease(); // value-- 실행
counter2.decrease(); // value-- 실행
counter2.getValue(); // -3

makeCounter에 의해 리턴된 객체는 makeCounter를 실행할 때에 선언되는 value 값을 각각 독립적으로 가지게 된다. 따라서 counter1valuecounter2value는 서로에게 영향을 끼치지 않고 각자의 값을 보존 할 수 있다. 이와 같이 함수 재사용성을 극대화해서 함수 하나를 완전히 독립적인 부품 형태로 분리하는 것을 모듈화라 한다. 클로저를 통해 데이터와 메서드를 같이 묶어서 다룰 수 있기 때문에 클로저는 모듈화에 유리하다.


Uploaded by N2T