치춘짱베리굿나이스
클로저 본문
클로저
먼저 렉시컬 스코프와 스코프에 관해 읽고 오자
내가 아는 클로저는 이 캐릭터 뿐이다
자바스크립트에서의 클로저란 무엇일까…
설명
MDN에서의 설명
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
클로저는 함수와 함수가 선언된 어휘적 환경 (Lexical Scope) 의 조합이다.
MDN에 따르면 그렇단다
이게 무슨 말일까?
예시
function foo() {
const str = "Hello World!";
function bar() { // 함수 bar의 선언부
console.log(str);
}
return bar;
}
const closureFunc = foo();
closureFunc(); // bar 실행
이 예시를 보자
bar
는foo
안에 선언되어 있고,foo
는str
이라는 변수를 가지고 있으며,bar
는 그str
을 출력하는 단순한 함수이다- 그리고
foo
는bar
를 반환한다
여기서 우리가 bar
함수를 반환받아 실행시킨다면, str
은 foo
의 스코프 안에 있으니 foo
가 종료된 (값을 반환한) 시점에선 존재하지 않는 변수라고 생각할 수 있다
하지만 그 생각은 매우 틀렸다
우리가 사용하는 언어는 자바스크립트이기 때문이다
자바스크립트는 함수를 반환했을 때, 그 반환한 함수가 클로저를 형성하기 때문에, str
변수는 클로저 안에서 살아 숨쉬게 되는 것이다
클로저가 형성되었다 = 함수와, 함수가 선언된 시점에서의 어휘적 환경 (= 렉시컬 스코프) 이 하나의 환경을 구성하였으므로, 렉시컬 스코프 안에 있는 변수와 상수 모두 참조가 가능하다
다르게 말하면, 함수가 자신이 선언되었을 적의 환경을 기억하고 그 환경에 그대로 접근할 수 있는 것이다!
따라서 클로저란
스코프를 이용하여 함수의 범위를 닫아 (close) , 내부 함수에서 외부 함수의 정보를 참조할 수 있도록 하는 개념이다
즉, 자신이 생성될 때의 환경 (렉시컬 스코프) 을 기억하고 이에 접근할 수 있는 함수가 클로저 함수이다
function foo() {
const str = "Hello World!";
function bar() { // 함수 bar의 선언부
console.log(str);
}
return bar;
}
const closureFunc = foo();
closureFunc(); // bar 실행
다시 예시를 가져왔다
자바스크립트에서 모든 함수는 클로저가 될 수 있으나, 명확하게는 위의 예시에서 반환받은 bar()
가 실질적으로 렉시컬 스코프를 참조하고 있으므로 bar()
를 좁은 의미의 클로저 함수라고 부른다
외부 함수 (foo
) 가 종료되어 실행 컨텍스트가 반환되어도, 실행 컨텍스트의 활성 객체 (변수, 함수 선언 등의 정보가 들어있는 객체) 는 내부 함수 bar가 참조하는 한 그 생명력이 유지되어 내부 함수가 활성 객체 내의 정보들을 참조할 수 있게 된다고 한다
클로저 용례
정보 은닉
function countNumber() {
let n = 0;
function add() {
n++;
console.log(n);
}
return add;
}
const add = countNumber();
add(); // 1
add(); // 2
add(); // 3
add(); // 4
add(); // 5
add()
는 countNumber()
내부에 선언되어 있고, 마찬가지로 countNumber()
내부에 선언된 n
을 사용한다
countNumber
는 add
함수를 반환한 뒤에 컨텍스트가 반환되어 종료되어야 하지만, 활성 객체는 add
에 의해 참조되어 살아있으므로 n
또한 접근 가능한 변수가 된다
- 첫 번째
add
의 시점에서n
은 0이고 이에 1을 더했으므로 n은 1으로 출력된다 - 두 번째
add
의 시점에서n
은 1이고 이에 2을 더했으므로 n은 2로 출력된다 - 세 번째
add
의 시점에서n
은 2고 이에 1을 더했으므로 n은 3으로 출력된다 - 네 번째
add
의 시점에서n
은 3이고 이에 1을 더했으므로 n은 4로 출력된다 - 다섯 번째
add
의 시점에서n
은 4고 이에 1을 더했으므로 n은 5로 출력된다
add
함수는 자신이 선언된 렉시컬 환경 countNumber
을 기억하므로 n
변수에 접근 가능하고, 그 외의 외부 변수나 함수들은 자신과 관련된 환경이 아닌 countNumber
스코프를 인식할 수 없다
이처럼 무언가 변수 등을 숨기되 상태값으로 계속 사용하고 싶을 경우 클로저를 활용하며, 이를 이용하여 클래스의 private 또한 흉내낼 수 있다
useState
function useState(initValue) {
let _val = initValue;
const state = () => _val;
const setState = (newVal) => {
_val = newVal;
};
return [state, setState];
}
const [state, setState] = useState("hello");
console.log(state());
setState("world");
console.log(state());
리액트에서 엄청 많이 사용되는 useState가 클로저로 구현된 대표적인 사례이다
실제로는 훨씬 복잡하게 구현되어 있지만, 간략하게는 state
와 setState
가 위와 같은 클로저 형태로 구성되어 있다
- 위의 예시에서
state
도 함수 형태로 구현되어 있는 이유는 함수와 달리 변수로는 클로저의 특성을 살리지 못하기 때문에setState
를 통해 상태값을 바꿔주어도 이전 값이 그대로 남아있기 때문이다 - 따라서 getter 함수 형태로 구현하였다
state
, setState
가 클로저로 구현되어 있음으로써 _val
이라는 렉시컬 스코프의 값을 참조할 수 있게 되고, 따라서 상태를 유지했다가 호출해서 사용하는 것이 가능하다
커링
function multiply(x, y, z) {
return x * y * z;
}
function multiplyX(x) {
return function multiplyY(y) {
return function multiplyZ(z) {
return x * y * z;
}
}
}
multiplyX(3)(6)(9);
function multiply = x => y => z => x * y * z; // 위와 같으면서 더욱 간결함
multiply(1)(2)(3);
커링은 클로저를 이용한 예시 중 하나로, 이전 컨텍스트를 기억하는 함수" 여러개를 반환함으로서 여러 인자를 받는 함수를 단일 인자를 받는 함수 여러 개로 쪼개는 함수형 프로그래밍 기법이다
커링을 이용하면 위의 multiply
함수를 multiplyX
, multiplyY
, multiplyZ
함수로 쪼갤 수 있으며,
multiplyY
는multiplyX
의 실행 컨텍스트를 기억하므로, 매개변수x
를 가져다 사용할 수 있다multiplyZ
는multiplyY
의 실행 컨텍스트를 기억하므로, 매개변수x
,y
를 가져다 사용할 수 있다
function multiplyX(x) {
return function multiplyY(y) {
return x * y;
}
}
const multiplyBy3 = multiplyX(3);
multiplyBy3(6); // 18
multiplyBy3(9); // 27
커링을 사용하면 동일한 입력에 대하여 동일한 출력이 나오게끔 할 수 있고, 재사용성이 높아지므로 생산성과 유지보수 면에서 탁월하다고 한다
위의 예시에서 우리는 스코프에 3을 저장해둔 채로 multiplyBy3
함수를 재활용할 수 있는 것을 볼 수 있다
참고 자료
https://poiemaweb.com/js-closure
https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
'Javascript + Typescript > 이론과 문법' 카테고리의 다른 글
2차원 배열에서 값 단 하나만 바꾸고 싶은데 모든 줄이 다 바뀌는 경우 (0) | 2023.08.08 |
---|---|
this in JavaScript (0) | 2023.07.26 |
UnhandledPromiseRejection (0) | 2023.07.17 |
[Typescript] 인덱스 시그니쳐 (0) | 2023.07.14 |
이벤트 버블링과 캡쳐링 (0) | 2023.07.13 |