본문 바로가기
  • soldonii's devlog
Javascript 공부/Advanced Javascript(-)

자바스크립트의 스코프 체인과 변수 환경

by soldonii 2019. 8. 29.

*Udemy의"Advanced Javascript Concepts"강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. 함수 선언 - 함수 표현식과 함수 선언식

새로운 함수가 실행될 때마다 콜스택에 실행 컨텍스트가 하나씩 쌓인다고 배웠다. 이 실행 컨텍스트 내에서 변수 또는 함수가 메모리에 저장되는데, 함수 표현식과 함수 선언식은 메모리에 함수가 저장되는 방식이 서로 다르다.

 

// function Expression
var canada = function() {
  console.log('cold');
}

// function Declaration
function india() {
  console.log('warm');
}

// Function
// Invocation / Call / Execution

 

1) 함수 표현식

함수 표현식으로 정의된 함수는 함수가 실행될 때에 함수가 정의된다. 함수 표현식의 경우 함수를 특정 변수에 담아놓기 때문에, 이 경우 실행 컨텍스트 내에서 생성 단계에 var, let, const 등의 키워드를 만나면서 자바스크립트 엔진에 의해서 호이스팅이 진행되는데, 따라서 함수가 담겨있는 변수는 호이스팅되면서 undefined로 초기가 된다. 그리고 이후에 함수가 실행될 때 실제 담고자 했던 함수 값이 이 변수에 담기는 것이다.

2) 함수 선언식

반면 함수 선언식의 경우는 parse time, 즉 자바스크립트 엔진 속 컴파일러에 의해서 함수가 호이스팅 될 때 함께 정의된다. 이렇게 함수 선언식으로 정의된 함수의 경우, 이 실행 컨텍스트 안에는 this 뿐 아니라 'arguments'라는 객체가 존재한다.

 

2. arguments란?

function marry(person1, person2) {
  console.log(arguments); // { 0: 'Tim', 1: 'Tina'}
  return `${person1} is now married to ${person2}`
}

marry('Tim', 'Tina');

 

코드를 보면 console.log(arguments)를 했을 때, 파라미터로 제공된 'Tim', 'Tina'가 arguments 객체의 값으로 포함되어 있음을 알 수 있다. 즉 arguments 키워드를 통해 특정 함수에 input된 파라미터의 값을 얻을 수 있다. 이 arguments는 글로벌 실행 컨텍스트에서는 존재하지 않으며, 특정 함수가 실행되어서 그 함수의 실행 컨텍스트가 생성되었을 때, 해당 실행 컨텍스트 안에서만 발생하는 객체이다.

 

그런데 arguments 키워드를 출력했을 때 나오는 값은 배열이 아니라 객체이다. 그렇기 때문에 배열 관련 메소드를 사용하기가 어렵게 된다. 더 손쉽게 이 객체를 다루고 싶을 때는 Array.from() 메소드를 이용할 수 있다.

function marry(person1, person2) {
  console.log(arguments); // { 0: 'Tim', 1: 'Tina'}
	console.log(Array.from(arguments)); // ['Tim', 'Tina']
}

marry('Tim', 'Tina');

 

또는 디폴트 파라미터(default parameter) 개념을 사용해도 배열의 형태로 값을 얻을 수 있다. default parameter는 함수 선언 시 argument를 '...args'로 설정했을 때, 함수 실행 시 input되는 파라미터를 args 키워드로 얻을 수 있게 해준다.

function marry2(...args) {
  console.log(args); // ['Tim', 'Tina']
  return `${args[0]} is now married to ${args[1]}` // 'Tim is now married to Tina'
}

marry2('Tim', 'Tina');

 

참고로 매 실행 컨텍스트마다 arguments 객체를 이용할 수 있으며, 만약 어떤 파라미터도 전달되지 않은 함수의 경우에는 arguments 객체는 빈 객체가 된다.

function india() {
	console.log(arguments); // {}
}

 

3. 변수 환경

전역 변수를 많이 생성할 시에는 문제가 발생할 수 있다는 이야기를 여기저기서 들어봤을 것이다. 그럼 전역에 변수를 선언하지 않고 각각의 실행 컨텍스트 내부에 변수를 선언하면 내부적으로 어떤 일이 발생할까?

