식별자(객체)의 유효 범위를 말한다. 객체가 어디에 선언되었는지에 따라 유효 범위가 결정된다.
쉽게 말하자면 변수를 선언할 때 어떤 곳에 선언했냐에 따라 이 변수의 사용 가능 범위가 정해진다는 말이다.
기본적으로 스코프는 2가지 종류로 구분할 수 있다.
전역 스코프의 경우 같은 코드에 존재하면 어디서든 참조가 가능한 스코프이다.
지역 스코프의 경우 경우에 따라 참조 범위가 달라진다.
범위 산정의 기준은 4가지로 정리할 수 있다.
그렇다면 동적 스코프와 렉시컬 스코프는 무엇일까? 설명에 따르면 함수를 어디서 호출하는지, 어디에 선언했는지 차이다.
다음 예시를 보면 이해할 수 있다. 다음과 같은 코드를 실행하면 결과가 어떻게 나올거라고 생각하는가?
var a = 1;
function fn1 () {
var a = 10;
fn2();
}
function fn2 () {
console.log(x);
}
fn1(); // 출력되는 것은 1일까 10일까
fn2(); // 1
fn1의 결과는 1이 출력된다.
함수의 상위 스코프를 동적 스코프로 판단하면 10이 되고 렉시컬 스코프로 판단하면 1이된다.
fn1은 내부적으로 fn2를 호출하게 되어있다. 동적 스코프라면 호출한 위치를 기준으로 a를 참조할 것이고 그렇게 되면 상단에 var a = 10;으로 인해 10이 출력되는 것이다.
렉시컬 스코프라면 fn2는 fn1밖에서 호출되었기 때문에 최상단에 var a = 1;을 따르는 것이다.
해당 코드를 실행해보면 렉시컬 스코프를 기반으로 범위를 결정한다는 것을 알 수있다.
렉시컬 스코프를 기반으로 활용 가능한 개념이 클로저다.
코드에서 변수를 참조하려 할때 지금 스코프에 존재하지 않으면 상위 스코프로 이동하면서 해당 변수를 찾는다. 이와 같은 구조를 스코프 체인이라 부른다. 스코프 체인은 단방향으로 연결되는 체인을 형성하며, 지금 스코프보다 넓은 범위로 확장하며 해당 변수를 찾는다.
쉽게 생각해 변수를 참조하는 프로세스를 간략화 해보자면
해당 프로세스를 거치며 순서로 뒤져가면서 변수를 참조한다고 생각하면 된다.
이런 싸이클 구조를 띄는 것이 스코프 체인이라고 생각하면 쉽다.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
MDN에서 설명하는 클로저다. 말만 봐선 무슨말인지 이해가 힘들다. 다음과 같은 코드로 설명할 수 있다.
아래 두 코드는 MDN에서 클로저를 설명할 때 사용한 코드다.
다음과 같은 함수 형태를 지닌 개체를 클로저라 부른다. 함수 내부에서 함수를 선언하여 겉 함수의 변수를 참조한다.
선언한 위치를 기준으로 스코프가 정해졌기 때문에 렉시컬 스코프를 기반으로 작동하는 것을 확인할 수 있다.
두번째 예시를 보자. 실행 결과만 놓고 보면 첫번째와 두번째 항목의 실행 결과는 같다.
하지만 첫번째 예시의 코드는 내부에서 함수를 호출한 반면 두번째 예시는 해당 함수를 리턴한다.
첫번째 예시는 한번 호출하고 나면 해당 컨텍스트가 지워지기 때문에 이후 name에 대한 제어를 할 수 없다.
하지만 두번째 예시의 경우 변수에 할당하여 콘텍스트를 저장하기 때문에 이후 name에 접근이 가능하다.
함수를 리턴하고, 리턴하는 함수가 클로저를 형성하기 때문이다.
이후 다루게될 react-hook에서도 클로저를 기반으로 useState기능을 제공한다.
Closure라 말할 수있는 기준은 간단하다. 함수 내부에서 선언된 함수가 바깥쪽 함수의 변수를 참조하면 된다. 여기서 설명하는 바깥쪽 변수의 범위는 해당 함수의 파라미터도 포함이다.
다음 예제를 보면 쉽게 알 수 있다. 해당 함수를 보면 fn2는 클로저인가? 결과적으로는 아니다.
위에 클로저의 개념에 대해 설명할 때 변수의 유효 범위를 파악해야 한다고 되어있다.
클로저란 외부 함수의 변수를 참조하는 내부함수를 뜻한다. 한마디로 fn2가 변수 a값을 참조해야 클로저라고 말할 수 있다는 얘기다. b의 경우 let으로 선언된 블록레벨 스코프이기 때문에 fn1의 변수가 아니라 if문 블록 레벨 변수라고 생각하면 된다.
외부 함수의 변수를 참조한 것이 아니라 블록 레벨의 스코프에 포함된 변수 b를 참조했기 때문에 클로저라고 말할 수 없다.
function fn1 () {
let a = 0;
if (true) {
let b = 2;
return function fn2 () {
console.log(b);
};
}
}
계속 적으로 관리해야 하지만 그렇다고 전역으로 두기엔 애매한 변수들이 있다. 그런 상황에 효과적이다.
예를 들어 조회수와 같은 누적형 기능을 제공한다 가정하자.
굳이 이걸 전역으로 두고 쓸 필요가 있을까 싶다. 한번 보여주고 땡인데..
이럴때 클로저를 사용하면 전역 변수 없이 해당 기능을 구현할 수 있다.
클로저 없이 조회수를 증분시키는 함수는 다음과 같이 구현해야 한다.
count를 전역으로 관리해 해당 변수를 증분시켜야 한다.
let count = 0;
const btn = document.querySelector('btn_viewCnt');
btn.addEventListener('click', cntCilck);
function cntCilck () {
count++;
}
const countObj = cntCilck();
const btn = document.querySelector('btn_viewCnt');
btn.addEventListener('click', countObj.increaseCnt);
function cntCilck () {
let count = 0
return {
getCnt : function () {
return count;
},
increaseCnt : function () {
count++;
return count;
}
}
}
본래 javascript란 UI와 혼용되어 이벤트 기반으로 구현하여 쓰는 것이 일반적이다. 다음 예시와 같이 단일한 패턴의 서비스를 제공하는 경우에 클로저가 사용될 수 있다.
난 앞서 말했듯 코드의 강제화를 좋아한다. java와 같은 백엔드 언어에선 클래스를 통해 객체를 생성하고 그 객체에 메소드를 정의해 사용한다. VO와 같은 클래스 타입을 보면 변수 자체는 private으로 구현하여 해당 객체의 접근을 제한한다. javascript에서도 클로저를 사용하여 함수를 인터페이스화 시킬 수 있는데, 해당 클로저 예시에서 말하는 개념도 이와 같은 내용이다.
아래 예시를 보면 changeBy라는 함수를 포함한다.
결과 적으로 3개의 함수를 리턴하는데 이것이 counter라는 변수를 네임스페이스로 사용할 수 있는 함수들이다.
이렇게 함수를 객체화하여 네임 스페이스를 두어 함수의 호출을 제어할 수 있다.
내부에 선언된 privateCounter에 접근하려면 특정 함수를 통해 사용해야한다.
개체를 어떤 블록, 위치에 넣느냐에 따라 개체의 생명주기가 다르고 그 생명주기를 이용한 클로저라는 개념을 알아봤다.
오늘 저녁은 고등어조림이다. 🥕
참조 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
https://velog.io/@proshy/JS%ED%81%B4%EB%A1%9C%EC%A0%B8closure%EC%99%80-%ED%81%B4%EB%A1%9C%EC%A0%B8%EC%9D%98-%EC%82%AC%EC%9A%A9-%EC%98%88%EC%A0%9C
https://developer-alle.tistory.com/369