JavaScript / 전역변수와 지역변수 차이, 스코프 이해하기

전역 변수는 현재 실행 환경의 바깥 범위에 선언되어 여러 코드에서 접근할 수 있고, 지역 변수는 함수나 블록처럼 선언된 범위에서만 접근할 수 있습니다. 같은 이름을 안쪽 범위에서 다시 선언하면 그 안에서는 안쪽 변수가 바깥 변수를 가립니다. 범위는 이름을 어디에서 읽거나 바꿀 수 있는지를 결정합니다.

함수 밖과 안에서 같은 이름 사용하기

다음 코드에서 함수 안의 message는 바깥의 message와 다른 변수입니다. 함수가 안쪽 변수를 읽어도 바깥 값은 그대로 남습니다. 이처럼 안쪽 선언이 바깥 이름을 가리는 것을 섀도잉(shadowing)이라고 합니다.

const message = '바깥';

function readInside() {
  const message = '안쪽';
  return message;
}

console.log(readInside()); // '안쪽'
console.log(message);      // '바깥' 

반대로 함수 안에서 새 변수를 선언하지 않고 바깥의 let 변수에 대입하면 바깥 값이 바뀝니다. 의도하지 않은 공유 상태를 만들 수 있으므로, 필요한 값을 함수 인수로 전달하고 결과를 반환하는 방식도 고려하세요. MDN의 스코프 정의에서 이름을 찾는 범위를 확인할 수 있습니다.

let total = 5;
function addOne() {
  total += 1; // 새 total을 선언하지 않았으므로 바깥 변수를 변경
}
addOne();
console.log(total); // 6

let·const의 블록 범위와 var의 함수 범위

let과 const는 선언된 중괄호 블록 안에서만 이름을 사용할 수 있습니다. var는 블록이 아니라 함수를 기준으로 범위가 정해집니다. 따라서 if 블록 안의 var는 같은 함수의 나머지 부분에서도 보일 수 있습니다.

function checkBlock() {
  if (true) {
    let modern = '블록 안';
    var legacy = '함수 안';
    console.log(modern); // '블록 안'
  }
  console.log(legacy); // '함수 안'
  // console.log(modern); // ReferenceError: 블록 밖
}
checkBlock();

예제의 주석을 해제하면 modern은 범위 밖이므로 ReferenceError가 납니다. 확인한 뒤 다시 주석 처리하면 원래 예제로 되돌릴 수 있습니다. const는 변수 이름에 다른 값을 재대입하지 않을 때, let은 재대입이 필요할 때 사용합니다. var와의 범위 차이는 MDN의 let 문서와 MDN의 var 문서에 설명되어 있습니다.

함수가 끝나면 지역 변수도 항상 사라질까요?

지역 변수는 함수 밖에서 이름으로 직접 접근할 수 없지만, 함수를 반환해 그 안쪽 변수를 계속 참조하면 값이 유지될 수 있습니다. 이를 클로저(closure)라고 합니다. 따라서 “함수가 종료되면 지역 변수는 무조건 소멸한다”는 설명은 정확하지 않습니다.

function createCounter() {
  let count = 0;
  return () => ++count;
}

const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2

두 번째 호출에서도 count가 이어지는 이유는 반환된 함수가 그 변수를 참조하기 때문입니다. MDN의 클로저 안내에서 이 동작과 사용 사례를 살펴볼 수 있습니다.

전체 예제: 값의 범위와 유지 시간 확인

다음 코드를 UTF-8로 scope-example.html에 저장하고 브라우저에서 엽니다. 화면에는 바깥과 함수 안의 label이 각각 표시됩니다. 버튼으로 클로저 안의 카운터가 호출 사이에 이어지는지도 확인할 수 있습니다.

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>전역과 지역 변수 범위 비교</title>
</head>
<body>
<h1>변수 범위 확인</h1>
<button id="increase" type="button">카운터 증가</button>
<button id="reset" type="button">처음으로</button>
<pre id="output" aria-live="polite"></pre>
<script>
const label = '바깥';

function labelInside() {
  const label = '안쪽';
  return label;
}

function createCounter() {
  let count = 0;
  return () => ++count;
}

let nextCount = createCounter();
let currentCount = 0;
const output = document.querySelector('#output');
function show() {
  output.textContent = `바깥 label: ${label}\n함수 안 label: ${labelInside()}\n카운터: ${currentCount}`;
}

document.querySelector('#increase').addEventListener('click', () => {
  currentCount = nextCount();
  show();
});
document.querySelector('#reset').addEventListener('click', () => {
  nextCount = createCounter();
  currentCount = 0;
  show();
});
show();
</script>
</body>
</html>

처음에는 바깥 label: 바깥, 함수 안 label: 안쪽, 카운터: 0이 보입니다. ‘카운터 증가’를 두 번 누르면 2가 되며 바깥의 label은 바뀌지 않습니다. ‘처음으로’를 누르면 새 카운터가 만들어져 0으로 돌아갑니다. 브라우저의 일반 스크립트에서 최상위 const·let은 전역 범위의 이름일 수 있지만 window의 속성이 되지는 않으며, 모듈 스크립트의 최상위 이름은 모듈 범위에 속합니다.