본문 바로가기

[자바스크립트] 클로저에 대해 알아보자

 

 

 

들어가며

알고리즘 문제를 하나씩 풀면서, 클로저란 무엇인지 알아보고 싶었습니다. 다른 글이나 설명들을 보면 상당히 복잡한 경우가 있었는데, 이걸 어떻게 하면 쉽게 설명할 수 있을까 고민해보고 싶었습니다. 이번 기회에 이 부분을 제대로 정리해봐야겠다고 생각했습니다.

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

변수의 유효범위

클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지 먼저 이해해야 합니다. 

 

 

 

{
  let message = "Hello"; 
  console.log(message); 
}

console.log(message); 

 

위의 경우 스코프 안에서는 변수를 콘솔 값에 출력할 수 있었지만, 스코프 밖에서는 스코프 안에 있는 변수를 콘솔 값에 출력할 수 없었습니다. 즉 자바스크립트는 함수 내부에서 함수 외부에 있는 변수에 접근할 수 있지만 함수 외부에 있는 변수는 함수 내부에 접근할 수 없습니다. 이때 함수와 함수가 접근할 수 있는 스코프가 클로저 관계를 맺습니다. 그렇다면 클로저란 무엇일까요? 한 번 알아보겠습니다.

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

출처 : 자바스크립트 클로저(Closure)

 

 

클로저(Closure)

클로저는 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미합니다. 일단 클로저를 살펴보기 전에 하나의 예시를 보겠습니다.

 

.....

let name = 'epitone'
function log() {
	console.log(name);
}

function wrapper() {
	let name = 'epitone';
    log();
}

wrapper();

 

지금 상황으로는 전역범위 안에 function log와 function wrapper가 있습니다. 이 때 function log()와 전역범위는 클로저 관계를 이루고 있습니다. 즉 무슨 코드를 써도 클로저 관계가 형성될 수 밖에 없습니다. 왜냐하면 log함수는 외부의 변수를 기억하고, 외부 변수에 접근할 수 있는 함수이기 때문입니다. 

 

function outer(){
	var a = 1;
	console.log(a);
}

outer();

 

이 경우는 함수안에서 콘솔을 실행하는데, 콘솔 값으로 출력하는 a가 무슨 값이 들어있는지 알아야합니다. 이때 무슨 값이 들어있는지 찾는 곳이 바로 스코프입니다. 지금 a는 outer라는 함수 안에 속해있습니다. 자바스크립트는 함수 단위로 스코프가 생성됩니다.

 

function outer(){
	var a = 1;
    
    function inner(){
    	var a = 2;
    	console.log(a);
    }
	inner();
}

outer();

 

outer 함수를 실행하면, 안에서 Inner 함수가 실행됩니다. 그럼 Inner안의 콘솔이 찍힐 것입니다. 찍어보면 2가 찍히는 것을 알 수 있습니다. a가 2개가 있는데, 어느 a를 실행시킬 것인지 찾으려면, 스코프를 찾으면 됩니다. outer 말고, Inner용 스코프가 생깁니다.  함수 단위, inner 함수가 실행될 때 찾는 것이 스코프입니다. a를 찾았으니, inner의 표에서 먼저 a를 찾는 것입니다. 

 

function outer(){
	var a = 1;
    var b = 'B';
    
    function inner(){
    	var a = 2;
    	console.log(b);
    }
	inner();
}

outer();

 

만약 outer 함수에 변수 b를 넣어보겠습니다. 이번엔 inner 함수에서 b를 콘솔로 찍었는데, inner 스코프 안에 변수 b가 없으므로 다음 스코프에서 변수를 찾습니다. 

 

 

var d = 'X';

function outer(){
	var a = 1;
    var b = 'B';
    
    function inner(){
    	var a = 2;
    	console.log(d);
    }
	inner();
}

outer();

 

만약 전역범위에 변수 d가 있다면, inner 함수에서 d를 콘솔값으로 찍는다면, 콘솔값으론 X가 나올 것입니다.

 

var d = 'X';

function outer(){
	var a = 1;
    	var b = 'B';
    
    	function inner(){
    		var a = 2;
    		console.log(b);
    	}
	return inner;
}

var someFun = outer();
someFun();

 

이 상황에서 outer 함수가 하는 일은 inner 함수를 리턴하고, inner 함수에서는 b를 출력합니다. 그래서 변수 someFun은 outer 함수를 실행했을 때의 리턴 값으로 inner를 할당받고, someFun을 실행하면, inner 함수를 실행하게 됩니다. 즉 someFun을 실행하면, inner 함수가 실행됩니다. 

 

