최근 면접 준비 하면서 자바스크립트의 개념에 대해서 다시 살펴보고 있는데요. 면접 단골 질문이기도 하면서 자바스크립트의 동작 방식을 이해하기 위해서 필수로 알아야할 클로저에 대해서 정리해보려고 합니다~!
클로저는 주변 상태(어휘적 환경)에 대한 참조와 함께 묶인 함수의 조합니다. 즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다. Javascript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성됩니다. 출처-MDN
사실 우리는 코딩을 하면서 이미 클로저를 자주 사용해왔습니다. 클로저 구조는 다음과 같습니다.
function outer() {
let x = 10;
function inner() {
console.log(x);
}
return inner;
}
outer(); // 10
위 코드의 결과가 왜 10이 나올까요? 코드가 다음과 같이 실행되기 때문입니다.
위 처럼 클로저란, 함수 안에 정의된 다른 함수가 외부 함수의 변수에 접근할 수 있게 해주는 구조입니다. 그렇다면 자바스크립트에서는 이런 클로저가 왜 가능한 걸까요?
자바스크립트에서 클로저가 가능한 이유는 렉시컬 환경(Lexical Environment) 과 실행 컨텍스트(Execution Context) 덕분입니다. 자바스크립트는 렉시컬 스코프를 따르기 때문에, 함수가 어디에 선언되었는지에 따라 변수의 유효 범위가 결정됩니다. 이를 기반으로 클로저가 형성됩니다.
렉시컬 스코프란?
함수의 선언 위치에 따라 변수의 유효 범위(스코프)가 정해지는 것을 말합니다. 즉, 함수 내에서 참조할 수 있는 변수들은 그 함수가 선언된 위치를 기준으로 결정됩니다.
렉시컬 환경이란 변수나 함수가 어디서 정의되었는지를 기억하고 관리하는 개념입니다.
렉시컬 환경을 구성하는 두가지 요소가 있습니다.
실행 컨텍스트는 자바스크립트 코드가 실행될 때 생성되는 실행 환경을 의미 합니다. 즉, 자바스크립트 엔진이 코드를 해석하고 실행하는 동안의 환경을 말하며 함수 호출이나 코드 블록이 실행될 때 새로운 실행 컨텍스트를 생성합니다.
실행 컨텍스트의 구성 요소는 크게 렉시컬 환경, 변수 객체, this 값이 있습니다.
정리하자면,
따라서, 렉시컬 환경을 통해 변수와 함수의 선언 위치를 추적하고 이를 바탕으로 실행 컨텍스트 내에서 변수에 접근 할 수 있도록 도와주기 때문에 클로저가 가능하게 됩니다.
클로저는 다음과 같은 일을 가능하게 합니다.
데이터의 은닉화는 외부에서 특정 데이터를 직접 수정하지 못하도록 하고, 그 데이터를 수정하는 메서드만을 제공하는 방식입니다. 이를 통해 중요한 데이터를 보호하고, 예기치 않은 변경을 방지할 수 있습니다.
function createCounter() {
let count = 0; // 외부에서는 접근할 수 없는 변수
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
console.log(counter.getCount()); // 1
이 예시에서 count 변수는 createCounter 함수 내부에 정의되어 있으며, 외부에서 직접 접근할 수 없습니다. 대신, 클로저를 통해 반환된 메서드(increment, decrement, getCount)만 사용하여 값을 변경하거나 조회할 수 있습니다. 이렇게 데이터를 은닉하고 외부에서의 직접적인 접근을 차단하는 방식으로 데이터의 은닉화가 이루어집니다.
함수형 프로그래밍은 함수를 일급 객체로 취급하며, 함수들이 다른 함수를 반환하거나 인자로 받을 수 있는 특징이 있습니다. 클로저는 이러한 함수형 프로그래밍에서 유용하게 사용됩니다. 특히, 내부 함수가 외부 상태에 의존하면서 그 상태를 기억하는 데 클로저가 사용됩니다.
function multiply(factor) {
return function(number) {
return number * factor;
};
}
const multiplyBy2 = multiply(2);
const multiplyBy3 = multiply(3);
console.log(multiplyBy2(5)); // 10
console.log(multiplyBy3(5)); // 15
위 코드에서 multiply 함수는 내부에서 또 다른 함수를 반환합니다. 이때 반환된 함수는 factor 변수에 접근할 수 있는데, 이는 클로저 덕분입니다. 클로저를 사용하면 외부 함수의 인자 값을 기억하는 고차 함수를 쉽게 구현할 수 있으며, 이러한 방식은 함수형 프로그래밍에서 자주 사용됩니다
클로저는 비동기 작업에서 상태를 유지하는 데 유용하게 사용됩니다. 비동기 작업이 완료된 후 콜백 함수가 실행될 때 외부 함수의 변수에 접근할 수 있습니다.
function fetchData(url) {
let data = null;
return new Promise((resolve, reject) => {
setTimeout(() => {
data = `Fetched data from ${url}`;
resolve(data);
}, 1000);
});
}
function displayData() {
fetchData('https://api.example.com').then(data => {
console.log(data); // "Fetched data from https://api.example.com"
});
}
displayData();
위 코드에서 fetchData 함수는 비동기적으로 데이터를 가져오는 작업을 합니다. 데이터가 가져와지면, then 안의 콜백 함수에서 data를 사용할 수 있습니다. 이때 data는 fetchData 함수 내에서 정의된 변수입니다. 비록 비동기 작업이 완료되었지만, 클로저 덕분에 비동기 작업 후에도 data에 접근할 수 있는 것입니다. 이처럼 클로저는 비동기 작업에서 중요한 역할을 하며, 작업이 끝난 후에도 데이터를 안전하게 사용할 수 있게 합니다.
사실 클로저는 새로운 개념은 아니고 이미 알고 있고 사용하는 개념이었습니다. 클로저에 대해 정확히 정의하고, 자바스크립트에서 왜 가능한지를 이해함으로써 자바스크립트의 동작 원리를 더욱 깊이 이해할 수 있었습니다. 특히 그동안 통신할 때 사용했던 비동기 처리 방식을 이해하는데 큰 도움이 됐습니다. 원리를 이해하고 사용하는 것의 중요성을 다시 한번 느끼면서 20000...