[Javascript] 스코프

yoon·2023년 10월 4일
0
post-thumbnail

스코프

스코프란?

스코프란 유효 범위라고도 부르며, 변수가 어느 범위까지 참조되는 지를 뜻한다. 변수는 자신이 선언된 위치에 따라 다른 코드가 변수 자신을 참조할 수 있는 범위가 결정된다. 변수 뿐만 아니라 모든 식별자에 해당되므로 스코프는 식별자가 유효한 범위를 뜻한다. 스코프는 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이다.

스코프의 종류

전역(Global) 스코프

전역이란 코드의 가장 바깥 영역을 말한다. 전역에 변수를 생성하면 전역 변수가 되며 전역 변수는 어디에서든지 참조할 수 있다.

지역(Local) 스코프

지역이란 함수 몸체 내부를 뜻한다. 지역은 지역 스코프를 만들며 지역에 선언된 지역 변수는 자신이 선언된 지역과 하위 지역에서만 참조할 수 있다.

var var1 = 1; // 전역 변수

function foo() {
  var var2 = 2; // 지역 변수

  function bar() {
    var var3 = 3; // 지역 변수
  }
}

console.log(var1); // 1
console.log(var2); // ReferenceError: var2 is not defined
console.log(var3); // ReferenceError: var3 is not defined

var1은 코드의 가장 바깥에 선언되었으므로 전역 변수이다.
var2var3은 함수 내부에 선언된 지역 변수로 두 변수는 지역 스코프의 외부에서는 참조할 수 없다.

스코프 체인

모든 스코프는 상/하위로 하나의 계층적 구조로 연결된다. 모든 지역 스코프의 최상위 스코프는 전역 스코프이다. 이처럼 스코프가 계층적으로 연결된 것을 스코프 체인이라고 한다.

변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작해서 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.

var x = 'global';

function outer() {
  var y = 'local';
  
  console.log(x); // global
  console.log(y); // local
  
  function inner() {
    var x = 'local';
    
    console.log(x); // local
    console.log(y); // local
  }
  
  inner();
}

outer();

console.log(x); // global
console.log(y); // ReferenceError: y is not defined

inner 함수 내부에서 참조되는 변수 xinner 함수의 지역 스코프에서 변수 x가 선언됐는지 검색한다. 변수 x가 존재하므로 변수 x의 값이 출력된다. inner 함수 내부에서 참조되는 변수 y도 마찬가지로 inner 함수 내부에서 변수 y를 검색해보고 없으면 상위 스코프인 outer 함수의 지역 스코프로 이동한다.

이처럼 자바스크립트 엔진은 스코프 체인을 따라 변수를 참조하는 코드의 스코프에서 상위 스코프로 이동하며 변수를 검색한다. 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만 하위 스코프에서 유효한 변수를 상위 스코프에서 참조하는 것은 불가능하다.

예시는 변수로 들었지만, 스코프는 식별자를 검색하는 규칙에 해당되므로 함수 등도 똑같이 적용된다.

스코프와 호이스팅

var x = 'global';

function foo() {
  console.log(x); // (1)
  var x = 'local';
}

foo();
console.log(x); // global

foo 함수 내부에서 선언된 지역 변수 x(1) 시점에 이미 선언되었고, undefined로 초기화 되어 있다. 따라서 지역 변수 x를 참조한다. 이처럼 호이스팅은 스코프를 단위로 동작한다.

함수 레벨 스코프 & 블록 레벨 스코프

함수 레벨 스코프

함수 레벨 스코프는 함수에 의해서만 지역 스코프가 생기는 것이다. var 키워드로 선언된 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정한다.

블록 레벨 스코프

블록 레벨 스코프는 모든 코드 블록(if, for, while, {}...)이 지역 스코프를 만든다.

var var1 = 1;

if (true) {
  var var2 = 2;
  if (true) {
    var var3 = 3;
  }
}

function foo() {
  var var4 = 4;

  function bar() {
    var var5 = 5;
  }
}

console.log(var1); // 1
console.log(var2); // 2
console.log(var3); // 3
console.log(var4); // ReferenceError: var4 is not defined
console.log(var5); // ReferenceError: var5 is not defined

이 예시에서 모든 변수는 var 키워드를 통해 선언되었다. var은 함수 레벨 스코프이기 때문에 함수 몸체만 지역 스코프로 여긴다. 따라서 if 문 내부에 선언된 변수도 전역 변수에 속하므로 참조할 수 있다. 그러나 함수 foo, bar 내부에 선언된 변수들은 하위 스코프이기 때문에 참조가 불가능하다.

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 1000)
}

