📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄

[JavaScript] 클로저(Closure)란 무엇이며, 왜 쓰는걸까?

정의

클로저(Closure)란, '함수가 선언될 때 외부 함수의 렉시컬 환경을 참조하는 현상' 정도로 정의할 수 있다.

클로저의 예시를 찾아보면 외부 함수내부 함수가 있고, 외부 함수의 변수를 내부 함수에서 가져다가 쓰는 것을 클로저라고 설명하고 있다. 외부 함수의 변수를 쓸 수 있는 이유가 바로 외부 함수의 렉시컬 환경을 참조하기 때문이다.

(우선적으로 스코프 체인의 개념을 알아야 한다. 스코프 체인이란, 식별자를 찾을 때 자신이 속한 스코프에서 찾고

없을 시 상위 스코프에서 찾는 현상을 말한다.)

 

클로저는 외부 함수가 return 값으로 내부 함수를 선언하고 있고, 내부 함수에서는 외부 함수의 변수를 가져다가 사용하는 것으로 되어있다. 여기서 주의할 점은 내부 함수가 외부 함수의 변수를 참조한다는 점이다. 무조건 외부 함수와 내부 함수로 구성되어 있다고 해서 클로저가 아니라는 뜻이다.

 

ES6가 나오면서 var가 아닌 letconst로 변수를 선언하는 경우가 많기 때문에, (블록 레벨 스코프로 인해) 클로저의 입지(?)가 많이 줄어든 편이라고 한다. 하지만, 여전히 클로저는 자바스크립트에서 중요한 개념이기 때문에 아래의 몇 가지 예시들로 클로저를 이해해보자.

 

예시

function outer(){
    let count = 1;
    return function inner(){
        console.log(count);
    };
};

const a = outer();
a(); // 1

 

(위의 예시가 클로저의 전형적이고 기본 형태이기 때문에, 클로저를 이해하기 위해 위의 식 자체를 외워두는 것도 좋다.)

outer()의 변수 count의 정보가 inner()의 렉시컬 환경에 담겨 있기 때문에, count의 값을 가져올 수 있게 되었다. 또한, outer() 함수를 그냥 실행해버리면 inner의 함수식 자체가 return되기 때문에 변수 a에 담아서 사용했다.

 

일반적으로 변수를 이용하는 함수를 선언한다고 할 때, 변수를 전역 변수로 선언하고 사용하는 경우가 대다수일 것이다. 하지만, 클로저를 이용하게 되면 전역 변수로 선언하지 않아도 된다. 함수 안에 지역 변수로서 선언을 하고 내부 함수로 그 변수를 불러오게 하면 전역 변수를 줄일 수도 있고, 전역 변수 남발로 인한 오류를 미연에 방지할 수도 있다.

 

function outer(){
    let count = 5;
    return function inner(x){
        count += x;
        return console.log(count); 
    }
}

const a = outer();

a(5); // 10
a(5); // 15
a(5); // 20

위 코드는 내부 함수가 외부 함수의 변수를 어떻게 대하는 지에 대해서 설명하고 있다. 외부 함수 outer()는 변수 a에 들어가는 순간 사실상 죽은 거라고 보면 된다. 주인공은 inner()이다.

inner()의 렉시컬 환경에는 count라는 이름의 변수가 5라는 값을 가진 상태로 저장되어 있다. (외부 함수에서 뺏어옴)

inner()의 count와 outer()의 count는 이제 이름만 같지 다른 아이이다. 그렇기 때문에 아무리 outer()에서 count = 5로 선언되어 있다고 한들 inner()의 count는 다른 삶을 살고 있기 때문에 10, 15, 20처럼 계속 바뀔 수 있는 것이다.

 

for (let i = 0; i <= 5; i++){
    setTimeout(() => {
        console.log(i);
    }, 2000 * i)
}

// console 출력 (2초 마다 출력)
// 0
// 1
// 2
// 3
// 4
// 5

위 코드도 클로저의 예시로 자주 나오는 식이다. 바로 for문을 이용한 함수 실행문인데, 위 코드는 정상적으로 작동된다.

하지만, let이 아닌 var였다면 결과는 모두 5가 출력되는 상황이 나왔을 것이다. (ES6가 나온 게 얼마나 다행인지...)

그 이유는 var 같은 경우에는 블록 레벨 스코프가 아니기 때문에 setTimeout()이 i를 가져오지 못한다.

따라서, i는 먼저 5가 되버리고 그제야 setTimeout()이 실행돼서 결과적으로는 5만 출력됐을 것이다.

 

블록 레벨 스코프(let, const)로 인해 스코프 안에 스코프 형태, 즉 클로저의 환경이 만들어졌고 setTimeout()이 있는

내부 스코프에서 외부 스코프인 for문의 변수 i를 참조할 수 있게 되어서 0부터 5까지 순차적으로 출력될 수 있었다.

 

const click = (function (){
    let count = 0;
    return function (){
        count++;
        console.log(count);
    }
})();

btn.addEventListener('click', click);

즉시실행함수는 함수를 반환하고 즉시 소멸하는 함수이다. 하지만, 변수 count는 소멸되지 않고 렉시컬 환경에 남아 내부 함수가 count를 기억하게 된다. btn을 클릭하면 내부 함수가 호출되며 이때, 기억한 count의 값이 1 증가한다.

count는 클로저에 의해 참조되고 있기 때문에 계속 유효하며 변경된 최신의 상태를 계속 유지한다.

 

이처럼 클로저는 상태를 기억한다는 특징이 있기 때문에 click의 지역 변수인 count를 전역 변수처럼 사용할 수 있는 것이다. 쉽게 설명하자면, 클로저의 주인공은 사실 내부 함수이고 외부 함수는 처음 실행되는 순간 변수만 남기고 날라가버린다. 즉, 외부 함수의 역할은 내부 함수의 렉시컬 환경에 자신의 변수를 남기고 사라지는 게 끝이다.

 

1. 클로저는 외부/내부 함수로 구성되어 있으며, 외부 함수의 변수를 내부 함수가 사용하는 것 까지가 클로저이다.
2. 외부 함수는 내부 함수에 자신의 변수를 전달하고 사라진다. 내부 함수 입장에서는 그 변수를 자신의 렉시컬 환경에
    등록시켰기 때문에 그 변수를 계속 참조할 수 있으며, 그 변수의 값을 외부 함수 영향 없이 마음대로 수정할 수도 있다.
3. 외부 함수는 그저 실행될 때 이름만 빌려주고, 변수를 전달해주는 그림자일 뿐 진짜 주인공은 내부 함수이다.
4. 클로저를 이용하면 전역 변수를 사용하지 않아도 된다. 전역 변수는 언제든 누구나 변경하고 접근할 수 있기 때문에
    많은 부작용을 유발해 오류의 원인이 되므로 되도록이면 덜 사용하는 게 좋다.