각각의 실행 컨텍스트 내부에는 변수 환경(Variable Environment)가 존재한다.

각각의 실행 컨텍스트 내부에는 '변수 환경(Variable Environment)'라는 것이 존재한다. 그리고 각 실행 컨텍스트 내부에서 선언된 변수는 모두 변수 환경에 저장된다.

function two() {
  var isValid;
}

function one() {
  var isValid = true;
  two();
}

var isValid = false;
one();

 

이 코드를 실행할 경우, 1차적으로 생성 단계(creation phase)에서는 함수 two와 함수 one이 완전히 hoisting된다.(함수 선언식으로 선언되었기 때문에) 또한 변수 isValid도 호이스팅 되어 undefined로 값이 초기화 된다. 생성 단계를 마치면 실행 단계(execution phase)에 들어간다.

 

실행 단계에 들어가면 글로벌 실행 컨텍스트 내에서 변수 isValid의 값은 false가 되고, 이후 함수 one을 실행하는 단계에서 함수 one 실행 컨텍스트가 생성된다. 이 실행 컨텍스트 내부에서는 변수 환경에 isValid 변수가 true 값으로 담겨져 저장되고, 또 two 함수를 실행하게 되면서 two의 실행 컨텍스트가 생성된다. 마찬가지로 이 안에서 변수 환경에 isValid는 undefined로 담긴다.

 

function two() {
  var isValid;
}

function one() {
  var isValid = true; // local environment(variable environment)
  two(); // create new Execution Context
}

var isValid = false;
one();


// 아래는 stack
// two() -- isValid는 undefined
// one() -- isValid는 true
// global() -- isValid는 false

 

같은 변수명이지만 각각의 실행 컨텍스트 내에서, 즉 각각의 소우주 내에서 isValid 변수는 서로 다른 값을 지닌다. 이 사례에서 기억해야 할 것은 각각의 실행 컨텍스트는 각각 변수 환경을 따로 가지고 있기 때문에, 실행 컨텍스트 내에서 변수를 선언할 경우, 그 실행 컨텍스트 내에서만 해당 변수에 접근해서 사용할 수 있으며, 해당 실행 컨텍스트의 모든 실행 단계가 끝나면 콜스택에서 pop up 되면서 동시에 해당 실행 컨텍스트 내에서의 변수 값 또한 메모리 공간에서 사라지게 된다.

 

4. 스코프 체인(Scope Chain)

각각의 실행 컨텍스트는 독립적인 소우주이지만, 해당 함수가 어디에서 선언되었는지(어디서 호출되었는지가 아니라), 즉 해당 함수의 렉시컬 환경이 어디인지에 따라서 다른 소우주와 연결이 될 수 있다.

var x = 'x';
function findName() {
	console.log(x);
  var b = 'b';
  return printName();
}

function printName() {
  var c = 'c';
  return 'Andrei Neagoie';
}

function sayMyName() {
  var a = 'a';
  return findName();
}

sayMyName()

 

이 코드의 경우 변수 x는 전역에서 선언되었다. 변수 x의 렉시컬 환경은 글로벌 실행 컨텍스트 내에 존재하는 window 객체가 된다. findName, printName, sayMyName 함수 또한 글로벌 스코프에서 선언되었기 때문에 마찬가지로 이들의 렉시컬 환경도 window 객체이다.(바꿔 말하면, 변수 x와 3개 함수 모두 window 객체의 property 중 하나이다.)

 

여기서 현재 findName 실행 컨텍스트 내부에서 x를 console.log하는데 이 경우 아무 오류 없이 x를 출력한다. findName이라는 독립된 소우주, 독립적인 실행 컨텍스트 내부에 존재하는 변수 환경에는 사실 현재 b만 보관되어 있는 상태이다. 그렇기 때문에 이 소우주 내에서는 x를 찾을 수 없는 것이 맞다. 하지만 자바스크립트 엔진은 자신의 실행 컨텍스트 내부에서 원하는 값을 찾지 못할 경우, 부모의 실행 컨텍스트로 거슬러 올라가 그 곳의 변수 환경을 뒤져서 원하는 값이 있는지를 찾는 과정을 반복한다. 계속 거슬러 올라가면 최종적으로는 window 객체까지 거슬러 올라가게 된다. window 객체에서도 원하는 변수를 찾지 못할 경우에는 ReferenceError가 발생한다.

 

