[Codeit Sprint]Weekly Paper 3 -1 : var, let, const 뭐가 다를까?

DreamPaste·2025년 4월 29일

Weekly Paper

목록 보기
7/8
post-thumbnail

var, let, const

Javascript에는 변수 선언 방식으로 var, let, const 방식이 있습니다.
ES6에서 let과 const가 도입되기 전에는 모든 변수 선언에 var만 사용했지만, 현재는 세 가지 키워드를 상황에 맞게 사용합니다.
각 선언 키워드는 호이스팅(hoisting), 일시적 데드 존(TDZ), 스코프(scope), 재할당, 불변성(immutability) 등의 차이가 존재하고 각 관점에서 살펴보겠습니다.

호이스팅(Hoisting)

호이스팅(hoisting) 이란 자바스크립트 엔진이 변수 선언(및 함수 선언)을 스코프의 최상단으로 끌어올려서 해석하는 동작을 말합니다. 이로 인해 코드에서 변수 선언이 아래에 나와 있어도 위에서 해당 변수를 참조할 수 있는 것처럼 보입니다.
그러나 var, let, const는 호이스팅되더라도 동작이 서로 다릅니다.

#️⃣ var의 호이스팅:

var로 선언된 변수는 선언 단계와 초기화 단계가 함께 호이스팅됩니다. 스코프의 시작 시점에 var 변수는 자동으로 undefined로 초기화되므로, 선언 이전에 접근해도 에러가 나지 않고 undefined 값을 반환합니다.

console.log(x);   // undefined (호이스팅으로 초기화됨)
var x = 5;
console.log(x);   // 5

위 코드에서 console.log전에 x를 선언하지 않았지만, var x 선언 키워드로 인해 x가 호이스팅되어 기본값 undefined로 초기화된걸 확인할 수 있습니다.

#️⃣ let과 const의 호이스팅:

let과 const도 사실 호이스팅되지만, 초기화 단계가 보류됩니다. 즉, 스코프의 시작 시점에 메모리 공간은 만들되 초기화는 실제 선언문에 도달했을 때 수행됩니다. 그 전에 해당 변수에 접근하면 일시적 데드 존(TDZ) 에 걸려 ReferenceError가 발생합니다.

// console.log(y); // ReferenceError (TDZ 때문에 아직 접근 불가)
let y = 10;
console.log(y);    // 10

// console.log(z); // ReferenceError (TDZ)
const z = 20;
console.log(z);    // 20

이처럼 var와 달리 let/const는 호이스팅되어도 선언 이전에 사용을 막음으로써 의도치 않은 undefined 참조를 방지해줍니다.

typeof 연산자로도 TDZ 상태의 변수를 참조하면 에러가 발생합니다.
예를 들어 typeof undeclaredVar"undefined"를 반환하지만, TDZ에 있는 typeof i는 ReferenceError를 throw합니다, 이는 let/const 변수의 선언이 이미 존재한다고 엔진이 알고 있기 때문에, 안전장치로 초기화 전 접근을 막기 때문입니다.

일시적 데드 존(Temporal Dead Zone, TDZ)

일시적 데드 존(TDZ) 은 let과 const (또는 class) 변수가 스코프에 진입한 시점부터 실제 선언 위치까지의 구간을 가리킵니다. 이 구간 동안에는 해당 변수에 접근할 수 없습니다. 쉽게 말해, 변수가 물리적으로 범위 안에는 있지만 아직 “초기화되지 않았기 때문에 죽어있는” 구간입니다.

  • TDZ의 효과: TDZ에 있는 변수에 접근하면 ReferenceError가 발생합니다.

  • TDZ의 목적: 이런 메커니즘은 개발자가 의도하지 않은 순서로 변수를 사용하는 실수를 방지합니다.

    예를 들어 var라면 의도치 않게 undefined 상태의 변수를 사용해버릴 수 있지만, let/const는 차라리 에러를 내주어 잘못된 사용을 빨리 알아차릴 수 있게 합니다.

  • const와 TDZ: const도 let과 동일하게 TDZ가 적용됩니다. 추가로 const는 선언 시 반드시 초기화를 해야 하므로, 더욱이 TDZ 구간이 끝나는 즉시 특정 값이 할당되어야 합니다. (이 부분은 뒤에서 자세히 다룹니다.)

스코프(Scope) 차이: 함수 스코프 vs 블록 스코프

