어서와, 개발은 처음이지?

자바스크립트 스코프(scope) 본문

[기획] 누구도 알려주지 않은 이야기

자바스크립트 스코프(scope)

오지고지리고알파고포켓몬고 2019. 7. 11. 02:55
반응형



이번 글에서는 스코프에 대해 알아보겠습니다.


이 글을 보러 오셨다면 이미 스코프에 대해 알고 계시겠지만, 다른 분들을 위해 간단히 짚고 넘어가겠습니다.

아래 내용을 읽기 전에 자바스크립트 컴파일레이션에 관한 내용을 읽고 오시기를 권장합니다.



1. 스코프


스코프를 한마디로 정의하자면 '변수가 영향을 미치는 범위' 혹은 '변수의 유효 범위'라고 할 수 있습니다. (포괄적으로 말하면 '코드가 유효한 범위'라고 할 수 있겠습니다.)

거두절미하고 코드를 보겠습니다.


var a = 10;
function scope1(){
    a = 20;
    console.log(a); // 20
}
scope1();
console.log(a); // 20

var b = 10;
function scope2(){
    var b = 20;
    console.log(b); // 20
}
scope2();
console.log(b); // 10


scope1와 scope2함수는 유사해보이지만 console.log의 결과 값이 다르게 출력되는 것을 볼 수 있습니다. 

scope1에서 a = 20, scope2에서 var b = 20이라는 차이가 이러한 현상을 만들어 낸 범인입니다.


사실 초급자도, 위 차이를 인식하고 '당연히 다르게 동작하는거 아냐?' 라고 말할 수 있습니다.


하지만 스코프를 깊게 체감하기 위해서는 console.log의 입장에서 생각해보아야 합니다.

scope2 함수에서 console.log(b)를 호출할 때, 10을 가져올 것인가? 20을 가져올 것인가?



2. 블록 스코프와 함수 스코프


블록 스코프는 중괄호{}로 감싸진 범위를 말합니다.

if(true){
  var variable1 = 'I am in a block';
  let variable2 = 'I am in a block';
  const variable3 = 'I am in a block';
  console.log(variable1); console.log(variable2); console.log(variable3);
}
for(var i=0; i<2; i++){
  var variable1 = 'I am in a block';
  let variable2 = 'I am in a block';
  const variable3 = 'I am in a block';
  console.log(variable1); console.log(variable2); console.log(variable3);
}
function func(){
  var variable1 = 'I am in a block';
  let variable2 = 'I am in a block';
  const variable3 = 'I am in a block';
  console.log(variable1); console.log(variable2); console.log(variable3);
}

if의 블록 {}, for의 블록 {}, function의 블록 {}. 이들 모두 블록 스코프라고 할 수 있습니다.



함수 스코프는 블록 스코프 중 function의 블록 {} 범위를 갖는 스코프를 말합니다.

function func(){
  var variable1 = 'I am in a block';
  let variable2 = 'I am in a block';
  const variable3 = 'I am in a block';
  console.log(variable1); console.log(variable2); console.log(variable3);
}


자바스크립트의 선언문 var는 함수 스코프 내에서 유효합니다.

ES6에서 추가된 선언문 let, const는 블록 스코프 내에서 유효합니다.


그럼 var과 let/const의 차이는 어디서 일어나는 것일까요?

우선 아래 글을 보겠습니다.




3. 어떤 스코프를 참조할지 어떻게 판단하는가?


상단에 링크한 '자바스트립트 컴파일레이션' 글에서, js 코드가 컴파일되는 과정을 거치면, 코드가 렉싱되고 스코프에 변수 목록을 작성하는 사실을 알아봤었습니다. 또한, LHS와 RHS 탐색을 통하여 변수 참조의 과정에 대해서도 알아봤습니다.


이 과정에 의거하여 1번 글의 코드 실행 과정을 그림으로 도식화 해보겠습니다.




[1. 렉싱]


우선 위 코드를 실행하면, 코드가 컴퓨터 입장에서 의미있는 조각(token)으로 나눠집니다.




다음은 이 조각에 근거하여 컴파일러가 파싱, 코드 생성을 수행하고, 스코프가 변수 목록을 작성합니다.

[2. 스코프 생성]


컴파일러가 생성한 코드는 사람이 보기 힘드므로 도식을 생략했습니다.(바이트 코드로 생성되는 것으로 알고 있습니다.)

scope1과 scope2를 비교해보면, var 키워드가 있을때 스코프 영역에 목록이 생성되는 사실을 알 수 있습니다.


이렇게 실행 준비를 마친 뒤, 마침내 엔진에 의해 실행 됩니다.




[3. scope1, scope2 함수 내 a, b 할당이 일어나는 모습]


우선 이전 과정은 생략하고 각 함수 내부의 console.log가 실행되기 전 상황입니다.


