[JavaScript] 렉시컬 스코핑, 클로저

김방울·2023년 2월 8일
1

JavaScript

목록 보기
5/7

얼마 전 기술 면접 때 클로저에 대한 질문을 받았는데, 두루뭉술하게만 알고 사용하던 개념이라(심지어 자주 쓰는데도 불구하고...!!!) 명확한 대답을 하지 못했습니다.
기초적인 개념 정리를 위해 글을 적어보도록 합니다...🐒

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

🤦🤦🤦

어휘적 범위 지정 (Lexical scoping)

스코프란 식별자(변수)의 유효 범위를 말합니다. 함수 밖에서도 참조할 수 있는 글로벌 스코프(전역 변수) 와 함수 내부에서만 접근할 수 있는 로컬 스코프(지역 변수) 가 있습니다.
렉시컬 스코프란 함수의 호출이 아닌 함수가 선언되는 시점에 상위 스코프가 결정되는 것을 의미합니다. (정적 스코프)

const name = 'bangul'; // 전역 변수 name

function cat(){
  const name = 'cheese'; // 지역 변수 cheese
  meow();
}

function meow(){ // 선언 시 가장 가까운 상위 스코프의 변수를 참조
	console.log(`${name}: meow`);
}

cat(); // bangul: meow
meow(); // bangul: meow

함수 meow() 를 선언할 때, 함수 내부의 변수 name은 자신의 스코프로부터 가장 가까운 상위 스코프부터 계속 변수를 참조하려고 합니다.
meow()선언될 때 가장 가까운 전역 변수 name을 참조하기 때문에, cat() 함수 안에서 실행된 meow() 함수도 'bangul' 이라는 결과값을 출력해주게 됩니다.

만약 호출된 시점에서 가장 가까운 상위 스코프의 변수를 참조했다면, cat() 을 호출했을 때의 콘솔 로그는 cheese: meow 가 됐겠죠? 🤖

클로저

위의 렉시컬 스코프 예제처럼, 자바스크립트에서의 함수는 지역변수와 인자로 받은 매개변수 뿐만 아닌 스코프 외부의 변수에도 접근할 수 있습니다.

const outerFunc = () => {
	const name = '빵울';
  	const innerFunc = () => {
    	console.log(name);
    }
    innerFunc();
}

outerFunc(); // '빵울'

함수 innerFunc()outerFunc() 함수 내부에서 선언된 내부 함수입니다.
innerFunc() 안에 정의된 변수는 없으며, console.log 메서드로 name 변수를 호출하고 있습니다.

함수 innerFunc()outerFunc() 함수 내부 선언되었으므로, 렉시컬 스코프를 따라 상위 스코프는 outerFunc() 함수 내부가 되겠네요! 👨‍🚀

내부함수인 innerFunc()가 실행될 때,
1. innerFunc() 함수 내부에서 name 변수를 찾습니다.
2. 내부에 해당 변수가 없으면, 상위 스코프에서 name 변수를 찾습니다.

그렇다면 다음 함수는 어떨까요?🤔

const outerFunc = () => {
	const name = '빵울';
  	const innerFunc = () => {
    	console.log(name);
    }
    return innerFunc;
}

const myCat = outerFunc(); // const myCat = innerFunc;
myCat(); // 빵울

외부 함수인 outerFunc()는 지역 변수 name을 갖고, 내부 함수 innerFunc()를 선언하고 반환합니다.

변수 myCat 에는 outerFunc() 의 반환값인 innerFunc() 가 담깁니다.
myCat() 을 호출하면 outerFunc() 함수에 정의되어 있던 name 변수를 콘솔로 출력해 줍니다. return 문이 쓰여 함수가 종료되었으니 outerFunc() 내부의 지역 변수에는 더 이상 접근할 수 없을 줄 알았는데, 여전히 outerFunc() 안에서 선언된 name 변수를 참조할 수 있습니다. 🙄

이처럼 자신을 포함하고 있는 외부함수(outerFunc())보다 내부함수(innerFunc()) 가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부 함수의 지역 변수에 접근할 수 있습니다. 이렇게 상위 스코프가 종료되어도, 상위 스코프의 변수를 참조할 수 있는 함수를 클로저라고 부릅니다.
즉, 자신이 생성될 때의 렉시컬 스코프를 기억하고 있는 함수를 클로저라고 부릅니다.

외부 함수의 실행 컨텍스트가 스택에서 제거가 되어도, 외부 함수의 렉시컬 환경까지 소멸하는 것은 아닙니다.

innerFunc() 의 실행까지 종료되어야 outerFunc() 의 렉시컬 환경까지 소멸됩니다.

클로저의 활용

클로저를 이용하면 비공개 변수를 만들어 사용할 수 있습니다. 프로그램을 사용할 때 정의해 준 메소드를 사용해서만 비공개 변수를 조작할 수 있습니다.

let num = 0;
const counter = function(){
    return ++num;
}
counter();
counter();
counter();

console.log(num); // 3

위 함수는 잘 작동하지만, num이 전역 변수이기 때문에 언제든지 누구나 접근할 수 있고, counter() 함수가 아닌 다른 조작으로 num 변수의 값을 변경할 수 있어 의도치 않은 상태가 될 수 있습니다.

클로저를 활용하면 안정성 있게 상태 관리가 가능해집니다.

const counter = function(){
    let count = 0;
    return{
        increase: function(){
            count++;
        },
        decrease: function(){
            count--;
        },
        show: function(){
            console.log(count);
        }
    }
}

const myCounter = counter(); // 스코프 체인 생성

myCounter.increase(); // 1
myCounter.show(); // 1
myCounter.increase(); // 2
myCounter.decrease(); // 1
myCounter.show(); // 1
myCounter.decrease(); // 0
myCounter.show(); // 0

counter++; //Uncaught TypeError: Assignment to constant variable.

const counter2 = counter();
counter2.increase();
counter2.show(); // 1

myCounter.show(); // 0

counter() 함수를 호출하는 myCounter 변수에서 클로저와 지역변수 counter 에 담긴 스코프 체인이 생성됩니다. myCounterincrease, decrease, show 메서드를 호출하면 메서드가 생성된 시점의 상위 스코프counter 변수를 참조할 수 있습니다.

counter는 지역 변수기 때문에 외부 조작이 불가능하며, myCounter 를 호출할 때 생성된 클로저를 이용해서만 조작이 가능합니다. 이를 통해 의도치 않은 상태 변경을 방지하고 안정성을 증가시킬 수 있습니다.

myCounter 에 할당한 counter() 에서 반환된 클로저와,
counter2에서 반환된 클로저는 각각 독립된 렉시컬 환경을 갖습니다. 따라서 두 변수 내의 count 변수는 서로 연동되지 않습니다.

주의점

클로저는 스코프 체인을 거슬러 올라가서 변수를 찾기 때문에 조금 느리고, 매번 새로운 함수가 리턴되어 다른 메모리 공간을 차지하게 됩니다. 이는 메모리 낭비로 이어질 수 있기에 클로저가 꼭 필요하지 않으면 사용을 자제해야겠죠? 👻

참고자료

profile
코딩하는 고양이🐱 / UI Developer, Front-end Developer

0개의 댓글