이처럼 한 실행 컨텍스트 내부의 변수 환경은, 해당 실행 컨텍스트를 포함하고 있는 부모의 변수 환경과 연결이 되어 있는데, 이를 스코프 체인(scope chain)이라고 한다. 

 

이 스코프 개념은 자바스크립트에서는 정확히 이야기하면 static scope(=lexical scope), 즉 정적 스코프이다. 함수가 어디에서 호출되었는지, 즉 실행 컨텍스트가 콜 스택의 어디에 위치하는지는 변수 또는 데이터의 접근 범위에 아무런 영향도 미치지 않는다. 함수가 어디에서 정의되었는지에 따라서 변수에 대한 접근 범위가 결정되는 것이다. 

 

위 코드의 실행컨텍스트 및 스코프 체인. 최종적으로 각 실행 컨텍스트의 변수환경은 맨 아래의 글로벌 실행 컨텍스트의 그것과 연결되어 있다.

 

function sayMyName() {
  var a = 'a';
  return function findName() {
    var b = 'b';
    return function printName() {
      var c = 'c';
      return 'Andrei Neagoie';
    }
  }
}

sayMyName() // [Function : findName]
sayMyName()() // [Functon : printName]
sayMyName()()() // 'Andrei Neagoie'

 

바로 위의 코드 사례와 유사하지만 약간 다르다. 이 경우에는 스코프 체인이 위의 그림과 달라지게 된다. 각 함수가 서로 연결점을 가지게 되는 것이다. 이러한 연결 방식은 function lexical environment라고 한다. 첫번째 사례에서는 모든 함수의 렉시컬 환경이 window였다면, 본 사례에서 findName의 렉시컬 환경은 sayMyName, printName의 렉시컬 환경은 findName이 된다.

 

따라서 printName의 경우에는 변수 c, b, a에 모두 접근이 가능하고, findName은 변수 b, a는 접근이 가능하지만 c에는 접근이 불가능하다. 자식은 부모에게 접근이 가능하지만, 부모는 자식에게 접근이 불가능하다.(마치 현실처럼..)

 

서로 연결되어 있는 실행 컨텍스트들

 

기존 글에서 eval()과 with 키워드는 코드 최적화에 어려움을 준다고 작성했었는데(요기 참고!) 그 이유가 바로 스코프 체인에 변형을 가하기 때문이다. 

 

참고로 어떤 함수를 만들면, 해당 함수의 속성 중 하나로 [[Scopes]]가 있다. 여기에는 해당 함수의 렉시컬 환경이 어디인지에 대한 정보가 담겨있다.

[[Scopes]에서 렉시컬 환경이 Global이라고 안내해준다.

 

# 변수 선언 시에는 var, let, const 키워드를 반드시 사용하자!

function weird() {
  height = 50;
  return height;
}
weird();

 

이 경우 height 변수는 weird 실행 컨텍스트의 변수 환경에 저장되지 않는다. 대체 왜?!?!

왜냐하면 var, let, const 키워드를 컴파일러가 마주치지 못했기 때문에, 이를 변수 환경에 저장하는 대신 부모에게 간다. 글로벌 실행 컨텍스트에 가서 "height라는 변수 너 가지고 있니?"라고 물어보지만.. 그 곳에도 없다. 이러면 자바스크립트는 알아서 height를 자기가 변수로 만들어주기는 하지만, 변수 환경에 저장이 되지는 않는 것이다.

 

이러한 'use strict'를 맨 위에 써서 예측 불가능성을 방지할 수 있지만,, 그냥 var, let, const 키워드를 항상 쓰면 될 것 같다;

 

var heyhey = function doodle() {
  // do something
	return 'heyhey';
}
heyhey() // 'heyhey'
doodle() // ReferenceError

 

정말 마지막으로(긴 글이었다...흑흑) 이 사례를 보면 doodle 함수는 자기 자신의 scope 안에 갇혀 있다(?) 본인의 실행 컨텍스트 안의 변수 환경에 heyhey라는 변수의 값으로 doodle 함수가 들어가 있는 형태이기 때문에, 따라서 글로벌 스코프에서는 heyhey 변수에는 접근이 가능해도, doodle 함수에는 접근이 불가능하다.

댓글