스코프(scope) 는 변수가 유효한 범위를 의미합니다. JavaScript에는 크게 함수 스코프와 블록 스코프 두 가지 개념이 있습니다.

#️⃣ var – 함수 스코프(Function Scope):

var로 선언된 변수는 오직 함수 단위로 스코프가 구분됩니다. 함수 내부에서 선언된 var 변수는 그 함수 전체에서 접근 가능하고, 함수 밖(전역)에서 선언된 var는 전역 스코프를 가집니다.

블록({ ... })은 var의 스코프를 생성하지 않습니다.

#️⃣ let/const – 블록 스코프(Block Scope):

let과 const로 선언된 변수는 자신이 속한 모든 중괄호 { } 블록을 스코프로 가집니다. 즉, 함수, if문, for/while 루프, try/catch 등 모든 블록 내에서만 유효하고 블록을 벗어나면 사라집니다.

function scopeTest() {
  if (true) {
    var x = 1;
    let y = 2;
    const z = 3;
  }
  console.log(x); // 1  (`var`는 함수 전체에서 유효)
  console.log(y); // ReferenceError (`let`은 블록 밖에서 접근 불가)
  console.log(z); // ReferenceError (`const`도 블록 밖 접근 불가)
}
scopeTest();

x는 var라서 함수 scopeTest 전체에서 유효하므로 블록 밖에서도 x 값을 출력할 수 있습니다. 그러나 yz는 각자 블록 내에서만 유효한 let과 const라 블록을 벗어나면 사라지므로 ReferenceError가 발생합니다.

📚 중첩 스코프와 shadowing:

블록 스코프가 도입되면서, 바깥 스코프의 변수 이름을 안쪽 블록에서 let/const로 다시 선언(섀도잉shadowing)할 수도 있습니다. 예를 들어, 전역에 let value = 1;이 있고 함수 안 블록에 let value = 2;를 선언하면, 블록 내부에서는 새롭게 선언된 2를 가리키고 블록 밖에서는 여전히 전역의 1을 가리킵니다.
var의 경우에는 동일 이름을 블록 안에 선언해도 결국 같은 함수 스코프이므로 섀도잉 개념이 적용되지 않고 같은 변수를 가리키게 됩니다.

🗾 전역 스코프 차이:

전역에서 var로 변수를 선언하면 해당 변수는 전역 객체(window 등의)의 속성으로도 추가되지만, let/const로 전역 선언한 변수는 전역 객체의 속성이 되지 않습니다. 예를 들어 브라우저 환경에서 var globalVar = 5;window.globalVar로 접근 가능하지만, let globalLet = 5;window.globalLet으로 접근되지 않습니다. 이는 스코프 체인과 전역 객체와의 관계 차이입니다.

스코프 체인 (Scope Chain)

스코프 체인이란 현재 실행 중인 코드(렉시컬 환경)가 어떤 변수에 접근할 때, 먼저 자신의 환경 레코드(LexicalEnvironment.record) 에서 찾고, 없으면 외부 렉시컬 환경으로 거슬러 올라가며(링크된 outer), 최종적으로 전역 렉시컬 환경까지 탐색하는 일련의 구조입니다. 이 순서(“체인”) 덕분에 중첩 함수가 외부 변수에 접근할 수 있습니다.

전역 객체 (Global Object)

브라우저 환경에서는 window(또는 globalThis), Node.js 환경에서는 global 같은, 전역에서 공통으로 사용할 수 있는 특수한 객체입니다. 전역 객체는 프로퍼티를 통해 전역 변수를 저장·관리합니다.

  • ES5 var 로 선언된 전역 변수는 전역 객체의 프로퍼티가 됩니다.
  • ES6 let/const 전역 선언은 DeclarativeEnvironmentRecord 에 저장되어, 전역 객체 프로퍼티로는 노출되지 않습니다.

재할당과 재선언 규칙

var:

선언 후 재할당 가능하며, 같은 스코프 내 재선언도 허용됩니다.
예를 들어 var a = 1; a = 2; 또는 var a = 1; var a = 3;처럼 동일한 변수명을 다시 var로 선언해도 에러가 나지 않습니다. 두 번째 var a는 사실상 의미가 없고 무시되거나 기존 값을 유지하지만, 문법적으로 오류는 아닙니다.

let:

