클로저
👀 실행 결과는?
const outerFunction = () => {
const a = 10;
return function (b) {
console.log(a + b);
};
};
const getValue = outerFunction();
getValue(20);
다음 함수의 실행 결과를 알기 위해서는, closer의 개념을 알아야합니다.
자바스크립트의 실행 과정
우선 클로저 개념을 알기 위해서는, 자바스크립트의 실행 과정과 실행컨텍스트, 스코프와 같은 개념을 알아야합니다.
자바스크립트 코드는 다음 세 단계를 거쳐 실행됩니다.
1️⃣ 파싱
코드를 실행하기 전에, 자바스크립트 엔진은 먼저 파싱 과정을 거칩니다.
파싱은 코드를 의미 있는 조각(토큰)으로 나누고, 토크나이징이 끝나면, 파서(parser)는 이 토큰들의 목록을 받아 문법 규칙에 따라 코드의 구조를 나타내는 트리 형태로 만듭니다. 이를 추상 구문 트리라고 합니다.
코드로 대략 보기
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [//declarations 선언에 포함된 변수 목록을 담는 자식 배열
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "sum"
},
"init": { //초기값
"type": "Literal",
"value": 10,
"raw": "10"
}
}
],
"kind": "let" //let 키워드 사용
}
],
"sourceType": "module"
파싱 과정에는 소스코드 분석과, AST(추상 구문트리) 생성이 있습니다.
- 소스 코드 분석: 자바스크립트 파일이 V8 엔진에 의해 처음으로 로드되면, 엔진은 이 코드를 파싱합니다.
- AST 생성: AST는 자바스크립트 코드의 구조를 트리 형태로 나타내며, 코드의 문법적 구조를 표현합니다. 이를 통해 V8 엔진은 코드가 무엇을 하려는지 이해할 수 있습니다.
2️⃣ 컴파일
컴파일 과정은 파싱과정에서 AST가 생성 된 후 이를 바이트 코드로 변환하는 과정
바이트 코드 : 고수준의 언어(JS) → 기계어의 중간 단계에 위치하는 코드
JIT(Just - In- Time) → 컴파일을 활용하여 실행 중에 바이트 코드가 네이티브 코드(기계어)로 변환합니다.
3️⃣ 실행(Execution)
컴파일이 완료된 바이트 코드는 실행 단계에서 처리됩니다.
실행이 시작되면 실행 컨텍스트가 생성되며, 변수 할당, 함수 호출, 연산 수행 등이 이루어집니다.
자바스크립트는 싱글 스레드로 하나의 콜스택을 활용하여 실행 컨텍스트를 관리합니다. 함수가 호출되면 새로운 실행 컨텍스트가 스택에 추가되고, 수행이 완료되면 제거됩니다.
function first() {
second();
console.log('함수1');
}
function second() {
third();
console.log('함수2');
}
function third() {
console.log('함수3');
}
first();
다음 코드의 실행 결과는 어떻게 될까요? 정답: 실행 결과는? 함수3 → 함수2 → 함수1
코드가 시작될 때 생성되고 전역 공간의 정보가 저장되는 컨텍스트는 → 전역 실행 컨텍스트입니다.
함수가 호출될 때 생성되는 컨텍스트는 → 함수 실행 컨텍스트입니다.
실행 컨텍스트의 단계
실행 컨텍스트는 코드가 실행될 때 자동으로 생성되며, 크게 생성, 실행의 두가지 단계를 거칩니다.
1. 생성 단계
실행 컨텍스트가 생성되고, 자바스크립트 엔진은 실행할 코드의 환경을 설정합니다.
-
이때 전역 코드, 함수 코드의 여부가 판단됩니다.
-
var 키워드: 호이스팅되어 스코프의 최상단으로 끌어올려져, undefined로 초기화 됩니다.
-
let, const 키워드 → 초기화 X(TDZ), 변수 이름만 등록
-
함수 선언문 → 호이스팅 및 초기화
2. 실행 단계
코드를 위에서 아래로 한 줄씩 실행하며 값을 할당하고, 연산이 수행되고, 함수가 실행됩니다.
function sayHello() {
let text = 'Hello, world!';
console.log(text);
}
sayHello();
- sayHello 호출
- 실행 컨텍스트 등록
- text 변수 이름 등록
- 할당 → Hello world 변수에 할당
- console.log 실행 → Hello world 출력
- 실행 컨텍스트가 콜스택에서 제거
실행컨텍스트 환경 실행 컨텍스트는 변수환경과, 렉시컬 환경을 가집니다.
변수 환경은 렉시컬 환경과 구조가 동일하지만, 단순히 변수의 값이 변경되는 부분만을 관리합니다. → ex) 호이스팅 관리!!!
렉시컬 환경: 실행 컨텍스트의 스코프와 스코프 체인을 관리합니다.!!
렉시컬 환경
환경 레코드: 변수, 함수, 매개변수 저장
외부 렉시컬 환경: 현재 실행컨트 바깥의 렉시컬 환경을 참조하여 변수를 찾습니다.
function outer() {
let outerVar = "바깥 변수";
funtion inner(){
let innerVar = "안쪽 변수";
console.log(innerVar);//"안쪽 변수"
console.log(outerVar);//"바깥 변수"
}
inner();
}
outer();
클로저
outer함수의 실행 결과는?
function outer(){
let count = 0;
return funtion(){
count++;
console.log(count);
};
}
const counter = outer();
counter();//1
counter();//2
다음에서 1, 2가 순차적으로 출력되는 이유는 무엇일까요?
클로저의 개념 때문인데요, 앞서 살펴 보았던 내용처럼 자바스크립트의 실행 과정은 파싱 → 컴파일 → 실행의 흐름을 따라가며, 그 안에서 실행 컨텍스트, 렉시컬 환경, 스코프 체인이 함께 동작해 변수와 함수의 스코프를 관리합니다.
이때 중요한 것은
- 변수는 어떤 환경(Variable/Lexical Environment)에 저장되느냐
- 함수는 자신이 “선언된 위치”의 렉시컬 환경을 기억한다는 점 입니다.
이 구조 덕분에 함수는 자신이 선언될 당시의 외부 변수들을 기억하고, 함수가 종료된 이후에도 해당 환경을 유지한 채 접근할 수 있게 됩니다.
그리고 바로 이 동작 원리가 클로저(Closure)의 근본적인 원리입니다.
즉, 클로저란 “함수 + 그 함수가 선언될 당시의 렉시컬 환경”이 함께 묶여 동작하는 구조를 말합니다.
outer() 함수는 이미 실행을 마쳤지만, outer() 내부에서 생성된 익명 함수는 자신이 선언될 당시의 렉시컬 환경을 내부 슬롯([[Environment]])에 저장하고 있습니다.
이 렉시컬 환경 안에는
count = 0
이라는 변수가 존재하고,counter()가 호출될 때마다 다음 순서로 동작합니다.
익명 함수가 자신이 기억하고 있는 렉시컬 환경을 확인한다.
그 안의 count 변수를 찾아 1 증가시키고
증가된 값을 출력한다.
즉,
첫 번째 호출 → count가 0 → 1로 증가 → 출력
두 번째 호출 → 같은 count(이미 1인 값)를 다시 증가 → 2로 증가 → 출력
이렇게 같은 환경을 공유하며 변화가 누적되기 때문에 1, 2가 순차적으로 출력되는 것입니다.
const outerFunction = () => {
const a = 10;
return function (b) {
console.log(a + b);
};
};
const getValue = outerFunction();
getValue(20);
따라서, 처음 보았던 이 함수의 결과도, 30이 출력될 수 있는 것입니다.