Scope

kevin kim·2020년 6월 24일
0

Scope

오늘은 Scope에 대해 정리해보겠다. Scope는 범위라는 뜻을 가지고 있다.

전역 변수와 지역 변수

자바스크립트에서 주로 변수를 사용해 데이터를 저장했었다. 대부분 var로 변수를 선언했을 것이다. 물론 변수를 만드는 일은 문제가 되지 않지만 전역(global)변수를 만드는 일은 최대한 지양해야한다.
전역 변수란 자바스크립트에서 제일 바깥 범위(함수 안에 포함되지 않은)에 변수를 만드는 것이다.
즉, window 객체에 변수를 만드는 것이다.

var x = 'global';
function ex() {
  var x = 'local';
  x = 'change';
}
ex(); // 변수 x의 값을 바꿔본다.
alert(x); // global

위의 코드를 보면 같은 x여도 ex 함수 바깥은 x는 전역 변수이고, ex 함수 안의 x는 지역 변수이다. 지역 변수는 함수 안에 들어있는 변수를 의미한다.

스코프(Scope)

위의 상황에서 지역 변수는 아무리 해도 전역 변수에 영향을 끼칠 수 없다. 바로 함수 스코프 때문이다. 한국말로 '범위'라는 말처럼 함수 안에서 선언된 변수는 해당 함수 안에서만 사용할 수 있다. var x = 'local'은 ex 함수 안에서만 그 데이터를 사용할 수 있다. 그 밑에 있는 x = 'change'도 함수 안의 지역 변수 x를 바꾸는 것이다. 다른 예시인 아래 코드를 보자.

var x = 'global';
function ex() {
  x = 'change';
}
ex();
alert(x); // change

코드를 보면 아까와는 달리 ex 함수 안에서 var를 선언하지 않았다. 이제는 x = 'change'를 했을 때 전역 변수가 바뀌는 것을 확인할 수 있다. 자바스크립트는 변수의 범위를 호출한 함수의 지역 스코프부터 전역 변수들이 있는 전역 스코프까지 점차 넓혀가며 찾기 때문이다. 함수 ex의 범위 안에 x가 없기 때문에, 더 넓은 범위인 전역 스코프에서 찾는 것이다.

정리를 해보면 첫 번째 코드에서는 ex의 범위 안에 바로 x(ex의 지역 변수)가 있었기 때문에 지역 변수 x를 바꾸고, 전역 변수 x는 바꾸지 않은 것이다.

스코프 체인

바로 전역 변수와 지역 변수의 관계에서 스코프 체인(scope chain) 이란 개념이 나온다. 내부 함수에서는 외부 함수의 변수에 접근이 가능하지만, 외부 함수에서는 내부 함수의 변수에 접근할 수 없다. 그리고 모든 함수들은 전역 객체에 접근할 수 있다. 아래의 코드를 확인해보자.

var name = 'kei';
function outer() {
  console.log('외부', name);
  function inner() {
    var enemy = 'John';
    console.log('내부', name);
  }
  inner();
}
outer();
console.log(enemy); // undefined

inner 함수는 name 변수를 찾기 위해 먼저 자기 자신의 스코프에서 찾고, 없으면 한 단계 올라가 outer 스코프에서 찾고, 없으면 다시 올라가 결국 전역 스코프에서 찾는다. 다행히 전역 스코프에서 name 변수를 찾아 'kei'라는 값을 얻었다. 만약 전역 스코프에도 없다면 변수를 찾지 못하였다는 에러가 발생한다.
이렇게 꼬리를 물고 계속 범위를 넓히면서 찾는 관계를 스코프 체인이라고 부른다.

렉시컬 스코핑(lexical scoping)

스코프는 함수를 호출할 때가 아니라, 선언할 때 생기는 것이다. 정적 스코프라고도 불린다.
다음 코드에서 console이 어떻게 찍힐 지 예상해보자.

let name = 'kei';
function log() {
  console.log(name);
}

function wrapper() {
  let name = 'John';
  log();
}
wrapper();

console이 어떻게 찍힐 것이라고 생각하는가?
스코프는 함수를 선언할 때 생긴다고 한 말을 기억할 것이다. log 함수 안의 name은 wrapper 안의 지역 변수 name이 아니라, 전역 변수 name을 가리키고 있는 것이다. 이런 것을 lexical scoping이라고 한다.

lexical scoping이 좀 이해하기 힘들기 때문에 다시 한번 살펴보자.
함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 자신 스코프로부터 가장 가까운 곳(상위 범위에서)에 있는 변수를 계속 참조하게 된다. 위의 코드에서는 log 함수 안의 name 변수는 선언 시 가장 가까운 전역 변수 name을 참조하게 된다. 그래서 wrapper 안에서 log 함수를 호출해도 지역 변수 let name='John'을 참조하는 것이 아니라 그대로 전역 변수 name의 값인 kei가 나오는 것이다.