선언 후 재할당 가능하지만, 같은 스코프 내 재선언은 불가합니다.
예를 들어 let b = 1; b = 2;는 가능하지만, let b = 1; let b = 2;를 같은 블록에서 실행하면 SyntaxError가 발생합니다.
한 번 선언된 b를 동일 블록 안에서 다시 선언할 수 없다는 뜻입니다.

var와 다르게 의도치 않은 중복 선언을 막아주는 장점이 있습니다.

const:

선언 후 재할당이 불가능합니다. const로 선언한 변수는 한 번 할당된 값이 변하지 않는다는 의미인데, 정확히는 “변수 바인딩(참조)을 바꿀 수 없다” 는 뜻입니다.
따라서 const c = 1; c = 2;와 같이 재할당하려 하면 TypeError(할당 오류)가 발생합니다. 또한 const는 선언과 동시에 초기화가 필수입니다. const d;처럼 선언만 하는 것은 문법 에러입니다. 재선언도 당연히 불가능합니다.

const와 불변성(immutability)

const로 선언했다고 해서 값 자체가 완전히 불변(immutable) 이 되는 것은 아닙니다. const는 변수에 대한 참조(reference) 를 고정하는 것이지, 그 값의 내부를 얼리는 것은 아닙니다.

원시 값(primitive) 의 경우:

숫자, 문자열 등 원시 타입은 값을 직접 갖고 있고 재할당만 막으면 되므로, const로 선언하면 사실상 그 값은 불변처럼 취급할 수 있습니다. 예를 들어 const PI = 3.14;는 PI를 다른 숫자로 바꿀 수 없으니 불변 상수로 쓰입니다.

객체(object)나 배열(array) 의 경우:

const는 해당 객체를 가리키는 참조를 변경할 수 없게 할 뿐, 객체 내용 수정까지 막지 않습니다.

const person = { name: "Alice", age: 25 };
person.age = 26;               // 객체 프로퍼티 변경 (이건 허용됩니다)
console.log(person.age);       // 26 (내부 값 변경됨)

위 코드에서 person은 const이므로 다른 객체로 재할당은 불가능하지만, person.name이나 person.age와 같은 내부 속성은 변경할 수 있습니다. 즉, 얕은 불변(shallow immutability) 만 보장되는 셈입니다.

마찬가지로 const numbers = [1, 2, 3];이라 해도 numbers.push(4)처럼 배열에 요소를 추가하는 건 가능합니다 (배열 자체의 참조는 그대로이고 내용만 바뀜). 하지만 numbers = [4,5,6];처럼 아예 다른 배열을 할당하는 것은 불가능합니다.

요약하면, const는 변수 재할당을 막을 뿐, 객체의 내부 상태까지 자동으로 불변으로 만들지 않습니다.

루프에서의 차이점

for 루프와 스코프

for 루프의 헤더에서 var로 변수를 선언하면, 그 변수는 전체 함수 스코프를 가집니다. 반면 for 루프 헤더에서 let으로 선언하면 그 변수는 루프의 반복마다 새로운 블록 스코프를 가집니다.

for (var i = 0; i < 3; i++) {
  // ... 
}
console.log(i); // 3 (루프 종료 후에도 i는 살아있음)

for (let j = 0; j < 3; j++) {
  // ... 
}
// console.log(j); // ReferenceError: j is not defined (j는 루프 밖에서 없음)

let의 반복마다 새로운 바인딩

let으로 선언한 루프 변수가 가지는 특별한 점은 매 이터레이션(Iteration)마다 독립적인 새로운 변수로 취급된다는 것입니다. 자바스크립트 엔진은 for loop을 돌 때, 반복마다 현재 값을 캡처한 새 렉시컬 환경을 생성합니다. 이를 통해 다음 섹션에서 설명할 클로저 문제를 해결하게 됩니다.

while 루프

while이나 do...while의 경우에는 루프 자체가 별도 블록으로 간주되지 않으므로, var는 함수 스코프, let은 해당 while 블록 에서만 유효합니다. while은 반복마다 새 스코프를 만들지는 않습니다.

for 루프의 특별한 점은 헤더 부분에 선언한 let 변수를 반복 시마다 새로 바인딩해준다는 것이고, 일반 while은 그런 동작은 없습니다.

for...of / for...in

이 루프들도 마찬가지로 let이나 const를 사용할 수 있습니다. 특히 for...of나 for...in에서는 const를 사용하는 패턴도 볼 수 있는데, 매 반복마다 새로운 상수로서 아이템을 받기 때문에 가능합니다.

