Javascript 에 대한 깊은 이해를 하기 위해서는 클로저 (Closure)에 대해 알아야 되며, 이를 알기 위해서는 스코프(Scope) 에 대한 이해가 필요하다.
유효범위(스코프)는 자바스크립트 뿐만 아니라 모든 프로그래밍 언어에서 가장 기본적인 개념의 하나로 반드시 알고 넘어가야 한다. 하지만 자바스크립트의 유효범위는 다른 언어의 유효범위와 다른다.
프로그래밍 언어에서 유효범위는 어느 범위까지 참조하는 지를 뜻한다.
유효범위의 특징은 다음과 같다.
- 함수 단위의 유효범위 (function-level-scope)
- 변수명 중복 허용
- 암묵적 선언 (implied globals)
- Lexical scoping (Static scoping)
function-level scope 란 함수 코드 블럭 내에서 선언된 변수는 함수 코드 블럭 내에서만 유효하고, 함수 외부에서는 유효하지 않다는 것이다.
var x = 0;
{
var x = 1;
console.log(x); // 1
}
console.log(x); // 1
let y = 0;
{
let y = 1;
console.log(y); // 1
}
console.log(y); // 0
클로벌 영역에 변수를 선언하면, 이 변수는 어느 곳에서든지 참조할 수 있는 global scope 를 갖는 전역 변수가 된다.
var global = 'global';
function foo() {
var local = 'local';
console.log(global); // global
console.log(local); // local
}
foo();
console.log(global); // global
console.log(local); // Uncaught ReferenceError: local is not defined
명시적으로 변수 앞에 `var
을 붙여주지 않으면 암묵적 전역변수가 된다.
function foo() {
x = 1; // Throws a ReferenceError in "use strict" mode
var y = 2;
}
foo();
console.log(x); // 1
console.log(y); // ReferenceError: y is not defined
⭐️ 자바스크립트는 함수가 선언된 시점에서의 유효범위를 갖는다.
var number = 1234
function printNumber() {
console.log(number);
}
function wrapper() {
number = 4321
printNumber();
}
wrapper(); // 4321
printNumber 함수를 실행하기 직전에 number 의 값을 변경해주었으므로 wrapper 함수를 실행시켰을 때에 값이 변경되었다.
var number = 1234
function printNumber() {
console.log(number);
}
function wrapper() {
var number = 4321
printNumber();
}
wrapper(); //1234
wrapper 함수 안에서 var 키워드를 사용하자 결과가 달라진다.
함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 스코프로부터 가장 가까운 곳(상위 범위에서)있는 변수를 계속 참조하게 된다.
위에 예시에서는 printNumber
함수 안의 number
변수는 선언 시 가장 가까운 전역변수 number
를 참조하게 된다.
새롭게 정의된 스코프는 상위의 스코프에 접근할 수 있다.
스코프 체인은 scope 의 가장 내부에서 scope chain 을 따라 바깥쪽으로 검색을 하게 된다.
클로저는 함수가 선언된 환경의 (렉시컬) 스코프를 기억하여, 함수가 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기술이다.
// changeCount는 inner함수
// 객체를 리턴하고 있고 객체 안에는 increase, decrease, show와 같은 inner함수들을 저장
const counter = function() {
let count = 0;
function changeCount(number) {
count += number;
}
return {
increase: function() {
changeCount(1);
},
decrease: function() {
changeCount(-1);
},
show: function() {
alert(count);
}
}
};
// counter를 실행하면 outer함수 스코프를 기억하고 있는 클로저들이 담긴 객체를 반환
// counterClosure는 counter함수 내부에 정의된 count나 changeCount에 접근 가능
const counterClosure = counter();
counterClosure.increase(); //
counterClosure.show(); // 1
counterClosure.decrease();
counterClosure.show(); // 0
함수형 컴포넌트에서 이전 상태와 현 상태의 변경이 있는지를 감지하기 위해서는 함수가 실행됐을 때 이전 상태에 대한 정보를 가지고 있어야 한다. React 는 이 과정에서 클로저를 사용한다.
useState 는 initialState 를 인자로 받는 함수로 선언되어 있다. 함수를 보면 resolveDispatcher 라는 또다른 함수를 통해 리턴된 dispatcher 의 useState 메소드에 initialState 를 전달한 결과를 리턴하고 있다.
resolveDispatcher 함수로 타고 들어가보면 다시 ReactCurrentDispatcher 안의 current 값을 리턴하고 있다.
ReactCurrentDispatcher 까지 들어가본다.
전역에 선언된 current 라는 값을 담은 변수이다.
useState는 외부에 선언된 상태값에 접근해서 이전 상태를 가져오고 변경된 상태값을 관리하고 있다. 함수형 컴포넌트도 결국 함수이기 때문에, 클로저를 통해 선언되는 시점에 접근 가능했던 외부 상태값에 계속 접근할 수 있는 것이다. 함수형 컴포넌트에서 상태값을 변경하면 외부의 값이 변경되고, 리렌더링 (=함수 재호출)을 통해 새로운 값을 받아오게 된다.
클로저는 내부함수가, 외부함수의 지역변수에 접근할 수 있고, 외부 함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근할 수 있는 것이다.
(클로저는 함수가 선언된 환경(렉시컬) 스코프를 기억해 함수가 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 된다.)
React 에서 함수형 컴포넌트의 상태관리를 위해서는 컴포넌트 외부에 저장된 값을 사용하며 클로저를 통해 해당 값에 접근해 상태를 비교하고 변경한다. useState 는 컴포넌트 내부에서 값을 변경시키는 것이 아니라, 외부에 있는 값을 변경시키기 때문에 상태가 변경된 직후 컴포넌트가 가진 값은 이전의 값을 그대로 참조한다.
각 컴포넌트의 상태 정보는 배열 형태로 저장되기 때문에 상태를 변화시키는 hook 을 조건문이나 반복문 안에서 사용하면 잘못된 순서의 값을 참조하게 될 수 있다.