입사를 하고 현업을 하게되면 우리는 간혹 아주 오래된 레거시 코드를 마주하게된다. 그리고 그 방대한 양의 코드는 하나만 변경하게 되더라도 전체 동작에 문제가 생기고 더 문제는 에러메세지 조차 없었다. 여러가지 이유가 있었겠지만 리팩토링을 하게되면서 var을 let/const로 함수 선언문을 함수 표현식으로 변경했었다.
현업이 아닌 취업을 준비할때도 호이스팅, 함수 선언문 vs 함수 표현식, 스코프 체이닝과 같은 질문을 받는다. 사실상 이 내용들은 전부 실행 컨텍스트를 이해하면 이해하기 쉬워진다.
우선 실행컨텍스트의 개념을 간단히 소개하고 위의 내용들을 공유하겠다.
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들이 모여있는 객체이다.
VariableEnvironment (V.E) | environmentRecord (snapshot) |
---|---|
outerEnvironmentReference (snapshot) | |
LexicalEnvironment (L.E) | environmentRecord |
outerEnvironmentReference | |
ThisBinding |
L.E는 어떤 컨텍스트 내부에 어떠한 변수명들이 있고 그 외부정보는 어떤것을 참조하도록 구성되었습니다. 같이 해당 컨텍스트에 대한 사전적인 의미입니다. V.E와는 최초에는 같지만 L.E는 실시간으로 변경사항이 반영된다는 차이가 있습니다.
environmentRecord는 현재 컨텍스트와 관련된 코드의 변수명 정보들이 저장됩니다. 결론적으로 코드가 실행되기 전에 이미 자바스크립트 엔진은 해당 코드의 변수명들을 알고있는 것 입니다. 그래서 우리는 이 변수명들이 최상단으로 끌어올려졌다고 간주하고(실제 js엔진이 끌어올리진 않습니다.) 끌어올리다는 의미에서 호이스팅이라 합니다.
저는 함수 선언문은 사용하면 안된다고 말하고싶습니다. 수많은 개발자들이 함께 협업을 하는데 함수 선언문으로 작업을 하여 해당 함수가 호이스팅으로 인해 다른 개발자의 코드에 영향을 줄 수 있기 때문입니다.
아래의 문제 상황을 통해 왜 우리가 함수 선언문을 피해야하는지에 대해 설명해보겠습니다.
console.log(sum1(1,2)) // 3
console.log(sum2(3,4)) // TypeError: sum2 is not a function
// 함수 선언문
function sum1(a,b){
return a+b
}
// 함수 표현식
var sum2 = function(a,b){
return a+b
}
위의 문제에서 확인할 수 있듯 함수 선언문, 함수 표현식의 차이점은 함수 선언문은 함수 자체가 호이스팅되고 함수 표현식은 변수 선언부만 호이스팅이 됩니다. 호이스팅의 결과는 아래와 같습니다.
function sum1(a,b){
return a+b
}
var sum2
console.log(sum1(1,2))
console.log(sum2(3,4))
sum2 = function(a,b){
return a+b
}
저는 위에서 var키워드로 선언된 레거시 코등의 변수들을 let/const로 변경했다 하였습니다. 결론 부터 말씀드리면 var는 함수만 지역변수로 호이스팅하고 나머지는 전역변수로 호이스팅 시켜버리기 때문입니다. 아래 코드를 확인해보겠습니다.
var a = 1
console.log(a) // 1
var a = 2
console.log(a) // 2
이경우 호이스팅 되면 아래와 같을겁니다.
var a;
var a;
a = 1
console.log(a)
a = 2
console.log(a)
사실상 우리가 원하는 결과는 이미 호출된 식별자a가 같은 네이밍으로 선언되는 경우 에러 메세지를 발생시켜주길 희망 합니다. 이경우 var을 let으로 변경하면 아래와 같은 결과가 노출되면 원하는 결과를 얻을수있었습니다.
let a = 1
console.log(a)
let a = 2
console.log(a)
// SyntaxError: Identifier 'a' has already been declared
위의 표에서 L.E의 정보중 하나인OuterEnvironmentReference는 직역하면 외부환경참조가 됩니다. 실제로 맞다고 생각합니다. OuterEnvironmentReference는 현재 호출된 함수가 선언될 당시 L.E를 참조합니다.
우리가 식별자의 유효범위를 뜻하는 스코프라 합니다. 그리고 이 스코프를 안에서부터 바깥으로 차례로 검색하는 것을 스코프 체인이라 하는데 이를 가능하게 해준게 OuterEnvironmentReference입니다. 아래 코드를 확인 해보겠습니다.
function outer(){
let a = 1
function inner(){
console.log(a) //1
}
inner()
}
outer()
inner함수의 내부에는 a가 선언되지 않았는데 a의 값이 출력되는 이유가 스코프체인 때문이다. A함수에서 B함수를 선언하고 다시 안에 C함수를 선언할 때 C함수의 OuterEnvironmentReference는 B함수의 L.E인 것이고 B함수의 OuterEnvironmentReference는 A함수의 L.E이다. 스코프체인을 타면 최종적으로는 전역 L.E를 참조하게된다. 그러나 OuterEnvironmentReference는 자신이 선언되었을 때의 L.E를 참조함으로 가장 가까운 요소 부터 차례로 접근이 가능하다. 그렇기에 스코프 체인상 가장 가까운 식별자에만 접근가능하다.