목차
Scope: var, let, const
변수를 선언할 때 스코프(scope)는 이름을 어디서 읽을 수 있는지를 정한다. var는 함수 범위, let과 const는 블록 범위를 갖는다. var를 쓰면 if나 for의 중괄호를 벗어나도 같은 함수 안에서 이름이 보일 수 있다. 이 글의 예제는 코드 조각을 각각 독립적으로 실행한다고 가정한다.
function compareScope() {
if (true) {
var fromVar = "보임";
let fromLet = "블록 안";
const fromConst = "블록 안";
}
console.log(fromVar); // 보임
// console.log(fromLet); // ReferenceError
// console.log(fromConst); // ReferenceError
}
compareScope();
var가 함수 밖에서 선언되면 실행 환경에 따라 전역 이름을 만들 수 있다. 특히 오래된 브라우저 스크립트의 전역 var는 전역 객체와 연결되지만, 모듈 안의 최상위 선언은 모듈 범위에 머문다. 그래서 var를 무조건 “항상 전역”이라고 설명하면 틀리다. 이름을 선언하지 않고 대입하는 암묵적 전역 문제도 엄격 모드와 모듈에서는 오류가 난다.
초기화 전 접근과 TDZ
console.log(oldValue); // undefined
var oldValue = 1;
{
// console.log(newValue); // ReferenceError
let newValue = 2;
console.log(newValue); // 2
}
var 선언은 스코프 시작에 등록되고 undefined로 초기화된다. let·const도 스코프에 등록되지만 선언문에 도달하기 전에는 일시적 사각지대(TDZ)라 읽을 수 없다. 코드가 실제로 위로 이동하는 것은 아니다. 변수 선언을 사용 지점 가까이에 두면 이 차이로 인한 실수를 줄인다.
const는 참조 재할당을 막는다
const user = { name: "Kim" };
user.name = "Lee"; // 객체 내용 수정은 가능
// user = { name: "Park" }; // TypeError: 재할당 불가
let attempts = 0;
attempts += 1; // 재할당 가능
const는 선언과 동시에 초기값을 줘야 한다. 값이 객체면 객체 자체가 깊게 불변이 되는 것은 아니다. 변경 불가능한 API가 필요하면 새 객체를 만들어 반환하거나 객체의 노출 범위를 좁힌다. Object.freeze도 기본적으로 얕은 동결이므로 중첩 객체에는 별도 설계가 필요하다.
반복문과 클로저
const callbacks = [];
for (let index = 0; index < 3; index++) {
callbacks.push(() => index);
}
console.log(callbacks.map(fn => fn())); // [0, 1, 2]
for의 let은 반복마다 별도 바인딩을 제공하므로 콜백이 각각의 index를 기억한다. 같은 자리에 var index를 쓰면 함수 범위의 바인딩 하나를 공유해 루프가 끝난 뒤 모두 3을 볼 수 있다. 원문의 for 예제는 변수 선언이 유효하더라도 plus1의 restul/result 오타와 여러 파일에 나누어야 할 export/import를 한 코드 블록에 섞어 결과를 확인하기 어려웠다. 모듈은 별도 파일의 내보내기와 가져오기로 분리해 다룬다.
일반적으로 기본은 const, 실제로 재할당이 필요하면 let을 선택한다. var가 반드시 버그라는 뜻은 아니지만 블록 스코프와 TDZ가 주는 지역적 계약이 현재 코드의 의도를 더 선명하게 한다. 변수 스코프와 객체의 변경 가능성은 별도 문제로 평가해야 한다.