JavaScript
선언, 렉시컬 스코프와 클로저
함수는 자신이 호출된 곳이 아니라 정의된 위치의 바깥 환경을 따라 변수를 찾으며, 클로저는 그 환경에 대한 연결을 계속 유지합니다.
이럴 때 필요해요
반복문 안의 비동기 콜백 문제나 비공개 상태 캡슐화, 선언 전 변수 접근 오류의 원리를 일관되게 이해할 수 있습니다.
어떻게 동작하나요?
함수 안에서 어떤 변수 이름을 읽을 때, 함수 자신의 블록 안에 그 변수가 없다면 어디에서 값을 찾아올까요? JavaScript는 함수가 어디서 호출되었는지가 아니라, 소스 코드의 어디에서 작성(정의)되었는지를 기준으로 바깥쪽 변수를 찾습니다. 이 규칙을 렉시컬 스코프(Lexical Scope)라고 부릅니다. 엔진이 함수를 호출할 때마다 현재 실행 상태와 변수 바인딩을 관리하는 실행 컨텍스트(Execution Context)를 만들고, 그 안의 렉시컬 환경 연결(스코프 체인)을 따라 가장 가까운 스코프부터 바깥쪽으로 이름을 탐색합니다.
변수를 선언하는 키워드에 따라 유효 범위(스코프)와 초기화 시점도 달라집니다. var는 함수 스코프(또는 모듈·전역 스코프)를 가지며 코드 실행 전에 undefined로 바로 초기화되지만, let과 const는 중괄호 단위의 블록 스코프를 가지며 실제 선언문이 실행되기 전까지 TDZ(일시적 사각지대)에 놓여 접근 시 ReferenceError가 발생합니다. 흔히 말하는 호이스팅(Hoisting)은 코드가 물리적으로 맨 위로 이동한다는 뜻이 아니라, 실행 전에 선언이 스코프에 먼저 등록되는 현상을 설명하는 말입니다.
바깥 함수가 내부 함수를 반환하고 실행을 마쳤더라도, 반환된 함수가 바깥 함수의 변수를 참조하고 있다면 그 변수 환경은 사라지지 않고 유지됩니다. 이처럼 함수와 그 함수가 만들어질 당시의 렉시컬 환경이 함께 묶인 구조를 클로저(Closure)라고 합니다. 아래 예제의 next 함수처럼 클로저는 생성 시점의 값을 사진 찍듯 복사해 두는 것이 아니라 변수 바인딩 자체를 참조하므로, 호출할 때마다 같은 변수를 이어서 갱신할 수 있습니다.
코드 예제
동작을 설명하는 읽기 전용 예제입니다.
function counter() {
let value = 0;
return () => ++value;
}
const next = counter();
console.log(next(), next()); // 1 2같은 반환 함수 next를 두 번 호출하므로 같은 value 바인딩이 1, 2로 바뀝니다. counter()를 다시 호출해 얻은 함수는 별도 환경에서 0부터 시작합니다.
자주 헷갈리는 포인트
- const는 변수 재할당만 금지할 뿐, 변수가 가리키는 객체 내부의 프로퍼티 변경까지 막는 선언이 아닙니다.
- 클로저를 사용한다는 이유만으로 메모리 누수가 되는 것은 아니며, 더 이상 쓰지 않는 큰 데이터의 참조가 불필요하게 살아남는지를 확인해야 합니다.
- 심화: 일반 블록과 달리 클래스 정적 초기화 블록은 자체 var 범위를 갖습니다. 모듈 최상위 var도 전역 객체의 속성이 아닙니다.
내 말로 설명해 보기
예제에서 next()를 한 번 더 호출하면 무엇이 나오고, counter()로 새로 만든 함수를 호출하면 무엇이 나올까요?
생각할 단서 next가 읽는 value가 어디에 남아 있는지, counter 호출마다 환경이 같은지 생각해 보세요.
예시 답변 확인
설명에 들어갈 핵심
- 같은 next 함수는 자신이 기억하는 동일한 value 바인딩을 계속 갱신합니다.
- counter()를 다시 호출하면 이전과는 독립된 새로운 렉시컬 환경을 만듭니다.
예시 답변
기존 next를 세 번째 호출하면 3입니다. counter()로 새로 만든 함수를 처음 호출하면 1입니다. 두 함수가 같은 코드여도 생성된 환경은 다릅니다.
문장을 그대로 외우기보다 예제의 값과 실행 흐름을 떠올리며 설명해 보세요.
함께 읽으면 좋은 개념
관련 면접 질문 8개
변수 선언·초기화·할당은 어떻게 다른가요?
선언은 스코프에 변수 이름을 등록하는 단계이고, 초기화는 그 변수에 첫 값을 정하는 단계이며, 할당은 변수에 값을 대입하는 연산입니다. 예를 들어 let x;는 선언문이 실행될 때 undefined로 초기화되고 이후 x = 3으로 값을 할당합니다. 반면 const x = 3은 재할당이 금지되므로 선언과 초기화를 반드시 한 번에 해야 하며, var와 let은 초기화되는 시점이 달라 선언문 앞에서 접근할 때의 결과도 다르게 나타납니다.
꼬리질문과 참고자료 →var·let·const와 TDZ는 어떻게 다른가요?
var는 함수 스코프(또는 전역·모듈 스코프)를 가지며 선언 처리 시점에 undefined로 바로 초기화되는 반면, let과 const는 블록 스코프를 가집니다. let과 const로 선언한 변수는 스코프에 등록되더라도 실제 선언문이 실행되어 초기화되기 전까지 TDZ(일시적 사각지대)에 놓이며, 이 구간에서 접근하면 ReferenceError가 발생합니다. 또한 let은 재할당이 가능하고 const는 재할당이 금지되지만, const에 할당된 객체 내부의 프로퍼티 변경까지 막는 것은 아닙니다.
꼬리질문과 참고자료 →함수 선언과 함수 표현식은 사용 시점이 어떻게 다른가요?
함수 선언문(function fn() {})은 스코프 시작 시점에 함수 객체까지 함께 초기화되므로 선언문보다 앞선 줄에서도 호출할 수 있습니다. 반면 함수 표현식(const fn = function() {} 또는 화살표 함수)은 변수에 함수 값을 할당하는 방식이므로, 해당 변수가 초기화되고 함수가 대입된 이후에만 호출할 수 있습니다. 또한 표현식으로 자주 쓰는 화살표 함수는 일반 함수와 달리 자체적인 this나 arguments, 생성자 기능을 갖지 않는다는 차이도 있습니다.
꼬리질문과 참고자료 →