script - 스코프와 전역변수의 문제점, 그리고 let과 const

스코프

변수, 함수와 깊은 관련이 있는 유효범위를 뜻하는 말
모든 식별자는 선언될 때 자신이 선언된 위치에 의해 참조할 수 있는 유효범위가 결정된다.

JS엔진은 식별자를 검색할 때 스코프라는 규칙을 통해 식별자를 결정한다. 같은 이름을 가진 식별자라도 스코프에 따라 구별될 수 있다. 같은 이름의 파일이 한 폴더에 있으면 안되지만 다른 폴더에 있다면 괜찮은 것과 같다.

전역변수와 지역변수

전역변수는 제일 밖에 있는 변수로 어떠한 블록도 없는 곳에 선언한 변수이다.
지역변수는 함수 내에서 선언한 변수이다.

함수 내부에서 전역변수와 같은 이름의 지역변수를 선언한 후 그 변수를 참조하면 지역변수가 참조된다. 이유는 JS엔진이 스코프 체인을 통해 참조할 변수를 검색하고 같은 이름 임에도 지역변수가 먼저 참조되기 때문이다.
스코프 체인이란?
-> 물리적으로 존재하는 연결 체인이며 이를 통해 JS엔진은 하위부터 상위로 이동하며 식별자를 검색한다. 지역 스코프가 먼저 검색되는 이유는 하위부터 검색하기 때문이다.

함수레벨 스코프(feat. var)

오직 함수블록 내에서만 선언된 변수를 지역 스코프(함수 레벨 스코프)로 인정한다. 단 var키워드 변수만 그렇다. let과 const 키워드는 if나 for문 등의 다른 블록에서도 지역 스코프로 인정된다. var로 if문에서 변수를 선언하면 전역변수처럼 사용할 수 있게 된다. 이는 치명적이므로 의도한 것이 아니라면 let이나 const를 주로 사용해야한다.

렉시컬 스코프

var x = 1;
function foo(){
  var x = 10; 
  bar(); 
}
function bar(){
  console.log(x); 
}
foo(); //x->10 or 1 ???

과연 JS엔진은 함수의 호출 위치에 따른 스코프(동적 스코프)를 채택했을까? 아니면 함수의 정의 위치에 따른 스코프(렉시컬 스코프)를 채택했을까?
위의 코드에서 foo를 호출하면 나오는 x의 값은 1이다. 따라서 JS엔진은 정의 위치에 따른 스코프를 채택한 것을 알 수 있다.

전역 변수의 문제점

전역 변수의 문제점을 알기 전에...

변수는 생물처럼 존재했다가 소멸하는 생명주기가 있다. 만일 변수가 소멸하지 않는다면 메모리는 꽉차게 된다.
전역 변수는 애플리케이션이 살아있는 경우 계속 살아있다. 서버를 돌리게 된다면 전역 변수로 선언한 변수는 서버가 꺼질 때까지 메모리에 살아있는 것이다. 변수의 생명주기는 메모리 공간 확보-> 메모리 공간 해제-> 가용메모리 pool에 반환이다.

변수 호이스팅과 함수 레벨 스코프

변수 호이스팅은 스코프 규칙에 맞게 적용된다.

//예시
var x = 0;
function foo(){
  console.log(x); // undefined 전역변수 x가 있어도 스코프 때문에 0이 출력되지 않는다.
  var x = 10;
}

순서는 전역변수선언 -> 전역변수할당 -> 함수 호출 -> 지역변수선언 -> 지역변수할당 이다.

전역 객체

이 객체는 특별한 객체이다. 소스코드가 실행되는 런타임 이전에 제일 먼저 생성되는 특수한 객체이다.
클라이언트 단에서는 window, 서버 단에서는 global 객체를 의미한다.

이 전역 객체의 프로퍼티는 표준 빌트인 객체 (Object, String, Number ...), 환격에 따른 호스트 객체(클라이언트는 WebAPI, 서버는 Nodejs의 호스트API), 전역 변수, 전역 함수 등이 있다.
그래서 window.전역변수이름을 사용할 수 있다.

드디어 전역 변수의 문제점

  • 모든 코드에서 전역변수를 참조 및 변경할 수 있다.
  • 생명주기가 길어서 메모리가 낭비된다.
  • 변수 이름이 중복될 가능성이 높다.
  • 전역변수를 검색하는 속도가 가장 느리다. 스코프 체인에서 가장 마지막인 종점에 있기 때문이다.