무슨 짓을 해도 log 함수가 한 번 선언된 이상, 전역 변수를 가리키게 되어있는 name 변수가 다른 것을 가리키게 할 수는 없다. 유일한 방법은 아까처럼 전역 변수를 다른 값으로 바꾸는 것이다.

전역 변수를 만드는 일은 지양해야 한다고 서두에 말했었는데, 그 이유는 변수가 섞일 수 있기 때문이다. 자바스크립트 앱을 만들면서 혼자서 개발하는 것이 아니라, 여러 명과 협업도 하고, 다른 사람의 라이브러리를 사용하는 일도 많다. 그런데 전역 변수를 사용하다보면, 우연의 일치로 인해 같은 변수 이름을 사용해서 이전에 있던 변수를 덮어쓰는 불상사가 발생할 수도 있다.

간단한 해결 방법은 전역 변수 대신 한 번 함수 안에 넣어 지역 변수로 만드는 것이다. 아니면 객체 안의 속성으로 만드는 방법도 있다!

var obj = {
  x: 'local',
  y: function() {
    alert(this.x);
  }
}

위의 코드 같은 방법으로 하면 obj.x, obj.y()이렇게 접근해야 하기 때문에 다른 사람과 섞일 염려가 없다. obj를 통째로 덮어쓰지 않는 이상은 말이다.

이런 방법을 네임스페이스를 만든다고 표현한다. obj라는 고유 네임스페이스를 만들어서 겹치지 않게 하는 것이다. 대부분의 라이브러리가 네임스페이스를 사용하고 있다. naver는 jindo, facebook은 FB, jquery는 jQuery(또는 $)같이 말이다.

하지만 이러한 방법의 단점은 누군가 고의적으로 x와 y를 바꿀 수 있다는 것이다. 코드 밑에 스크립트를 추가해서 말이다. obj를 통째로 바꾸지 않더라도 밑에 obj.x = 'hacked';라고 한 줄 추가만 하면 obj.y();를 했을 시 local 대신 hacked가 alert 된다.
그것을 방지하기 위해서는 아래 코드를 확인해 보자.

var another = function () {
  var x = 'local';
  function y() {
    alert(x);
  }
  return {y: y};
}
var newScope = another();

조금 복잡하지만 위의 코드와 같이 하면 된다. another();하는 순간 return에 의해 { y: function () { alert(x) } };가 newScope에 저장된다. 이제 newScope라는 네임스페이스를 통해서 y에 접근할 수 있다. x는 접근할 수 없다! 위처럼 함수로 감싼 후 return을 통해 공개할 변수(y)만 공개하고, 비공개 할 변수(x)는 비공개하는 방법을 취할 수 있다. 즉, return하는 변수는 공개 변수이고, 다른 것은 비공개 변수인 것이다.

위의 코드를 간략하게 바꾸면

var newScope = (function () {
  var x = 'local';
  return {
    y: function() {
      alert(x);
    }
  };
})();

이렇게 쓸 수 있다. another 같은 변수를 한 번 거치는 대신, newScope에 바로 집어넣은 것이다. 처음 보는 게 있을 것이다. 바로 (function() {})(); 구문이다. IIFE(즉시 호출 함수 표현식) 이라고도 하고, 모듈 패턴이라고도 하는데, 함수를 선언하자마자 바로 실행시켜버리는 것이다. 함수를 function() {}로 선언하면서 동시에 ()를 붙이니까 즉시 실행된다. 이 구문이 라이브러리를 만들 때 기본이다. 많은 라이브러리가 이 구문을 활용하고 있다. 비공개 변수가 없는 자바스크립트에 비공개 변수 기능을 만들어주기 때문이다. 이 패턴은 꼭 기억하고 있자!!

오늘은 scope에 대해 알아보았는데 조금 복잡할 수도 있다.
차근차근 읽어보면 이해가 될테니 잘 정리하면서 읽어보길 바란다.

profile
프론트엔드 개발자

2개의 댓글

comment-user-thumbnail
2020년 8월 1일

안녕하세요 js를 배우는 대학생입니다 글을 보다가 렉시컬 스코핑 부분에서 코드부분에서 실행을 하면 John이 나오는데 이는 wrapper()함수내의 name = 'john'으로 해서 전역변수 name이 변경된거같은데 var name = 'john'으로 해야 john이 아닌 전역변수 kei가 나올것 같습니다. 혹시나 제가 생각한것이 틀렸다면 추가적으로 알려주시면 감사하겠습니다.

1개의 답글