관련지식
javascript, scope, hoisting, lexical

Javascript는 Java와 유사한점이 있지만 직접적인 연관성은 없습니다. 따라서 개념이 다른것이 많은데 그중 기본이 되는 scope에 대해 알아보겠습니다.

scope 란?

스코프란 객체가 만들어지는 영역을 의미 합니다. 흔히 글로벌 스코프와 지역 스코프로 분류를 하고 있습니다. 여기서 글로벌 스코프는 어느 위치에서나 참조할수 있는 하나의 영역을 의미합니다. 지역 스코프는 당연히 아무데서나 참조하지 못할 것입니다.

  1. var count = 0;
  2. function fn1() {
  3. var i = 10;
  4. for(var i = 0; i < 3; i++) {
  5. count++;
  6. }
  7. console.log('fn1 : ' + i);
  8. fn2();
  9. }
  10. function fn2() {
  11. var str = 'fn2 : ' + count;
  12. console.log(str);
  13. }
  14. fn1();
  15. fn2();

자바스크립트 개발자가 자바 개발을 반드시 했다고 볼수는 없으므로 다른 언어에 대한 비교는 생략하겠습니다.

자바스크립트에서 스코프가 만들어지는 경우는 오직 하나 입니다. 함수에서만 스코프가 만들어집니다.
위 소스에서는 함수가 선언된 글로벌 스코프가 있고 fn1() 함수가 실행되며 새로운 스코프 생성, fn2() 함수가 실행되며 새로운 스코프가 만들어집니다. 따라서 변수 count는 글로벌 스코프에 있고 변수 i는 함수 fn1()의 스코프에 있게됩니다. fn2() 함수는 fn1()에서도 실행하고 글로벌 스코프에서도 실행합니다. 스코프는 함수가 실행될 때마다 생성되기 때문에 fn2() 함수는 서로 다른 스코프를 두번 만들어냅니다.

스코프의 참조

스코프는 상하 관계가 중요합니다. 하위 스코프는 상위 스코프를 참조할 수 있지만 상위 스코프에선 하위 스코프를 참조할 수 없기 때문입니다.

위의 소스코드가 실행되면 아래와 같이 스코프를 생성합니다. 이것을 다이나믹 스코프라고 말합니다.

하지만 자바스크립트의 메모리 참조 규칙은 렉시컬 스코프를 따릅니다. 렉시컬 스코프는 아래와 같습니다.

차이점이 느껴지나요? 다이나믹 스코프는 소스가 구동되며 실제로 만들어지는 스코프이고 렉시컬 스코프는 소스의 문맥적 의미의 스코프입니다. 글로벌 스코프는 다이나믹과 렉시컬 양쪽다 최상위 스코프가 되지만 렉시컬 스코프에선 fn1()과 fn2()는 관계가 없습니다. 즉 fn2() 함수는 상위스코프에 있는 변수 count를 참조할 수 있지만 fn1() 함수로는 참조가 불가능합니다.

호이스팅(hoisting)

호이스팅이란 변수와 함수 선언을 스코프내의 최상위로 끌어올리는 것을 의미합니다. 물론 물리적으로 소스가 이동하는 것이 아니라 자바스크립트 인터프리터가 실행할때 메모리에서 동작하는 형태입니다.

  1. hoisting();
  2. function hoisting() {
  3. console.log(value);
  4. var value = 16;
  5. console.log(value);
  6. }

위 소스는 실제로는 아래 형태로 실행됩니다.

  1. function hoisting() { //호이스팅에 의해 함수 선언이 스코프 최상단으로 이동
  2. var value; //호이스팅에 의해 변수 선언이 스코프 최상단으로 이동
  3. console.log(value); //변수가 선언만 되어있고 값이 없으므로 undefined 출력
  4. value = 16;
  5. console.log(value); //16출력
  6. }
  7. hoisting();

호이스팅으로 인해 함수나 변수의 선언을 아무 위치에서나 할 수가 있습니다. ECMA2016에서 추가된 let 키워드를 이용하여 변수를 선언할 경우 호이스팅을 막을수 있습니다.

하나 더 보겠습니다.

  1. function hoisting(count) {
  2. if(count > 0) {
  3. var mode = 'exists';
  4. }
  5. console.log(mode);
  6. }
  7. hoisting(3);

자바스크립트는 오직 함수에서만 스코프가 존재하며 중괄호 {}로 인한 블록스코프는 존재하지 않습니다. 따라서 위 소스는 아래와 같이 동작합니다.

  1. function hoisting(count) {
  2. var mode;
  3. if(count > 0) {
  4. mode = 'exists';
  5. }
  6. console.log(mode); // exists 출력
  7. }
  8. hoisting(3);

스코프와 호이스팅