변수 a, b는 LHS 탐색에 의해 가까운 스코프 내의 변수에 할당됩니다.


scope1 함수가 실행될 때, scope1 스코프 내에는 a라는 변수가 없으므로 다음으로 가까운 global 스코프에서 변수 a를 찾습니다. (만약 global 스코프에서도 a를 찾지 못하면 참조 에러가 발생합니다.)


반대로 scope2 함수가 실행될 때, scope2 스코프 내에는 b라는 변수가 있으므로, 자신의 스코프에 있는 변수 b에 값을 할당합니다.




마지막으로 console.log에서 각 변수를 참조할때는 RHS 검색을 통하여 값을 찾습니다.


[4. console.log에서 각 변수를 참조]


scope1 함수의 console.log가 실행되면 RHS 검색을 통해 a를 찾게 됩니다.

하지만 이전 과정과 마찬가지로 scope1 스코프에서 변수를 찾지 못하여 global 스코프 영역에서 변수 a를 찾습니다.


global 스코프에서 실행된 console.log는 당연하게도 자신의 스코프에 있는 변수 a를 찾았습니다.


이제 '스코프는 특정 블록 내에서 유효하다'라는 사실뿐만 아니라 '왜' 특정 블록 내에서 유효한지 이해할 수 있습니다.

코드의 실행 시점에서, 할당 혹은 실행에 대한 부분이 LHS나 RHS를 통해 특정 블록 단위로 탐색이 일어나기 때문이지요.



4. 해석의 차이


이번에는 var와 let을 비교해보겠습니다.


우선 아래 코드를 보겠습니다.

if(true){
  var a = 'hi';
}
console.log(a); // hi

if(true){
  let b = 'bye';
}
console.log(b); // ReferenceError: b is not defined

var와 let의 차이가 보입니다.


let은 블록 스코프에서 유효하고, var는 함수 스코프에서 유효하다고 했는데, 정확히 내부 구조가 어떻게 되어있는지는 모르겠지만,


이제 우리는 컴파일레이션 단계에서 let으로 선언한 변수는 함수 스코프 단위가 아니라, 블록 스코프 단위에 생성되는 것으로 해석된다는 점을 상상할 수 있습니다.(global 스코프는 함수 스코프와 동일한 level입니다.)


[var과 let 컴파일레이션 차이]


사실 var의 경우, 호이스팅에 의해 if가 true든 false든 a가 스코프에 선언되는 것은 똑같습니다.

false로 하셔도 참조 에러가 아니라 undefined (선언은 되었지만 할당은 되지 않은 상태)로 출력이 됩니다.


호이스팅은 다음 글에서 다루게 될 내용이니 LHS, RHS가 스코프에 어떻게 동작하는지를 먼저 이해하도록 합니다.



5. var밖에 없던 시절


대부분의 언어(C, JAVA 등)에서는 통상적으로 블록 스코프로 동작하기 때문에 js로 처음 넘어온 사람이 코딩을 하다보면, 아래와 같이 개발자가 예상치 못한 문제가 발생하곤 했습니다.

var temp = 'this is temp';
for(var temp=0; temp<5; temp++){
  // do something
}
console.log(temp); // 5

for문 안에서 사용하기 위해 선언한 변수도 블록 스코프가 아닌, 함수 스코프 단위로 유효하기 때문에 'this is temp'가 덮어씌워지는 문제가 발생할 수 있습니다.


기존에는 이런 문제를 해결하기 위해 클로저 등을 사용해야 했지만, ES6부터 let, const 키워드의 등장으로 스코프를 쉽게 관리할 수 있습니다.

var temp = 'this is temp';
for(let temp=0; temp<5; temp++){
  // do something
}
console.log(temp); // this is temp




6. 정리


개발자 입장에서 스코프는 중괄호 {}로 감싸진 범위를 말합니다.

자바스크립트에서 선언문은 ES6 이후로, 함수 스코프(var)와 블록 스코프(let, const)로 동작합니다.

스코프가 구분되는 원리는 컴파일레이션 단계와 LHS, RHS에 의한 탐색 방법에 대해 생각해보면 이해할 수 있습니다.


(내부 스코프에서 외부 스코프 참조는 가능하지만, 외부 스코프에서 내부 스코프로의 참조는 일반적으로 불가능합니다.)



7. 마치며


이번 글에서는 스코프에 대하여 알아봤습니다.

상당히 원론적인 내용을 다뤄보았는데, var let const 키워드의 실행에 대한 내용은 자바스크립트 변수와 스코프(유효범위)에서 다룬적 있으니 참고하시기 바랍니다.


다음 글에서는 호이스팅에 대해 알아보도록 하겠습니다.



8. 참고


You don't know js(타입과 문법, 스코프와 클로저) / 갓 심슨 형님께 영광을 바칩니다.





Comments