프로그래밍에서 변수나 함수에 접근할 수 있는 유효 범위를 뜻한다.
let globalVar = "I'm global"; // 전역 변수
function printGlobalVar() {
console.log(globalVar); // 어디서든 globalVar에 접근 가능
}
printGlobalVar(); // "I'm global"
지역 스코프는 함수나 블록({}) 내에서 선언된 변수에만 해당된다. 이 변수들은 그 블록 밖에서는 접근할 수 없다.
함수 안에서 선언된 변수는 함수 내부에서만 사용할 수 있다.
함수가 실행될 때마다 새로운 스코프가 생기고 그 안에서 변수가 유효하다.
function myFunction() {
let localVar = "I'm local"; // 지역 변수
console.log(localVar); // 함수 안에서만 사용 가능
}
myFunction(); // "I'm local"
console.log(localVar); // 오류 발생: localVar는 함수 밖에서 접근 불가
{} 중괄호로 감싸진 블록 내에서 선언된 변수에만 적용된다.
let 과 const로 선언된 변수는 블록 스코프를 따른다.
if (true) {
let blockVar = "I'm in a block";
console.log(blockVar); // 블록 안에서는 접근 가능
}
console.log(blockVar); // 오류 발생: blockVar는 블록 밖에서 접근 불가
렉시컬 스코프는 함수가 어디서 선언되었는지에 따라 결정되는 스코프 규칙을 이야기한다.
함수가 다른 함수 안에서 선언될 때 그 함수는 자신이 선언된 스코프를 기억한다.
클로저는 함수와 그 함수가 선언될 당시의 렉시컬 환경을 함께 기억하는 기능을 이야기한다.
자바스크립트의 스코핑과 함수 실행 방식에서 나오는 자연스러운 기능이다.
쉽게 이야기하면 함수와 함수 선언 당시의 상태를 기억하는 것이다.
function sayHello() {
let name = "Alice";
console.log(`Hello, ${name}!`);
}
sayHello(); // "Hello, Alice!"
여기서 sayHello라는 함수가 실행되면 그 안에 있는 name이라는 변수를 사용해서 메세지를 출력한다. 이 함수의 실행이 끝나면 name 변수는 사라진다.
function createGreeter() {
let name = "Alice"; //`name`은 createGreeter 함수 내에서만 사용 가능
return function() {
console.log(`Hello, ${name}!`); // inner 함수는 `name`을 참조할 수 있음
};
}
const greeter = createGreeter();
greeter(); // "Hello, Alice!"
위 코드에서 inner function은 name이라는 변수를 사용할 수 있다. 왜냐하면 inner function 은 createGreeter 함수 안에서 정의되있고 자바스크립트는 이 함수가 정의된 위치에서 가까운 스코프에 있는 변수를 찾기 때문이다. 여기서 중요한 것은 inner function이 createGreeter 함수의 변수에 접근할 수 있다는 것이다. 이게 바로 렉시컬 스코프이다.
또 이 예시에서 중요한 점은 createGreeter 함수는 실행이 끝났다. 보통 함수가 끝나면 함수 안에서 선언된 모든 변수는 사라진다. 그런데 반환된 함수(greeter)는 여전히 name이라는 변수에 접근하고 있다.
이게 바로 클로저의 역할이다. greeter 함수는 단순히 함수만 리턴하는 것이 아니라 이 함수는 자신이 만들어질 때 함수 주변에 있는 변수들(여기서는 name)도 같이 기억하고 있는다. 그래서 함수가 실행된 후에도 name 변수에 계속 접근할 수 있는 것이다.
자바스크립트는 함수형 프로그래밍 패러다임을 지원하는 언어로 함수를 1급 시민(First-Class Citizen)으로 다룬다.
함수를 1급 시민 또는 1급 객체로 다룬다는 것은 함수가 다른 값들(숫자, 문자열 등)과 동일하게 취급된다는 것을 의미한다. 즉 함수를 데이터처럼 다룰 수 있다는 의미이며 1급 시민 또는 1급 객체라 함은 다음 4가지 특징을 충족한다.
함수는 숫자나 문자열처럼 변수에 저장할 수 있다.
const sayHello = function() {
return "Hello!";
};
console.log(sayHello()); // "Hello!"
함수는 다른 함수의 인자로 전달될 수 있다. 이를 고차 함수라고 한다.
function greet(callback) {
console.log(callback());
}
greet(function() {
return "Hi!";
}); // "Hi!"
함수는 다른 함수에서 반환될 수 있다.
function createMultiplier(multiplier) {
return function(x) {
return x * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10
함수는 배열이나 객체 같은 데이터 구조에 저장될 수 있따.
const functions = [
function() { return "First"; },
function() { return "Second"; }
];
console.log(functions[0]()); // "First"
console.log(functions[1]()); // "Second"
함수를 1급 객체로 다루면 함수가 인자로 전달되거나 반환될 수 있으므로 함수가 원래 선언된 스코프를 벗어나서도 호출될 수 있다. 이때 발생하는 문제가 바로 함수가 선언될 당시의 변수 상태를 유지할 필요성이다.
function createGreeter() {
let name = "Alice"; // 원래 함수의 스코프에 있는 변수
return function() {
console.log(`Hello, ${name}!`);
};
}
const greeter = createGreeter();
greeter(); // "Hello, Alice!"
이 코드에서 함수가 반환되면 원래 createGreeter 함수는 종료되고 스코프도 사라진다. 하지만 반환된 함수는 여전히 name 변수에 접근하고 그 값을 사용하고 있다. 여기서 클로저가 동작하는 것이다. 반환된 함수는 자신이 선언된 스코프에 있던 name 변수를 기억하고 유지하기 때문에 함수가 종료된 이후에도 그 변수를 사용할 수 있다.
클로저는 변수를 함수 외부에서 직접 접근하지 못하게 하고 내부에서만 관리할 수 있는 정보 은닉 기능을 제공한다. 이를 통해 객체지향 프로그래밍의 캡슐화와 유사한 효과를 얻을 수 있다.
function createCounter() {
let count = 0; // count 변수는 외부에서 직접 접근할 수 없음
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
위 예시에서는 count 변수는 메서드만을 통해 값을 변경할 수 있게 된다.
클로저는 함수가 실행된 이후에도 특정 변수의 상태를 기억할 수 있기 때문에 상태를 저장하고 관리하는 데 유용하다. 여러분이 React를 하셨다면 useState의 원리가 클로저인지 아실 수 있으실 겁니다! 특히 c언어의 static 변수를 javascript에서 쓰고 싶어서 클로저를 사용한 적이 있다!
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
count 값은 계속 기억된다.
또한 리액트의 기본 이론적 개념에 대해 알아보자!의 Memoization 항목을 보면 아래와 같은 예시가 있다.
function memoize(fn) {
var cachedArg;
var cachedResult;
return function(arg) {
if (cachedArg === arg) {
return cachedResult;
}
cachedArg = arg;
cachedResult = fn(arg);
return cachedResult;
};
}
이런 식으로 caching을 할 수도 있다.
자바스크립트의 비동기 처리나 이벤트 핸들러에서 변수를 클로저를 통해 안전하게 캡처하고 사용할 수 있다. 특히 반복문과 비동기 호출을 함께 사용할 때 클로저는 매우 유용하다.
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 출력: 4 4 4
위 코드에서는 setTimeout이 실행될 때 i 값이 이미 4로 되어 있기 때문에 예상과 다르게 출력된다. 이를 클로저로 해결하면 각 i 값이 독립적으로 유지된다.
for (var i = 1; i <= 3; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, 1000);
})(i);
}
// 출력: 1 2 3
즉시 실행 함수를 사용해 클로저를 만들어 각 반복에서 i값을 캡처함으로써 원하는 값이 출력되도록 한다.
함수를 인자로 넘기거나 함수에서 함수를 반환할 때 클로저는 매우 유용하다.
function executeTwice(fn) {
let count = 0;
return function() {
count++;
if (count <= 2) {
return fn();
} else {
return "Function already executed twice!";
}
};
}
const myFunction = executeTwice(() => "Executed!");
console.log(myFunction()); // "Executed!"
console.log(myFunction()); // "Executed!"
console.log(myFunction()); // "Function already executed twice!"
위의 예시에서는 클로저 덕분에 함수 호출 횟수를 추적할 수 있다.
리액트의 기본 이론적 개념에 대해 알아보자의 Continuations 부분에 currying에 대한 설명이 있다!
여기서 설명을 하자면 currying은 하나의 함수가 여러 개의 인자를 한 번에 받는 대신 인자를 하나씩 받아 새로운 함수를 반환하는 패턴이다.
function add(x) {
return function(y) {
return x + y;
};
}
const add5 = add(5);
console.log(add5(3)); // 8
console.log(add5(10)); // 15
여기서 add(5)는 5를 기억하는 클로저를 생성하고 반환된 함수는 5를 더하는 함수로 사용할 수 있다. 이러한 방식은 코드의 재사용성을 높일 수 있다.