outer 내부에서 선언한 변수 b는 Outer가 실행하는 순간, 리턴하고 끝나면 b의 변수는 사라진다고 생각할 수 있습니다. 하지만 여기서는 b가 찍힙니다. 이게 클로저입니다. 생성한 시점의 스코프 체인을 들고 있습니다. 즉, 외부 변수를 기억하고, 외부 변수에 접근할 수 있는 콜로저 관계를 갖고 있습니다. 클로저 때문에 outer 함수가 실행된 다음에도 inner 함수가 outer 함수에 대한 스코프를 기억하고, 전역변수도 기억하고 있습니다. 그래서 outer 함수가 실행된 다음에도 outer 함수의 스코프에 접근할 수 있습니다.

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

for 문에서 var와 let의 차이

반복문에서 var와 let의 출력값은 큰 차이가 납니다. 아래의 코드를 살펴보겠습니다.

 

var btns = [
	document.getElementById('btn0'),
    document.getElementById('btn1'),
    document.getElementById('btn2')
];

function setClick() {
	for (var i = 0; i < 3; i++) {
    	btn[i].onclick = function() {
        	console.log(i);
        }
    }
}

setClick();

 

HTML 코드에서 버튼 3개를 만들고, btns 배열을 통해 각각의 버튼을 통제합니다. 그 후, setClick 함수를 호출해서 for문을 돕니다. 만약 for문을 돌면서 i번째 버튼에 클릭을 했을 때, i를 출력하는 것이 이 반복문의 목적입니다. 예상한다면, btn0 버튼을 누르면 0, btn1 버튼을 누르면 1, btn2 버튼을 누르면 2가 나와야합니다. 하지만 실행을 시켜보면, 버튼을 0을 눌러도 3, 1을 눌러도 3, 2를 눌러도 3이 나옵니다. 

 

하지만 let은 var와는 다른 결과를 출력합니다.

 

var btns = [
	document.getElementById('btn0'),
    document.getElementById('btn1'),
    document.getElementById('btn2')
];

function setClick() {
	for (let i = 0; i < 3; i++) {
    	btn[i].onclick = function() {
        	console.log(i);
        }
    }
}

setClick();

 

var를 let으로 바꾸면, 버튼을 누를 때 마다 버튼에 맞는 숫자가 나옵니다. 그렇다면, 무슨 차이가 있길래 이렇게 출력이 되는 것인지 알아보겠습니다.

 

 

var와 let의 차이

var과 let의 차이는, var는 함수 단위, let은 중괄호 단위입니다. var는 무조건 함수단위입니다. 밑의 코드를 보며 이해해보겠습니다.

 

function x() {
	{
    	var t = 1;
    }
	console.log(t);
}

x();

 

x라는 함수가 있습니다. 중괄호 안에는 다시 중괄호가 있고, 그 안에는 var t에 1을 할당합니다. 그리고 중괄호를 끝내고, 콘솔 값으로 t를 출력합니다. 그렇게 x 함수를 실행하면, 1이 출력됩니다. var는 함수 단위이기 때문에 중괄호와는 아무 상관없습니다. x라는 함수 범위 안에서 함수의 스코프에 들어갑니다.

 

let과의 차이에 대해 알아보겠습니다.

 

function x() {
	{
    	let t = 1;
    }
	console.log(t);
}

x();

 

let은 중괄호 안에서 동작합니다. 그래서 위의 경우 변수 t는 중괄호 안에서만 존재합니다.  그래서 콘솔값을 출력해보면, t는 아무것도 없다는 출력값을 받을 수 있습니다. 즉, var은 함수, let은 중괄호의 스코프에 들어갑니다.

 

그렇다면, 다시 반복문의 경우를 살펴보겠습니다.

 

 

 

 

 

 

 

var btns = [
	document.getElementById('btn0'),
    document.getElementById('btn1'),
    document.getElementById('btn2')
];

function setClick() {
	for (var i = 0; i < 3; i++) {
    	btn[i].onclick = function() {
        	console.log(i);
        }
    }
}

setClick();

 

전역범위에는 btns 배열과 setClick 함수가 존재합니다. 그리고 setClick이 실행될 때 setClick을 위한 스코프가 생깁니다. 여기에 들어오는 변수는 i가 있습니다. this도 들어가지만, 클로저, 스코프 체인에 관한 이야기이기 때문에 제외하겠습니다. 

 

동작 과정은 다음과 같습니다.

 

