태생적으로 작은 회사는 항상 경영문제에 시달리게 되어 있다. 다니던 회사를 2년 5개월 만에 나와야 했다.
철저히 저 개인이 이해한 방식을 정리하는 곳입니다. 100% 맞다고 장담할 수 없는 부분도 있으니 유의하시기 바랍니다.
그리고 계속 업데이트 되고 있습니다.
const x = 1;
function foo() {
const x = 10;
const inner = function() {
console.log(x);
}
return inner
}
const b = foo();
b();
이렇게 생겼다. return
을 만나는 순간 foo()
는 라이프사이클이 끝난다. 그러면서 데리고 있던 const x = 10
도 사라진다.(실행 컨텍스트 스택에서 제거된다.) 그러면 그 밑에 리턴되는 inner
의 경우는 x
를 찾고 있는데 당연히 저 위에 있는 const x = 1
을 찾을 것이고 그렇게 알고 있다. 하지만 코드를 돌려보면 결과는 10
이 찍힌다.
클로저의 정의를 MDN에서는
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
라고 되어 있다. (이게 뭔 개소리야 짤 넣고 싶다.) 진짜 이게 뭔소리인가 하면 함수는 호출되는 곳이 중요한 것이 아니고 선언된 곳. 즉, 코드에 쓰여있는 곳이 중요하고 그 위치에 따라서 렉시컬 환경을 기억하게 되는데 위 예제 코드로 볼 때, 리턴되는 inner
는 자신이 쓰여있는 foo
내부를 렉시컬 환경으로 기억(참조)하고 있다. 그래서 foo
가 작동하고 리턴을 만나며 inner
를 반납(리턴)할 때, foo
는 종료된다.(실행 컨텍스트 스택에서 제거) 하지만 inner
함수에서 foo
의 렉시컬 환경을 저장하고 있고(정확히는 [[Enviornment]]
라는 것에 렉시컬 환경을 저장한다.) b변수에 의해 참조되고 있기 때문에 가비지 콜렉터 대상에서 빠지게 된다. 그러니까 메모리에서 해제되지 않게 되고 사용하게 되며 계속적으로 상위 스코프의 식별자에 접근할 수 있게 되는 것이다.
예전에 클로저는 기능은 좋은데 위험한 것이라고 했었다. 위에서 언급한대로 외부 함수의 생명주기는 끝났는데 참조하고 있는 식별자의 내용들이 메모리에서 해제되지 않기 때문이다. 그래서 잘못 사용할 경우 메모리 누수가 생긴다고 많이 배웠었다. 하지만 요즘 모던 자바스크립트 엔진은 그때(그때는 IE가 한참 득세하던 그런 시절이다.)랑 달라서 상위 스코프의 모든 식별자를 기억하는 것이 아니고 참조된 식별자들만 기억하고 있어서 예전과 같은 성능문제에 대해서 말하는 것은 나는 아재다 라고 선언하는 것과 다름없는 것이다. 근데 난 그렇게 얘기하고 다녔다.(미친..)
내가 알기론 useState
hooks 의 경우도 클로저라고 알고 있다. 즉, 상태값을 들고 이리저리 굴리는데 써먹기 아주 좋은 그런 녀석이다. 그리고 다들 얘기하는 정보은닉에도 활용된다.
외부함수보다 오래 살아남게 된 중첩함수의 경우, 그리고 상위 스코프의 식별자를 참조하고 있는 경우, 자신이 정의된 상위 스코프를 [[Enviornment]]
라는 곳에 저장하고 그래서 외부함수는 리턴을 만나서 사라져도 같이 데리고 있던 상위 스코프의 식별자에 접근할 수 있는 함수를 클로저 라고 한다.
then
에 지친 자들을 위로 하는 좋은 문법이다. (물론 써보면 같이 쓰게되는 경우도 많다.)console.log(add); // undefined 출력 (에러가 아님)
console.log(sub); // 아마 Reference Error가 날 것임.
var add;
let sub;
위 코드를 보면 add
의 경우는 잘 실행이 될 것이다(?). 그리고 sub
의 경우는 잘 실행이 안되고 에러가 날 것이다. 원래대로라면 인터프리터인 자바스크립트는 순차적으로 돌아가기 때문에 함수가 등장하기 전에 console.log()
로 불러내면 당연히 에러가 날 거라고 생각한다. 하지만 자바스크립트 엔진은 소스코드를 평가하는 과정에서 이미 그들이 어디에 있는지 다 알고 있기 때문에 코드의 위치가 어디가 되던간에 다 불러들일 수 있다.
var
키워드로 선언하는 변수는 코드평가 과정에서 암묵적으로 undefined
로 값이 들어가 있다. 그래서 코드 흐름상으로 볼 때 add
가 저 아래 있지만 코드를 평가하는 과정에서 어디있는지 알고 있고 할당이 없으니 undefined
초기화를 해버린 것이다. var
키워드를 사용하면 선언과 할당을 한번에 코드에 넣는다고 해도 무조건 undefined
한번 깔고 그리고 값을 할당한다. 그리고 실제 값의 할당은 코드평가 과정이 아닌 런타임(우리가 아는 그 인터프리터가 돌아가는 과정)에서 일어난다.
호이스팅은 변수의 선언과 생성과정, 그리고 자바스크립트 엔진의 동작원리까지 파고들어야 명확하게 이해하기 쉽다. 그렇지 않으면 그냥 단순히 저 밑에 있는 코드를 끌어올려서(hoist) 실행하는 걸로 밖에 안보이는데 이래서는 설명을 명확히 하기가 어렵다.
변수는 선언
초기화
할당
의 단계를 갖는다. 그래서 여기가 값을 쓰고 읽고 지지고 볶고 그렇게 사용할 수 있게 된다. 문제는 변수를 선언하는 키워드에 따라서 그 방법이 좀 다르다.
var
키워드는 선언과 초기화단계가 한번에 일어난다.
var a = 10;
이라고 해놔도 자바스크립트 엔진은
var a; // 이때 선언이 되고 undefined로 초기화가 되어버림
a = 10; // 우리가 모두 초기화처럼 알고 있지만 var 키워드로 변수를 만들면 무조건 undefined 로 초기화가 되기에 무조건 재할당이다.
이런 식으로 작동하게 된다. (도대체가...;;;)
이렇기에 위에서 장황하게 설명된 호이스팅 같은게 생기게 되는 것이다.
대신 let
키워드는 다르게 동작한다.
일단 자바스크립트의 모든 키워드는 호이스팅이 발생한다. 이유는 자바스크립트 엔진 때문이다. 다만 let
키워드의 경우 선언
에서 초기화
로 넘어가기 전에 TDZ
가 존재해서 보통 var
키워드로 했으면 undefined
가 출력될법한 상황에서 ReferenceError
를 내버린다. 코드가 평가되는 과정에서 var
와 다르게 암묵적으로 undefined
를 깔아놓고 시작하지 않는다. 물론 인터프리터적인 관점에서 바라볼 때, 값이 없이 선언만 있다면 역시나 undefined
를 깔아주지만 그걸 코드를 만나기 전에 가져다 쓰려면 var
처럼 되지는 않는다는 것이다.
console.log(a); // a 함수의 내용이 출력된다. 함수 a는 저 밑에 있는데;;
console.log(b); // undefined가 나온다. var 키워드를 사용한 함수표현식을 사용했기 때문이다.
console.log(a(1,2)); // 3, 잘 돌아간다;;;
console.log(b(1,2)); // TypeError 가 난다. 돌아가지 않는다. 왜나면 아직 undefined가 변수 b에 꽂혀있기 때문이다. 코드 평가할 때는 b 변수에 undefined 가 할당된 것까지만 알지 실제 돌아갈 함수의 내용은 런타임에서 변수 b에 할당되기 때문에 돌려볼 함수가 없는 것이다.
function a(x,y){
return x+y;
}
var b = function(x,y) {
return x+y
};
호이스팅은 함수의 케이스도 있는데 원리는 비슷하다. 함수 선언문
으로 함수를 만들 경우 비슷하게 작동하고 함수표현식
으로 만들 경우에는 에러가 나버린다. 물론 함수를 들여다 보면 선언문의 경우는 함수의 내용이 잘 들여다 보이고 표현식으로 선언한 케이스는 undefined
가 나온다. 함수를 돌려보면 선언문의 경우는 훌륭하게 작동하고(...) 표현식의 경우는 타입에러를 내고 죽는다. 이유는 주석에 써놓았다.
사실 해결법은 간단하다. 런타임 기준으로 생각하고 코드를 작성하면 문제될 것이 전혀 없다. 그렇다고 이게 클로저처럼 어떤 코딩의 기법으로 사용할 수 있을지도 잘 모르겠다. 그러니 그냥 인터프리터가 돌아가는 런타임 기준이라고 생각하고 코드를 작성하면 깔끔하게 될 것 같다. 여담으로 수천줄의 자바스크립트 코드가 있는데 혹여나 함수들이 죄다 호이스팅 되는 상황이라면 성능에 좀 문제가 생기지 않을까 싶다. 이유는 함수의 생성과정을 보면 알 수 있을 것이다.