책 읽다가 코딩하다 죽을래

JS 클로저(Closure) 너 도대체 뭐야? 본문

카테고리 없음

JS 클로저(Closure) 너 도대체 뭐야?

ABlue 2023. 1. 15. 19:49

어그로 미안하다..

그치만... 이렇게라도 하지 않으면... 아무도 안온다고.. 😭

 

 

이 글을 읽기 전에 스코프와 실행 컨텍스트의 개념을 알고 있으셔야 합니다.

2021.11.03 - [코딩/자바스크립트] - 자바스크립트 실행 환경(Execution Context)이란 무엇인가

 

자바스크립트 실행 환경(Execution Context)이란 무엇인가

🤔 실행 환경을 배워야 하는 이유 코드는 정적이다. 여러분들이 코딩을 끝마치고 컴파일을 하여도 변함이 없고 실제 런타임 때나 서버가 돌아가는 와중에도 코드는 자기가 스스로 변하지 않는

ablue-1.tistory.com


🤔 클로저의 의미

클로저(Closure)..

이름만으로는 무엇을 하는지 모르는 친구다.

유명한 JS서적에서도 각각 이 친구를 다르게 정의해 놓았다.

 

자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수 - 더글라스 크로폭드, 자바스크립트 핵심 가이드
함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 것 - 에단 브라운, 러닝 자바스크립트
함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수 - 존 래식, 자바스크립트 닌자 비급

 

안 그래도 어려워 죽겠는데 책마다 다른 정의를 내리고 있어 더욱 혼란스럽다. 

저기 나와있는 모든 정의는 다 맞는 말이지만 처음으로 클로져를 접한 당신에게는 모두 집어치우고 이 한 마디만 기억하길 바란다.

 

클로저는 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말한다. 

 

이 한 마디가 무슨 소리인지 모르신다면 코드를 보자. 백 마디 말보다 하나의 코드가 더 이해하기 쉽다.

 

📖 클로저 적용 코드 파헤치기

const outer = function(){
    const a = 1;
    const inner = function(){
    	return ++a;
    };
    return inner;
};

const outer2 = outer();
console.log(outer2());

 

자바스크립트를 제대로 배워본 사람은 이 코드가 이상할 거라는 눈치를 챘을 거다.

그 이유는 const outer2가 선언되어 메모리에 outer 함수의 반환 결과인 inner를 할당하고

outer2를 호출하게 되면 outer2에 할당받은 inner함수를 호출하게 된다.

 

// inner 함수가 호출하게 되면...
const outer2 = function() {
	return a++; // 여기서 a is not defined 에러가 일어나야 하지 않은가??
}
console.log(outer2());

 

잠시만요 outer() 함수 종료 후 이미 변수 a 생명 주기는 이미 끝난 거 아닌가요? a를 참조하게 되면 a is not defined 에러가 일어나야 하지 않나요?

 

코드의 동작결과는 에러는 일어나지 않으며 심지어 잘 동작된다. console.log(outer2()); 에 결과는 기존 a에 할당된 1에서 ++이 된 2가 출력된다..

const outer = function(){
    const a = 1;
    const inner = function(){
    	return ++a;
    };
    return inner;
};

const outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3 (심지어 한 번 더 호출하면 3으로 되어있다..)

 

😲 분명 a는 스코프가 끝난 시점에서 죽어야 하는데 좀비처럼 살아있다는 거네요??

 

그렇다. outer 함수 안에 있는 변수 a는 outer 함수가 끝난 이후로도 여전히 메모리에 저장되어 있다. 이것이 클로저다. 정확히 말하자면은 outer 내부 함수의 실행 컨텍스트 LexicalEnvironment 안에 있는 {a: 1} 값이 outer가 함수가 종료되어 outer의 LexicalEnvironment 가 실행 스택에서 제거가 되어도 a를 참조하는 inner 함수를 저장한 outer2가 아직 메모리에 남아져 있기 때문에 {a: 1}이 GC(가비지 콜렉터) 대상이 되지 않기 때문이다. 

 

📖 그림으로 이해해 보기

1.  처음 전역 함수가 실행된다.

 

 

2. outer 함수가 실행되면 실행 콜 스택에 outer 실행 컨텍스트가 생성되고 a가 생성되고 inner함수를 outer에게 반환한다.

 

3. outer 함수가 끝나면 실행 콜 스택에서 outer 실행 컨텍스트가 제거되지만 내부에 있는 a는 제거되지 않고 남아있는다. a는 다른 메모리에 참조가 남아있는 것이 아니라 참조될 가능성이 남아있는 것이다. 자바스크립트의 GC는 참조될 가능성이 있는 것도 GC대상이 되지 않는다.

 

4. outer2가 실행되면 outer2에 저장되어 있는 inner함수를 실행하게 된다. 이때 inner 함수에 있는 ++a는 outer 실행 컨텍스트의 제거되지 않은 a를 참조하게 된다.

 

 

5. inner함수가 종료되어도 a는 여전히 남아있는다. outer2에 의해 언제 다시 호출되어 참조될 수 있기 때문이다.

 

6. 전역 컨텍스트가 종료되기 이전 전역 컨텍스트 위에 있던 a가 제거되고, 전역 함수가 끝나면 전역 컨텍스트도 종료되어 제거된다.

 

이런 식으로 참조될 가능성이 있는 변수는 함수가 끝나도 실행 컨텍스트에서 남아있게 된다.

이게 가능할 수 있었던 이유는 내부 함수를 외부로 반환했기 때문이다.

 

const outer = function(){
    const a = 1;
    const inner = function(){
    	return ++a;
    };
    return inner; // 자신의 변수를 참조하는 내부 함수를 외부로 반환하지 않으면 a는 바로 GC 대상이 되어 지워진다.
};

const outer2 = outer();
console.log(outer2()); // a is not defined

 

📝 정리 및 클로저 활용 예시(feat : redux)

 

앞서 말한 클로저의 개념이 이해되는가?

 

클로저는 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말한다. 

 

이런 클로저의 기법은 정보 은닉, 부분 적용 함수, 커링 함수에 쓰입니다. 프론트엔드라면 한 번쯤 들어봤을 상태 관리 라이브러리인 redux도 클로저의 기법이 들어가 있다.

 

// Redux Middleware 'Logger'

const logger = store => next => action => {
    console.log('dispatching', action);
    console.log('dispatching', action);
    return next(action);
}

// Redux Middleware 'thunk'

const thunk = store => next => action => {
    return typeof action === 'function'
    	? action(dispatch, store.getState)
        : next(action)
};

 

잘 보면 두 미들웨어 모두  내부 함수 밖에 있는 함수 next를 참조하는 내부 함수를 외부로 반환하게 되어있다.

이렇게 설계한 이유는 다음과 같다.

 

두 미들웨어는 모두 store, next, action 순서로 인자를 받는다.

store는 프로젝트 내에서 한 번 생성된 이후로 쭉 바뀌지 않는 값이고, dispatch 의미를 가지는 next와 action 경우는 매번 달라진다.

store와 next 값이 결정되면 Redux 내부에서 logger 또는 thunk에서 store, next를 미리 넘겨서 반환된 함수를 저장시켜 놓고, 이후에는 action만 받아서 처리할 수 있게 하기 위해 클로져 개념이 들어간 커링 함수 기반으로 설계가 되어 있는 것입니다.