전역 변수를 억제하는 방법

  • 즉시 실행함수 (function(){}())로 모든 코드를 감싸면 모든 변수는 지역변수가 된다.
  • 전역에 namespace를 담당할 객체를 생성하고 그 객체의 프로퍼티를 변수처럼 활용하는 방법
  • 모듈 패턴 사용하기 모듈 패턴이란 클래스를 모방한 패턴으로 다른 파일에 연관된 프로퍼티와 메서드를 만들어 전역변수 억제 및 캡슐화를 진행하는 것을 말한다. 캡슐화를 통해 은닉하여 프로퍼티와 메서드를 감추기도 한다. private이나 public과 같은 접근 제한자 키워드를 제공하진 않지만 모듈 패턴을 통해 이러한 기능을 사용할 수 있다.
    모듈 패턴의 예시
var Counter = (function(){ // 즉시 실행 함수로 반환값을 Counter에 할당한다.
  // private 변수
  var num = 0; // 그 이유는 반환값으로 num을 반환한다면 public이겠지만 그렇지 않다면 Counter를 통해서도 num에 접근할 수 없기때문에 private이다. 
  return { // private을 보호하기위해 보통 객체를 반환한다. 지역 변수를 반환하지 않는다.
	increase(){
      return ++num;
    },
    decrease(){
      return --num;
    }
  };
}());
  • ES6모듈 사용하기 이 모듈을 사용한 파일은 자체의 독자적인 스코프를 제공한다. 모던 웹브라우져에서 사용가능하며 아직까지는 Webpack등의 모듈 번들러를 사용한다.

let과 const

아까도 말했듯이 var로 선언한 변수는 여러가지 문제점들이 있다.

  • 변수 중복 선언을 허용한다. 덮어쓰기의 가능성이 있다.
  • 함수 레벨 스코프만 인정한다. if나 for문에서는 전역변수로 선언될 수 있다.
  • 변수 호이스팅. var로 선언한 변수는 선언 즉시 undefined로 초기화되어 호이스팅이 일어난다.

let 키워드

ES6부터 도입된 이 키워드는 여러 장점이 있다.

  • 변수 중복 선언을 금지한다. 같은 스코프 내에서는 중복선언이 금지된다.
  • 블록 레벨 스코프를 인정해서 모든 블록 내에서 선언된 변수는 지역변수로 선언된다.
  • 변수 호이스팅. let으로 선언한 변수는 선언 단계와 초기화 단계가 분리되어 진행된다. 그래서 선언은 미리 해놓지만 undefined나 할당할 값으로 초기화하는 과정은 그 선언문에 도달했을 때 실행된다. (단, 선언 단계에 식별자를 스코프에 등록하므로 호이스팅이 일어나기는 한다. 근데 그렇지 않은 것처럼 보여주는 것이다. 호이스팅이 일어나지 않으므로 선언되지 않은 변수를 사용하려고 한다면 RefferenceError가 난다.
let x = 1;
{
  console.log(x); // RefferenceError이다. 그 이유는 지역변수로 아래에 새로운 x를 선언했기 때문에 JS엔진은 새로운 x가 할당되지 않은 채인 것을 확인하고 에러를 낸다. 전역변수는 지역변수보다 나중에 확인되기 때문에 초기화가 되어있어도 console.log로 찍을 수가 없다. 
  //근데 실제로 f12를 눌러서 console창에서 찍어보니 SyntaxError가 나온다...이미 사용중인 변수인데 같은 변수를 또 선언하려고 한다는 에러이다...
  let x = 2;
}
  • var로 선언한 변수는 window(전역 객체)의 프로퍼티이지만 let은 아니다.

const 키워드

주로 사용해야할 키워드이다. 그 이유는 다음과 같다.

  • 선언과 초기화가 항상 동시에 일어나야 한다. 그래서 const x;는 에러가 난다.
  • 블록 레벨 스코프
  • 재할당 금지 const x=1; x=2;->에러 발생 이런 이유로 초기화가 항상 선언과 동시에 일어나야하는 것 같다.
  • 상수이다. 상수는 재할당이 금지된 변수이다. 이는 재할당을 금지할 뿐 불변을 의미하지는 않는다.
  • 객체를 할당할 경우 그 안의 프로퍼티는 참조이기 때문에 변경할 수 있다.
  • 대문자로 선언하고 주로 원시값을 받는다.

결론

변하지 않는 상수를 가장 많이 사용하도록 하고 변수를 사용할 때는 let을 사용하되 꼭 이용해야하는 경우에만 변수를 사용하는 것이 좋다. 의도적으로 호이스팅이나 중복을 만들고 싶은 경우에는 var를 사용해도 되지만 되도록 않하는 것이 좋다.

좋은 웹페이지 즐겨찾기