클로저에서의 var vs let (루프 클로저 이슈)

JavaScript에서 클로저(closure) 는 함수가 생성될 당시의 환경(스코프)을 기억하여 이후에도 그 내부 변수에 접근할 수 있는 기능을 말합니다. var와 let은 이 클로저와 상호 작용할 때 특히 차이를 보이는데, 주로 루프 안에서 클로저를 생성하는 상황에서 두드러집니다.

❗문제 상황 (var 사용 시)

for (var k = 0; k < 3; k++) {
  setTimeout(function() {
    console.log("var loop:", k);
  }, 100);
}

위 코드는 의도적으로 각 100ms 뒤에 k 값을 출력하는 타임아웃을 설정했습니다. 직관적으로는 0, 1, 2가 출력될 것 같지만, 실제로는 "var loop: 3"이 세 번 출력됩니다.

var k는 함수 스코프이기 때문에 루프를 돌면서 같은 k 변수를 공유합니다. 클로저 (여기서는 setTimeout의 콜백 함수)가 참조하는 k 또한 하나뿐인 동일한 k를 가리킵니다. 루프가 끝나고 나서 100ms 후 콜백이 실행될 때, k는 이미 3으로 증가된 후이므로 모든 콜백이 그 최종 값 3을 출력하는 것입니다.

✅ 해결책 – let 사용

for (let m = 0; m < 3; m++) {
  setTimeout(function() {
    console.log("let loop:", m);
  }, 100);
}

이 경우에는 "let loop: 0", "let loop: 1", "let loop: 2"가 순서대로 출력됩니다. 그 이유는 let m이 반복할 때마다 새로운 m 변수를 생성하여 각 콜백이 서로 다른 m을 가리키게 해주기 때문입니다.
즉, 첫 번째 iteration의 콜백은 m=0인 변수에 클로저를 걸고, 두 번째는 m=1인 새로운 변수에, 세 번째는 m=2인 변수에 클로저를 겁니다.
결과적으로 각각 독립된 값들을 기억하게 되어 의도한 대로 동작하게 됩니다.

정리하면, var는 함수 스코프라 루프를 돌면서 하나의 변수를 공유하지만, let은 블록 스코프라 매 반복마다 새로운 변수를 생성해서 클로저가 각각 그 값을 잡아두기 때문입니다.

📝 요약하면..

특성 var let const
호이스팅 호이스팅 됨 ✅(선언과 동시에 undefined로 초기화) 호이스팅 됨 ✅(하지만 TDZ로 선언 전 접근 불가) 호이스팅 됨 ✅(TDZ 적용, 선언 전에 사용 불가)
스코프 함수 스코프 (블록 무시) 블록 스코프 블록 스코프
TDZ 존재 ❌ (선언 이전에도 변수 존재하며 undefined) ✅ (선언 전까지 접근 시 ReferenceError) ✅ (선언 전까지 접근 시 ReferenceError)
재할당 가능 ✅ (값 변경 자유로움) 가능 ✅ (값 변경 가능) 불가능 ❌ (한번 할당한 후 변경 불가)
재선언 가능 ✅ (동일 스코프에 중복 선언 허용, 에러 없음) 불가능 ❌ (동일 스코프에 중복 이름 선언 시 에러) 불가능 ❌ (동일 스코프에 중복 이름 선언 시 에러)
불변성 해당 없음 ❌ (언제든지 값 변경 가능) 해당 없음 ❌ (언제든지 값 변경 가능) 참조 불변 ✅ (객체의 경우 속성은 변경 가능)
루프 사용 루프 후에도 변수 유지, 한 변수로 반복 ❌ (클로저시 문제 발생 가능) 루프 내부 및 반복마다 새로운 바인딩 ✅ (클로저에 안전) 루프 내부에서만 사용 ✅, for...of/in에 주로 활용 (일반 for에서 증감 불가)
클로저 캡처 하나의 변수 공유 ❌ (나중 값으로 치환되어 버림) 각 이터레이션 값 개별 캡처 ✅ (의도한 값 유지) 각 이터레이션 값 개별 캡처 ✅ (값이 변하지 않음)
전역 객체 속성 전역에서 선언 시 전역 객체(window 등)에 속성으로 추가됨 ✅ 전역에서 선언되어도 전역 객체의 속성이 아님 ❌ 전역에서 선언되어도 전역 객체의 속성이 아님 ❌
profile
Pasting my dream in code

0개의 댓글