[JS Deep Dive 스터디] 5회차 - scope

장효정·2025년 8월 31일

0. 스코프(scope)란?

스코프는 "식별자(변수 · 함수 이름)가 어디까지 유효한가"를 정하는 규칙이다.

즉, 변수를 어디서 선언했고, 어디까지 접근 가능한지를 결정하는 규칙이다.

JavaScript는 렉시컬 스코프(정적 스코프)를 따른다.

이는 함수가 어디서 호출되었는지가 아니라, 어디서 선언되었는지에 따라 상위 스코프가 결정된다는 뜻이다.

const x = 1;

function outer() {
  const x = 10;

  function inner() {
    console.log(x); // ?
  }

  inner();
}

outer();

outer 함수 안에는 지역 변수 x = 10이 있고, 전역에도 x = 1이 있다.

inner 함수는 outer 내부에서 선언되었으므로 상위 스코프가 outer의 지역 스코프로 고정된다.

따라서 inner 내부에서 x를 참조하면, 전역의 1을 보지 않고 outer의 10을 본다.

즉, 호출된 위치(전역이나 다른 함수)가 아니라, 선언된 위치(outer 내부)를 기준으로 상위 스코프가 결정되기 때문에 결과는 10이다.

🤓 그런데,

JavaScript는 렉시컬 스코프를 따른다고 했는데,

렉시컬 스코프는 무엇일까?

🙋🏻 렉시컬 스코프란?

렉시컬 스코프란, 함수의 상위 스코프가 호출 시점이 아닌 선언 시점에 의해 결정되는 규칙을 말한다.

즉, 함수는 자신이 어디서 만들어졌는지(태어난 환경)를 기억하고, 나중에 호출될 때도 그 환경을 기준으로 변수 검색을 한다.

const y = '전역';

function foo() {
  const y = 'foo-local';

  bar(); // bar를 foo 안에서 호출
}

function bar() {
  console.log(y);
}

foo();

foo 함수 안에서 bar()를 호출했지만, 결과는 '전역'이 출력된다.

왜냐하면 bar는 전역에서 선언된 함수이기 때문에, 상위 스코프가 전역으로 이미 고정되어 있기 때문이다.

만약 JavaScript가 동적 스코프 언어였다면, bar가 foo 안에서 호출되었으니 foo-local을 출력했을 것이다.

하지만 JavaScript는 렉시컬 스코프 언어라서, 함수의 상위 스코프는 호출된 곳이 아니라 선언된 곳으로 결정된다.

따라서 bar는 전역에 있는 y = '전역'을 참조한다.

👉 한마디로, JavaScript의 함수는 태어난 순간의 부모 스코프를 끝까지 따라간다. 🧑‍🧑‍🧒‍🧒

1. 전역 | 지역 | 블록 스코프

  • 전역 스코프 : 파일 전체, 어디서든 접근 가능
  • 지역 스코프 : 함수 내부에서만 접근 가능
  • 블록 스코프 : 중괄호 {} 내부에서만 접근 가능 (let, const로 선언한 경우)
const a = 'global';

function f() {
  const a = 'function-local';

  if (true) {
    const a = 'block-local';
    console.log(a);
  }

  console.log(a);
}

f();
console.log(a);

실행 순서대로 출력되는 값은 'block-local', 'function-local', 'global'이다.

console.log(a)를 실행할 때 엔진은 현재 스코프에서 먼저 변수를 찾고, 없으면 한 단계씩 바깥으로 올라가면서 찾는다.

이때, 가까운 스코프의 변수가 우선한다.

따라서 블록 안에서는 블록 내부의 변수가 출력되고, 블록을 벗어나면 함수 스코프의 변수가 출력되며, 함수 밖에서는 전역 변수가 출력된다.

이렇게 안쪽에서 바깥쪽으로 변수 검색을 이어가는 규칙스코프 체인이라고 한다.

2. 스코프 체인 (Scope Chain)

JavaScript 엔진은 변수를 참조할 때 현재 스코프 → 상위 스코프 → . . . → 전역 순서로 검색한다.

최종적으로 전역까지 갔는데도 찾지 못하면 ReferenceError가 발생한다.

const v = 0;

function g() {
  function h() {
    console.log(v);
  }

  h();
}

g();

h 함수에서 v를 찾을 때, h 내부 스코프에는 v가 없으므로 상위 스코프인 g 함수로 올라간다.

하지만 g 함수에도 v가 없으므로 다시 전역으로 올라가 전역의 v = 0을 찾게 된다.

이처럼 스코프가 계층적으로 연결된 구조를 스코프 체인이라고 한다.

3. var | let | const와 scope

구분varletconst
스코프함수 레벨 스코프 (function scope)블록 레벨 스코프 (block scope)블록 레벨 스코프 (block scope)
호이스팅선언과 동시에 undefined로 초기화 → 선언 전 참조 가능선언은 호이스팅되지만 초기화 전에는 TDZ에 있음 → 선언 전 참조 시 에러let과 동일, TDZ 존재
TDZ (Temporal Dead Zone)없음있음있음
재선언가능 (같은 스코프 내에서 중복 선언 가능)불가능불가능
재할당가능가능불가능 (단, 객체/배열 내부 프로퍼티는 변경 가능)
초기화 필요 여부선택 (선언만 해도 됨)선택 (선언 후 값 할당 가능)반드시 동시에 초기화해야 함
대표적 문제점블록 무시 → 의도치 않은 전역/함수 스코프 오염없음값 변경 불가라 유연성 떨어짐(하지만 안정성↑)
if (true) {
  var x = 1;
  let y = 2;
}