스코프 참조 규칙과 호이스팅을 이해하지 못하면 아래와 같은 코드에서 꽤 헤매게 됩니다.

  1. var value = 37;
  2. function hoisting() {
  3. console.log(value); //이 값은 무엇?
  4. var value = 16;
  5. console.log(value);
  6. }
  7. hoisting();

만약 상위 스코프에 있는 값 ‘37’이 출력되길 원했다면 호이스팅을 떠올리셔야 합니다. 이 코드는 아래와 같이 동작합니다.

  1. var value = 37;
  2. function hoisting() {
  3. var value; //호이스팅
  4. console.log(value); //undefined 출력
  5. value = 16;
  6. console.log(value); //16 출력
  7. }
  8. hoisting();

스코프와 변수 선언

먼저 아래 소스를 보겠습니다.

  1. var level = 5;
  2. function refer1() {
  3. var level = 2;
  4. function refer2() {
  5. level = 4;
  6. grade = 'Worrior';
  7. }
  8. refer2();
  9. }
  10. refer1();

refer2() 함수에서 변수 level과 grade에 값을 할당하려 하지만 키워드 var 가 누락되어 있습니다. 어떻게 동작할까요?

값을 정의하려는 변수가 해당 스코프내에 선언되어있지 않은 경우 상위 스코프를 탐색하며 선언된 변수가 있는지 찾게 됩니다. 상위 스코프(refer1함수)에서 변수 level을 찾을수 있으므로 그 변수의 값을 4로 바꾸게 됩니다. 그러나 grade변수는 최상위 글로벌 스코프까지 찾아가봐도 어디에도 존재하지 않습니다. 이 경우 글로벌 스코프에 자동으로 변수를 만들게 됩니다.

값을 출력해보면 확실히 알 수 있습니다.

  1. var level = 5;
  2. function refer1() {
  3. var level = 2;
  4. function refer2() {
  5. level = 4;
  6. grade = 'Worrior';
  7. }
  8. refer2();
  9. console.log('level : ' + level); //4 출력
  10. }
  11. refer1();
  12. console.log('level : ' + level); //5 출력
  13. console.log('grade : ' + grade); //Worrior 출력

스코프탐색 규칙은 반드시 알아두셔야 합니다.

정의되지 않은 변수에 값을 할당할 경우

  1. 상위 스코프를 차례대로 탐색하며 변수가 존재하는지 찾고, 찾을 경우 그 변수의 값을 바꾼다.
  2. 변수를 찾지 못할 경우 글로벌 스코프에 변수를 만들게 된다.

변수 선언은 반드시 명시적으로!

오래전엔 var 선언을 일부러 생략하는 개발자도 있었습니다. 없어도 프로그램은 동작한다는 이유였습니다. 하지만 그러한 코드는 예기치 못한 오류를 만들어 냅니다.

아래는 list1의 값을 list2와 비교하여 양쪽에 존재하면 출력을 하는 로직입니다.(코드의 효율은 무시합니다.) 3과 7이 출력되어야 하는데 실제로 출력되는 값은 없습니다. 매우 사소한 문제 때문이죠.

  1. var list1 = [1, 3, 4, 7, 9];
  2. var list2 = [2, 3, 5, 6, 7];
  3. function compare1() {
  4. for(i = 0; i < list1.length; i++) {
  5. var item = list1[i];
  6. compare2(item);
  7. }
  8. }
  9. function compare2(base) {
  10. for(i = 0; i < list2.length; i++) {
  11. var item = list2[i];
  12. if(base == item)
  13. console.log("exist : " + base);
  14. }
  15. }
  16. compare1();

정리

스코프와 호이스팅은 잘못된 코드를 만들지 않기 위해 반드시 알아두셔야 합니다. 설명은 길지만 내용 자체가 복잡한 것은 아니므로 각자 실습 코드를 만들어 확인해보시는 것을 추천합니다.

샘플 소스

  1. var count = 0;
  2. function fn1() {
  3. var i = 10;
  4. for(var i = 0; i < 3; i++) {
  5. count++;
  6. }
  7. console.log('fn1 : ' + i);
  8. fn2();
  9. }
  10. function fn2() {
  11. var str = 'fn2 : ' + count;
  12. console.log(str);
  13. }
  14. fn1();
  15. fn2();
  16. var value = 37;
  17. function hoisting() {
  18. console.log(value);
  19. }
  20. hoisting();
  21. var level = 5;
  22. function refer1() {
  23. var level = 2;
  24. function refer2() {
  25. level = 4;
  26. grade = 'Worrior';
  27. }
  28. refer2();
  29. console.log('level : ' + level);
  30. }
  31. refer1();
  32. console.log('level : ' + level);
  33. console.log('grade : ' + grade);