3 분 소요

스코프란?

  1. 스코프는 식별자가 유효한 범위이다.
let a = 1;

function help() {
  let b = 2;
  console.log(a); // ouput : 1
  console.log(b); // output : 2
}

help();

console.log(a); // ouput : 1
console.log(b); // output : Error!

왜 에러가 나는걸까?

  • 생명주기가 존재하기 때문임. 변수 b의 경우 help함수가 종료되면 저장공간에서 함께 사라짐. 없는 변수를 출력하려 오류 발생.
  1. 스코프는 네임스페이스이다.
let a = 1;

function help() {
  let a = 2;
  console.log(a); // ouput : 2
}

help();

console.log(a); // ouput : 1

대부분의 프로그래밍 언어는 같은 스코프 내에서 동일한 식별자를 가진 변수를 선언하면 안됨. ⇒ 이는 윈도우의 동일한 폴더 내에 이름이 같은 파일이 생성될 수 없는 것과 비슷함.

하지만, 같은 이름의 변수를 두 번 선언했지만 에러가 발생하지 않음. ⇒ JS에서 스코프를 통해 동일한 식별자를 가진 변수끼리의 충돌을 방지해주기 때문임.

ㅤ 💡 다음으로 가기 전에 알아둬야 될 사항이 있음.

function hello() {
  let a = 10;
  let a = 20; // SyntaxError : x가 이미 존재함!
}

💡 이렇게, let, const 키워드를 통한 변수 중복선언은 JS가 “아 이녀석 중복선언을 했구나”하고 바로 잡을 수 있음.

그러나, var는 원래 중복선언이 허용되기 때문에 이게 문제가 안되는 상황이 발생함.

function hello() {
  var a = 10;
  var a = 20;
}

💡 꼭 let과 const를 통해서 변수를 만들도록 하자…

ㅤ ㅤ ㅤ

스코프의 종류

  • 전역 스코프와 지역 스코프가 있음.
let a = 10;

function help() {
  let b = 20;
}

전역 스코프는 코드의 가장 바깥 영역을 말함. ⇒ 여기서 변수 a는 전역 스코프에 있기 때문에 전역변수임.

지역 스코프의 지역은 보통 함수 몸체 내부임. ⇒ 여기서 변수 b는 지역 스코프에 있기 때문에 지역변수임.

스코프 체인

  • 중첩 함수가 계층적으로 연결된 것을 말함.
function a() {
  // outer function : nested functiond을 포함하는 함수.
  let res = 1;
  function b() {
    // nested function이자, c의 outer function
    function c() {
      // nested function
      console.log(res); // output : 1
    }
  }
}
  • 변수를 참조할 때 JS 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.

⇒ console.log(res)를 위해 c ⇒ b ⇒ a까지 올라가서 변수 res를 찾고 출력하는 모습을 볼 수 있다. ㅤ ㅤ ㅤ

var, let, const의 스코프

아까, 스코프의 종류는 전역 스코프와 지역스코프가 있다고 했었다.

전역스코프의 경우 말 그대로 함수의 제일 바깥쪽인건 모든 언어가 동일하나, 지역스코프의 경우는 두 가지 경우가 존재한다.

  1. 함수 레벨 스코프
  • 무조건 함수만을 지역스코프로 인정하는 것.
  • var이 이에 해당함.
  1. 블록 레벨 스코프
  • if, for, while, try/catch, 함수등 모든 코드 블록을 지역 스코프로 인정하는 것.
  • 대부분의 언어 및 let과 const가 이에 해당함.

여기서 var을 쓰면 안되는 이유가 한번 더 나온다.

자, 아래의 코드를 보자. 변수 i를 var, let으로 선언했다는 차이점이 있는 코드이다.

//i를 var로 선언한 경우

var i = 10;

for (var i = 0; i < 5; i++) {
  console.log(i); // output : 0,1,2,3,4
}

console.log(i); //output : 5
//i를 let으로 선언한 경우
let i = 10;

for (let i = 0; i < 5; i++) {
  console.log(i); //output : 0,1,2,3,4
}

console.log(i); //output : 10

(물논, 코딩 시에 혼란이 있을 수 있기에 for문을 도는게 아니라면 변수 식별자에 의미없는 i를 붙이진 않는게 좋다.)

var을 사용한 경우에

  1. for은 지역 스코프로 인정받지 못했다.
  2. 이후, for문이 시작될 때 i가 0으로 중첩 선언되었다.
  3. 반복문이 끝났을 때 i는 5가 되었다.
  4. 우린 10을 출력하길 원했지만 5가 출력됬다.

let을 사용한 경우에

  1. for은 코드 블록으로 구성됬기 때문에 지역 스코프로 인정받았다.
  2. “전역 스코프의 i” 와 “for 안의 i”는 다른 변수로 인정받았다.
  3. 마지막에 우리가 원하는대로 10이 출력된다.

이렇게, var을 사용 시 본인도 모르는 사이에 중첩 선언을 해서 코드가 꼬여버릴 수 있다.

⇒ 제발, var 쓰지말자!!!!

렉시컬 스코프

다음의 코드는 어떤 결과가 나올까?

var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // ??
bar(); // ??

(과거 나의 생각)

foo를 실행할 때 var x = 10으로 중첩 선언이 되니까 첫번째 출력은 10이다!

bar을 실행할 때는 그래도 전역 스코프의 x를 보니까 1이네?

아쉽게도 필자는 그렇게 생각했고 틀렸다. 아마, 본인과 비슷한 생각을 한 사람들이 많을 것 같다.

답은 1,1이다. 왜 이런 현상이 발생했는지 알아보자.

우선, bar함수의 상위 스코프가 무엇인지 추리해봐야 한다.

  1. 함수를 호출한 곳(foo 함수 안)을 기반으로 상위 스코프를 정함(foo, 전역이 상위스코프가 됨)
  2. 함수를 선언한 곳(전역 스코프)을 기반으로 상위 스코프를 정함(전역이 상위스코프가 됨)

첫 번째 방법을 동적 스코프, 두 번째 방법을 렉시컬 스코프 또는 정적 스코프라고 부른다.

이미 제목으로 나왔기에 알아차릴 수 있듯이 JS 및 대부분의 언어는 정적 스코프를 따른다.

그럼 왜 1이 나왔는지는 간단해진다. 아까, js는 스코프 체인이 존재한다고 말했었고, 현재 스코프 체인은 다음과 같다.

전역 ← foo

← bar

또한, 함수에서 특정 변수를 쓸 일이 있을 때는 본인보다 상위의 스코프를 올라가면서 찾는다고 했다.

⇒ 그럼 뭐다? bar에는 x가 없으니까 전역 스코프에서 x를 찾아 출력한다!

⇒ 그렇기 때문에 1,1이 정답이다.

💡 렉시컬 스코프는 JS의 그 유명한 ‘클로저’와 깊은 연관이 있다. 클로저에 대해서도 알아보자.