JavaScript 스코프 & 클로저: 외우는 걸 멈추고, 이해하기 시작하라
Source: Dev.to
위 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다.
(코드 블록, URL, 마크다운 형식 등은 그대로 유지됩니다.)
가혹하게 솔직해지자
자바스크립트 기술 면접에 들어갈 때, 면접관은 Stack Overflow에서 복사‑붙여넣기 하는 사람을 찾는 것이 아닙니다. 그들은 기계가 어떻게 동작하는지를 이해하는 사람을 원합니다.
개발자들을 가장 많이 혼란스럽게 하는 두 개념은 Scope와 Closure입니다.
대부분의 개발자는 이들에 대해 막연하고 “대충” 이해하고 있습니다. 작동한다는 것은 알지만 왜 그런지는 모릅니다. 면접에서는 “대충”이라는 것이 통하지 않습니다. 변수가 undefined가 되는 이유나 루프가 잘못된 숫자를 출력하는 이유를 설명하지 못하면 시험에 떨어집니다.
이 개념들을 마스터하려면, 자바스크립트를 블랙 박스로 취급하는 것을 멈추고 자바스크립트 엔진처럼 생각해야 합니다.
Source: …
Part 1 – Scope Is a Set of Rules
개발자들이 흔히 저지르는 첫 번째 실수는 스코프를 변수가 존재하는 물리적인 “장소”라고 생각하는 것입니다.
스코프를 규칙 집합이라고 생각하는 것이 더 좋습니다. JavaScript 엔진은 이 규칙을 사용해 식별자 이름으로 변수를 찾아야 할 위치를 결정합니다.
JavaScript는 렉시컬 스코프(lexical scope) 라는 개념을 사용합니다. 오늘 읽게 될 가장 중요한 용어입니다. “Lexical”은 lexing 혹은 파싱 단계와 관련이 있습니다.
쉽게 말하면: 스코프는 여러분이 코드를 물리적으로 작성한 위치에 의해 결정됩니다. 함수들의 중첩 구조가 스코프의 중첩 구조를 결정합니다. 한 번 작성되면, 이 규칙들은 (대부분) 코드가 실행되기 전에 이미 확정됩니다.
정신 모델: 사무실 건물
프로그램을 여러 층으로 이루어진 사무실 건물에 비유해 보세요.
| 층 | 설명 |
|---|---|
| 1층 (로비) | 전역 스코프 |
| 선언하는 각 함수 | 현재 위치 위에 새로 생기는 개인 층 |
엔진이 함수 안(예: 3층)에서 코드를 실행하고 변수를 찾아야 할 때(starthroat라고 가정), 다음과 같은 엄격한 절차를 따릅니다:
- 현재 층에서 찾기 – 3층에
starthroat가 있나요? 있으면 사용합니다. - 바깥쪽으로 이동 – 없으면 계단을 내려 2층으로 갑니다. 거기에 있나요?
- 반복 – 로비(전역 스코프)에 도달할 때까지 한 층씩 내려갑니다.
- 포기 – 로비에도 없으면
ReferenceError를 발생시킵니다. 존재하지 않는 변수입니다.
중요 인터뷰 팁: 스코프 조회는 위쪽(바깥)으로만 진행됩니다. 아래쪽(안쪽)으로는 절대 이동하지 않습니다. 로비는 3층에서 일어나는 일을 볼 수 없습니다.
var buildingName = "JS Towers"; // Lobby (global)
function floorTwo() {
var manager = "Kyle"; // 2nd‑floor scope
function floorThree() {
var developer = "You"; // 3rd‑floor scope
// Engine looks here (floor 3), finds nothing.
// Goes down to floor 2, finds `manager`. Success.
console.log(manager);
}
}
Source: …
Part 2 – 클로저는 마법이 아니다
스코프가 조회 규칙의 집합이라면, 클로저는 그 규칙을 구부렸을 때 일어나는 현상입니다.
많은 사람들이 클로저를 막연하게 정의합니다. 여기서는 정확히 정의해 보겠습니다:
클로저는 함수가 자신의 렉시컬 스코프 밖에서 실행되지만, 여전히 그 스코프에 접근할 수 있을 때 관찰됩니다.
보통 함수 실행이 끝나면 그 스코프는 가비지 컬렉션에 의해 회수되고 메모리가 해제됩니다. 클로저는 이 현상을 방지합니다.
정신 모델: 배낭
함수 B를 함수 A 안에 정의하면, 함수 B는 함수 A의 주변 스코프에 대한 “숨은 링크”를 얻게 됩니다.
함수 B가 함수 A 밖으로 전달되어 다른 곳에서 사용될 때, 빈손으로 나가지 않습니다. 그 숨은 링크를 함께 가지고 나갑니다.
이를 배낭에 비유해 보세요. 함수 B는 함수 A가 존재하던 시점에 있던 모든 변수를 담은 배낭을 메고 있습니다. 함수 B가 어디서 실행되든, 언제 실행되든, 그 배낭을 열어 그 변수들에 접근할 수 있습니다.
function outer() {
var secret = "XYZ_123"; // 스코프 규칙에 따라 outer()가 끝나면 사라져야 합니다.
function inner() {
// `inner`는 `secret` 변수를 클로저합니다.
// `secret`을 배낭에 넣습니다.
console.log("The secret is: " + secret);
}
return inner; // `inner`를 외부로 보냅니다.
}
// outer()가 실행되고 완전히 끝납니다.
var myRef = outer(); // `inner`가 이제 `myRef`에 저장됩니다.
// ... 몇 시간 후 ...
// `myRef`가 전역 스코프에서 실행됩니다.
// 그럼에도 불구하고 여전히 `outer`의 스코프를 기억하고 있습니다.
myRef(); // "The secret is: XYZ_123"
면접에서 단순히 “기억한다”고 말하지 마세요. 이렇게 말하세요:
“클로저 덕분에
inner는outer의 렉시컬 스코프에 대한 참조를 유지하게 되며, 그 스코프가 가비지 컬렉션되는 것을 방지합니다.”
Part 3 – 고전적인 인터뷰 함정
인터뷰에서 클로저에 대해 질문받는다면, 다음 루프 문제의 변형을 볼 확률이 약 **90 %**입니다. 이는 스코프 경계와 값 참조의 차이를 이해했는지를 테스트합니다.
The Problem
for (var i = 1; i // (original code omitted in source)
Pro tip: 문제를 해결하려면, 각 반복마다 새로운 렉시컬 환경을 만들거나(let을 사용하고 var 대신) 현재 값을 IIFE나 bind로 캡처하세요.
Using let (block‑scoped)
// Using let (block‑scoped)
for (let i = 1; i console.log(i), i * 1000);
}
Using an IIFE
// Using an IIFE
for (var i = 1; i console.log(j), j * 1000);
})(i);
}
Bottom Line
- Scope = 엔진이 식별자를 찾기 위해 따르는 규칙입니다.
- Closure = 원래의 렉시컬 환경이 사라진 뒤에도 함수가 그 규칙에 접근할 수 있게 해 주는 메커니즘입니다.
이러한 사고 모델을 마스터하면 스코프나 클로저에 관한 어떤 인터뷰 질문에도 자신 있게 답변할 수 있을 뿐만 아니라, 더 중요한 것은 JavaScript가 그렇게 동작하는 이유를 이해하게 됩니다.
해결 방법 (Pre‑ES6 IIFE 패턴)
루프의 각 반복마다 현재 i 값을 “캡처”하기 위해 새로운 스코프를 생성해야 합니다.
for (var i = 1; i <= 3; i++) {
// Create an Immediately Invoked Function Expression (IIFE)
// This creates a new scope bubble for every loop iteration.
(function (j) {
setTimeout(function timer() {
// timer now closes over 'j', which is unique to this iteration's scope
console.log(j);
}, j * 1000);
})(i); // Pass in current 'i' value
}
(※ 참고: 최신 JavaScript에서는 for 루프 헤드에서 var i를 let i로 바꾸기만 하면 됩니다. let은 각 반복마다 새로운 블록 스코프를 자동으로 생성합니다. 두 가지를 모두 언급하는 것은 역사적 맥락과 최신 지식을 보여주기 위함입니다.)
요약
코드 조각을 외우지 말고 모델을 외우세요.
- Scope는 변수가 어디서 접근 가능한지를 나타내며, 작성 시점(lexically)에서 결정됩니다. 사무실 건물 층을 생각해 보세요.
- Closure는 변수가 언제 접근 가능한지를 나타냅니다. 함수는 나중에 실행되더라도 자신의 lexical scope를 기억합니다. 배낭을 생각해 보세요.
면접에서 코드를 볼 때, 머릿속으로 스코프 라인을 추적하세요. 스스로에게 물어보세요:
- “이 변수가 속한 스코프 버킷은 어디인가?”
- “이 함수가 배낭에 무엇을 담고 있는가?”
그렇게 하면 면접을 통과하는 것뿐만 아니라 매일 사용하는 언어를 실제로 이해하게 됩니다.