분명히 선언한 변수인데 왜 없다고 할까요? 함수 안에서 선언한 변수를 바깥에서 쓰려 할 때 ReferenceError가 납니다. JavaScript가 변수의 유효 범위를 엄격하게 구분하기 때문입니다.
스코프는 변수에 접근할 수 있는 유효 범위입니다. 변수가 어디서 선언되었는지에 따라, 코드의 어느 영역에서 그 변수를 읽고 쓸 수 있는지가 결정됩니다. 스코프가 없다면 모든 변수가 프로그램 전체에 노출되고, 이름 충돌과 예기치 않은 값 변경이 끊임없이 발생할 것입니다.
// 전역 스코프: 파일 최상단에 선언
const appName = "MyApp";
function greet() {
// 지역 스코프: 함수 안에서 선언
const message = "안녕하세요";
console.log(message); // "안녕하세요" — 접근 가능
console.log(appName); // "MyApp" — 전역 변수는 안에서 접근 가능
}
greet();
console.log(appName); // "MyApp" — 전역이므로 어디서든 접근 가능
console.log(message); // ReferenceError: message is not defined
전역 스코프에 선언된 변수는 어디서든 읽을 수 있습니다. 반면 함수 안에 선언된 변수는 그 함수 안에서만 살아있고, 함수가 끝나면 사라집니다.
var는 함수 단위로 스코프가 만들어집니다. if, for 같은 블록 {}은 var에게 새로운 스코프를 만들지 않습니다.
function testVar() {
if (true) {
var x = 10; // if 블록 안이지만 함수 스코프
}
console.log(x); // 10 — 블록 밖에서도 접근됨
}
function testLet() {
if (true) {
let y = 20; // 블록 스코프
const z = 30; // 블록 스코프
}
console.log(y); // ReferenceError
console.log(z); // ReferenceError
}
let과 const는 {}로 감싸인 블록 단위로 스코프가 생깁니다. for 루프에서 특히 차이가 도드라집니다.
// var: 루프가 끝난 뒤에도 i가 남아있음
for (var i = 0; i < 3; i++) {}
console.log(i); // 3
// let: 루프 블록 안에서만 유효
for (let j = 0; j < 3; j++) {}
console.log(j); // ReferenceError
| var | let | const | |
|---|---|---|---|
| 스코프 | 함수 스코프 | 블록 스코프 | 블록 스코프 |
| 재선언 | 가능 | 불가능 | 불가능 |
| 재할당 | 가능 | 가능 | 불가능 |
| 호이스팅 | 선언+undefined 초기화 | 선언만(TDZ 진입) | 선언만(TDZ 진입) |
let과 const는 호이스팅이 되기는 하지만, 선언 줄에 도달하기 전까지 TDZ 구간에 있어 접근하면 ReferenceError가 발생합니다.
호이스팅(Hoisting): 코드 실행 전에 변수·함수 선언을 스코프 맨 위로 끌어올리는 JavaScript 엔진의 동작. var는 선언과 함께 undefined로 초기화되고, let·const는 선언만 등록된 채 TDZ에 진입합니다.
TDZ(Temporal Dead Zone): 변수 선언이 호이스팅된 시점부터 실제 선언 코드가 실행되는 시점까지의 구간. 이 구간에서 변수에 접근하면 에러가 발생합니다.
JavaScript는 렉시컬 스코프(Lexical Scope) 방식을 따릅니다. 함수가 어디서 호출되었는지가 아니라, 어디서 정의되었는지를 기준으로 스코프가 결정됩니다.
const name = "전역";
function outer() {
const name = "outer";
function inner() {
// inner는 outer 안에서 정의되었으므로
// outer의 name에 접근할 수 있음
console.log(name); // "outer"
}
return inner;
}
const fn = outer();
fn(); // "outer" — 호출 위치(전역)가 아닌 정의 위치(outer 내부)가 기준
fn을 전역에서 호출했지만, inner가 정의된 곳이 outer 안이므로 outer의 name을 참조합니다.
변수를 찾을 때 JavaScript 엔진은 현재 스코프부터 시작해 바깥 스코프로 단계적으로 올라갑니다. 이를 스코프 체인이라고 합니다.
┌─────────────────────────────────┐
│ Global Scope │
│ const appName = "MyApp" │
│ │
│ ┌──────────────────────────┐ │
│ │ outer scope │ │
│ │ const name = "outer" │ │
│ │ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ inner scope │ │ │
│ │ │ search: name │ │ │
│ │ │ → not in inner │ │ │
│ │ │ → found: outer │ │ │
│ │ └──────────────────┘ │ │
│ └──────────────────────────┘ │
└─────────────────────────────────┘
search order: inner → outer → global → ReferenceError
안쪽 스코프에서 변수를 발견하면 탐색을 멈춥니다. 가장 가까운 스코프의 변수가 우선합니다.
var를 사용하면 의도치 않게 전역 변수가 늘어나고, 같은 이름의 변수가 충돌하는 전역 오염 문제가 발생합니다.
// a.js (라이브러리)
var count = 0;
function increment() {
count++;
}
// b.js (내 코드)
var count = 100; // 같은 이름으로 덮어씀
increment();
console.log(count); // 100? 아니면 1? — 예측 불가
두 파일이 같은 전역 스코프를 공유하기 때문에 count가 충돌합니다. let/const로 바꿔도 같은 전역 스코프라면 동일한 문제가 생깁니다.
| 개념 | 핵심 |
|---|---|
| 전역 스코프 | 파일 최상단 선언, 어디서든 접근 가능 |
| 지역 스코프 | 선언된 함수·블록 안에서만 유효 |
| 렉시컬 스코프 | 호출 위치가 아닌 정의 위치 기준 |
| 스코프 체인 | 안쪽에서 바깥쪽으로 단계적 탐색 |
| 키워드 | 스코프 | 재선언 | 재할당 | 호이스팅 |
|---|---|---|---|---|
var | 함수 | 가능 | 가능 | undefined로 초기화 |
let | 블록 | 불가 | 가능 | TDZ |
const | 블록 | 불가 | 불가 | TDZ |
var 대신 let/const를 사용하고 렉시컬 스코프의 동작 방식을 이해하면, 변수 관련 버그의 상당수를 사전에 차단할 수 있습니다.