console.log(x);
console.log(y);

이 코드를 실행하면 x는 1이 출력되지만, y는 ReferenceError가 발생한다.

var로 선언된 x는 블록 스코프를 무시하고 가장 가까운 함수(또는 전역) 스코프에 저장된다. 따라서 if 블록을 벗어나도 접근이 가능하다. 😵‍💫

let으로 선언된 y는 블록 스코프 안에서만 유효하기 때문에 블록 밖에서는 존재하지 않는다. 😌

3-1. 함수 레벨 스코프 (var의 특징)

🤓 그런데,

블록 안에서 변수를 선언했는데도 바깥에서 접근이 된다면,

그건 왜 그런 걸까?

JavaScript에서 var 키워드로 선언한 변수는 함수 레벨 스코프를 가진다.

즉, 함수 내부에서 선언되면 그 함수 안에서만 유효하지만, if, for, while 같은 블록 내부에서 선언해도 블록을 무시하고 가장 가까운 함수(혹은 전역)에 등록한다.

이 말은 곧, var로 선언된 변수는 블록을 경계로 스코프가 나뉘지 않는다는 뜻이다.

그래서 블록 안에서 변수를 선언해도 바깥에서 접근이 가능하다.

if (true) {
  var x = 10;
}

console.log(x); // 10

function test() {
  if (true) {
    var y = 20;
  }

  console.log(y); // 20
}

test();
console.log(y); // ReferenceError (함수 밖에서는 접근 불가)

첫 번째 경우, if 블록 안에서 var x = 10을 선언했지만, var는 블록 스코프를 만들지 않는다. 따라서 x는 전역에 선언된 것과 같아지고, 블록 밖에서도 그대로 접근할 수 있다. 😵‍💫

두 번째 경우, test 함수 내부에서 if 블록 안에 var y = 20을 선언했다. 블록은 무시되기 때문에 y는 test 함수 전체에서 유효하다. 따라서 console.log(y)는 20을 출력한다. 그러나 y는 함수 스코프에 속해 있으므로, 함수 밖에서는 접근할 수 없어 ReferenceError가 발생한다.

⚠️ 문제점

이러한 특징 때문에 var를 사용하면, 의도치 않게 변수가 함수 전체나 전역으로 퍼져서 예측하기 어려운 동작이 발생할 수 있다.

예를 들어, 반복문에서 var를 쓰면 모든 반복이 같은 변수를 공유하는 문제가 생긴다. 🤯

💡 let과 const 비교

if (true) {
  let a = 1;
  const b = 2;
}

console.log(a); // ReferenceError
console.log(b); // ReferenceError

let과 const는 블록 스코프를 따르기 때문에, 블록 내부에서 선언된 변수는 블록 밖에서 접근할 수 없다.

이로 인해 var의 문제점이 해결되고, 변수의 범위가 명확해져 코드의 안정성이 높아진다. 🤗

그래서 현대 JavaScript에서는 var 대신 let과 const 사용이 권장된다.

3-2. 호이스팅과 TDZ (차이의 원인)

🤓 그런데,

호이스팅은 뭐고, TDZ는 무엇일까?

🙋🏻 호이스팅(Hoisting)이란?

JavaScript 엔진이 코드를 실행하기 전에 변수 선언과 함수 선언을 스코프의 최상단으로 끌어올려 처리하는 것처럼 동작하는 현상을 말한다.

실제 코드가 옮겨지는 것은 아니지만, 실행 컨텍스트가 생성될 때 선언이 먼저 메모리에 등록되기 때문에 마치 끌어올려진 것처럼 동작한다.

🔳 TDZ란?

Temporal Dead Zone의 약자로, 일시적 사각지대를 뜻한다.

let과 const는 호이스팅되지만, 선언문에 도달하기 전까지 변수에 접근할 수 없는 구간이 존재한다. 이 구간을 TDZ라고 부른다.

따라서 선언 전에 접근하면 ReferenceError가 발생한다.

// var : 호이스팅 시 undefined로 초기화
console.log(a); // undefined
var a = 10;

// let : TDZ 때문에 에러 발생
console.log(b); // ReferenceError
let b = 20;

// const : TDZ + 반드시 초기화 필요
console.log(c); // RefereneceError
const c = 30;

첫 번째 console.log(a)에서는 a가 아직 값이 할당되기 전이지만, var 선언은 호이스팅되면서 동시에 undefined로 초기화되었기 때문에 undefined가 출력된다.

두 번째 console.log(b)에서는 b가 let으로 선언되었으므로 호이스팅은 되었지만 TDZ 구간에 있다. TDZ는 선언문을 만나기 전까지 변수에 접근할 수 없는 영역을 뜻한다. 따라서 이 시점에서 b를 참조하면 ReferenceError가 발생한다.

세 번째 console.log(c)도 같은 이유로 ReferenceError가 발생한다. const 역시 TDZ에 묶여 있고, 추가로 const는 선언과 동시에 반드시 초기화해야 한다.

👉 요약하면,

var : 호이스팅 시 선언 + 초기화(undefined) → 선언 전 참조 가능하지만 값은 항상 undefined.

let/const : 호이스팅 시 선언만 되고 초기화는 실제 코드에 도달했을 때 이루어짐 → 그 전 구간(TDZ)에서는 접근 불가.

0개의 댓글