먼저 setClick 함수에서 for문을 실행합니다. 반복을 하면서 첫번째 i에 0이 들어가고, 0번째 클릭에 콘솔값을 출력하는 함수 하나를 넣어줍니다. i가 0이므로, btns 배열에서 0번째 버튼에 함수 하나를 만들어서 연결해주는데, 그 함수가 실행될 스코프가 있을 것입니다. 이를 총 3번 반복합니다. 즉 0번째 버튼, 1번째 버튼, 2번째 버튼에서 함수를 실행합니다.

 

반복문을 돌면서, i가 3보다 작지 않을 때 for문을 종료합니다. 그리고 setClick이 종료됩니다.

 

만약 버튼을 클릭했을 때, 만약 스코프 안에서 원하는 값이 없으면 선언된 위치에서 상위의 스코프를 바라봅니다. 핸들러들을 찾아보고 없으면 setClick을 찾아보고, setClick에서도 없으면 전역범위에서 원하는 값을 찾습니다. 만약 btn2의 온클릭을 눌러서 실행되면, 맨처음 하는 것은 console.log(i)를 할 때 i가 스코프에 있나 확인하고, 없으면 상위 스코프를 바라봅니다. 이때 setClick에는 i가 3에서 반복문을 종료했기 때문에 상위 스코프에 있는 값인 3을 콘솔에 출력합니다.

 

 

 

 

 

 

 

 

 

var btns = [
	document.getElementById('btn0'),
    document.getElementById('btn1'),
    document.getElementById('btn2')
];

function setClick() {
	for (let i = 0; i < 3; i++) {
    	btn[i].onclick = function() {
        	console.log(i);
        }
    }
}

setClick();

 

하지만 var를 let으로 바꾸면 정상작동합니다. 그럼 이건 왜 정상작동 하는 것일까요? 그것은 바로 let으로 선언했을 때는 var로 선언했을 때 보다 중간에 스코프가 하나 더 있기 때문입니다. onclick을 감싸고 있는 중괄호가 존재합니다. 그럼 실질적으로 onclick이 setClick을 찾아보고 없을 때 setClick을 먼저 찾아보는 것이 아니라 let으로 선언한 변수를 먼저 찾아봅니다. 먼저 찾아보고 없으면 setClick에서 찾습니다.

 

즉, onclick 함수를 찾아보고, 찾아보고 없으면 for문의 중괄호를 찾아보고, setClick을 찾아보고, 그 다음에도 없으면 글로벌을 찾아보는 것입니다. 다시 정리하면, i는 for 문을 돌 때 마다 새로운 값을 넣어줍니다. 즉 두 번째 for문을 돌면, 두 번째 중괄호가 형성됩니다. 즉 새로운 스코프가 생긴다고 볼 수 있습니다. 

 

여기서 의문인건, 중괄호 스코프에 i가 형성되는건 알겠는데, 값이 계속 변합니다. 그럼 i를 어떻게 계속 증가시킬 수 있는지 궁금할 수 있습니다. let은 for문에서만 특이하게 동작합니다. 원래 i는 중괄호에서 형성될 때가 사라져야합니다. 하지만 다른 중괄호에서는 i값이 증가해도 전혀 다른 i인데 마치 같은 값인것마냥 증가합니다. 이는 for문에서만 갖고 있는 특징입니다. 편의 때문에 이렇게 만든 것 같습니다. let이 없던 시절에 babel을 통해 알 수 있습니다. 

 

 

 

 

 

 

 

 

 

 

 

for문을 실행하는데, 따로 loop라는 함수를 만들고, loop에다가 i를 넘겨줍니다. i argument로 받은 i는 function loop 스코프에 형성됩니다. i를 계속 주입해주는 것이고, 실행할 때 마다 새로운 루프 i의 스코프가 형성되는 것이고, 그 안에서 온클릭이 형성됩니다. 파란색으로 만든 스코프가 function 루프로 만드는 똑같은 일을 하는 것입니다. 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

마치며

클로저에 대해, 오랜 시간동안 이해하지 못한 부분이 있었는데, 차근차근 공부하면서 드디어 깨달을 수 있었습니다. 아무리 어려운 문제라도 시간을 갖고 공부하다보면, 언젠가는 이해하고 넘어갈 수 있는 날이 올 것이라 믿습니다. 앞으로도 꾸준히, 지속해서 공부할 수 있는 개발자가 되고 싶습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

출처

 

클로저 - JavaScript | MDN

클로저 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다. 다음을 보자:

developer.mozilla.org