이 코드는 로그에 5가 5번 찍힐 것이다. 이유는 var 키워드로 이루어진 변수 선언은 함수 스코프이기 때문에 setTimeout이 이미 실행될 시점에 i는 이미 5가 되어있고 모든 함수가 같은 값 i를 참조한다.

이를 고치기 위해서는 블록 레벨 스코프인 let을 사용해야 한다.

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 1000)
}

렉시컬(lexical) 스코프

함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정하는 방법을 렉시컬 스코프(정적 스코프)라고 한다. 함수 정의가 평가되는 시점에 상위 스코프가 결정된다. 따라서 함수의 상위 스코프는 언제나 자신이 정의된 스코프이다. 함수 정의가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억한다.

var x = 1;

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

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

foo(); // 1
bar(); // 1

bar 함수는 전역에서 정의되었으므로 전역 스코프를 기억한다. 따라서 bar 함수는 전역 스코프를 상위 스코프라고 생각하므로 두 함수 모두 1을 출력한다.

퀴즈

# 1

let x = 30;
function get() {
  return x;
}

let result = get(20); // 30

함수 getx를 리턴하는데 이 x는 전역 변수의 x를 가르킨다. get 함수 스코프 안에 x라는 변수가 별도로 선언되어 있지 않기 때문이다. 20은 인자로 받아 함수에 전달되었지만 함수 내부에 어떤 변수에도 할당되지 않는 즉, 사용되지 않는 값이다.

# 2

let x = 30;
function get(x) {
  return x;
}

let result = get(20); // 20

함수에 매개변수 x가 선언되었으므로 20의 값을 받아 리턴할 수 있다. 즉 함수가 리턴하는 x의 값은 전역 변수가 아니라 함수 스코프에 선언된 별도의 매개변수인 x이다.

# 3

let x = 30;

function get () { 
  return x; 
}

function set (value) { 
  x = value; 
}

set(10);
let result = get(20); // 10

set 함수에 있는 x 값은 전역 변수이므로 set(10)에 의해 전역변수가 x = 10으로 재할당 된다. get 함수에는 20을 받을 매개변수가 없으니 전역변수 x를 그대로 반환해주며 set 함수에 의해 재할당된 10을 리턴하게 된다.

# 4

let x = 10;

function outer () {
  let x = 20;
  
  function inner () {
    return x;
  }

  return inner();
}

let result = outer(); // 20

outer 함수 스코프에는 20을 값으로 하는 변수 x와 함수 inner가 선언되어 있다. inner 함수는 x를 반환한다. 그러나 inner 함수 스코프 내에 x라는 변수가 없기 때문에, 상위 스코프인 outer 함수 스코프로 이동해서 변수 x를 검색한다.

# 5

let x = 10;

function outer () {
  let x = 20;

  function inner () {
    x = x + 10;
    return x;
  }
  inner();
}

outer();
let result = x; // 10

변수 result에 할당된 값은 전역 스코프의 x이므로, result의 값은 outer 함수나 inner 함수 스코프에 의해 영향을 받지 않는다. 또한 inner 함수가 참조하는 x 값은 상위 스코프인 outer 함수에 정의된 x이다.

# 6

let x = 10;

function outer () {
  x = 20;

  function inner () {
    let x
    x = x + 20;
    return x;
  }
  inner();
}

outer();
let result = x; // 20

outer 함수에서 x = 20 코드에서 참조하고 있는 xouter 함수에 존재하지 않으므로 그보다 상위 스코프인 전역 스코프에 선언된 x이다. 따라서 전역변수 x의 값이 20으로 재할당 된다. inner 함수 스코프의 변수 x 값과 outer 함수 스코프의 변수 x 값이 참조하는 값은 다르다.

# 7

let x = 10;

function outer () {
  x = 20;
  
  function inner () {
    x = x + 20;
  }
  
  inner();
}

outer();
let result = x; // 40

모든 함수 안에 있는 x의 값은 참조하는 코드가 있는 스코프에 존재하지 않으므로 상위 스코프로 이동하며 변수를 검색한다. 따라서 전역 스코프에 선언된 전역 변수 x를 참조한다. 따라서 outer()를 통해 20으로, inner()를 통해 40으로 바뀌어 result에 할당된다.

profile
얼레벌레 개발자

0개의 댓글