스코프

식별자가 자신이 선언된 위치에 의해 다른코드가 식별자 자신을 참조할 수 있는 유효범위

let x = 'global';

function test() {
  let x = 'local';
  console.log(x); // 두개의 x 라는 변수 중 어떤 것을 참조해야할지 결정하는 것을 식별자 결정
  // 자바스크립트엔진이 참조해야할 식별자를 검색할 때 사용하는 규칙이라고도 할 수 있음
}

test(); // "local"

식별자는 어떤 값을 구분하기 위해 유일해야 하므로 중복될 수 없다. 하지만 모든 코드에서 유일한 이름을 가지는 식별자를 사용해야하는 것은 매우 번거로운 일이기 때문에 스코프를 통해 변수의 이름 충돌을 방지한다.

스코프 내부에서는 식별자가 유일해야하며 해당 스코프가 아닌 영역에서는 중복될 수 있다. (스코프 = 네임스페이스)

function test() {
  var x = 'aaa';

  var x = 'bbb'; // var로 선언된 변수는 중복선언이 가능하기 때문에 에러가 발생하지 않음
  console.log(x);
}

test(); // "bbb";

function test1() {
  let x = 3;
  let x = 5;
  console.log(x); // let 이나 const 키워드로 선언한 변수는 같은 스코프내에서 중복선언 할 수 없다
}

test1(); // SyntexError: identifier "x" has already bee declared

코드 문맥과 환경

코드가 어디서 실행되며 주변에 어떤 코드가 있는지를 렉시컬 환경 이라고함 코드의 문맥은 렉시컬 환경으로 이루어지며 이를 구현한 것 이 실행 컨텍스트 모든 코드는 실행 컨텍스트에서 평가되고 실행됨 렉시컬 환경과 실행컨텍스트는 이후 “실행컨텍스트” 목차에서 다시 다룰 예정


스코프의 종류

구분 설명 스코프 변수
전역 코드의 가장 바깥영역 전역 스코프 전역 변수
지역 함수 몸체 내부 지역 스코프 지역변수
  • 전역 변수는 어디에서든 참조 할 수 있음
  • 지역 변수는 자신의 지역 스코프와 하위 스코프에서만 유효하다

스코프 체인

함수는 중첩될 수 있기 때문에 함수 몸체 내부에서 생성되는 스코프또한 중첩되어 계층적 구조를 갖게 됨 이러한 계층구조를 스코프 체인 이라고 함

변수를 참조할때 스코프체인을 통해 해당 변수를 참조하는 스코프부터 시작하여 상위 스코프 즉 전역스코프를 향해 이동하며 선언된 변수를 검색함


함수레벨 스코프

코드 블록{} 이 아닌 함수에 의해서만 지역스코프가 생성됨 var로 선언된 변수는 오로지 함수에 코드블록 만을 지역 스코프로 인정한다. 이러한 것을 함수레벨 스코프 라고 함

var x = 1;

if (true) {
  var x = 10; // 앞서 선언된 전역변수 x에 중복 선언 된다.
  // 의도치 않게 변수 값을 변경하는 부작용이 발생할 수 있음
}

console.log(x); // 10

var i = 10;
for (var i = 0; i < 5; i++) {
  console.log(i);
}
// for문을 통해 값이 변경되었다
console.log(i); // 5

렉시컬 스코프

var x = 1;

function test() {
  var x = 10;
  poo();
}

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

test();
poo();

위 코드의 결과는 1이 두번 출력된다. 자바스크립트는 함수가 정의된 시점에 함수에 상위 스코프를 결정하는 렉시컬 스코프(정적 스코프) 를 따르게 때문에 위와 같은 결과가 출력된다.

함수를 어디서 호출했느냐에 따라 함수의 상위 스코프를 결정하는 것을 동적 스코프라고 함 함수가 어디서 정의되었느냐에 따라 함수의 상위 스코프를 결정하는 것을 렉시컬 스코프 또는 정적 스코프라고 함


블록 레벨 스코프

let 키워드

  • var 키워드와는 다르게 같은 이름의 변수를 중복 선언할 수 없다. 중복선언시 SyntaxError 가 발생한다.
  • 블록 레벨 스코프
let test = 1;

{
  let test = 2;
  let test1 = 3;
}

console.log(test); // 1
console.log(test1); // ReferenceError: test1 is not defined
  • 변수 호이스팅이 발생하지 않는 것 처럼 보이지만 var 키워드와는 다르게 선언단계와 초기화 단계가 분리되어 진행되기 때문에 초기화 이전 함수를 참조하면 참조에러가 발생한다.
console.log(test); // ReferenceError: test is not defined

let test;
console.log(test); // undefined

test = 1;
console.log(test); // 1

// 변수 호이스팅이 발생한다는 이유는 아래 코드로 알수 있다
let name = 'Lee';
{
  console.log(name); // ReferenceError: cannot access "name" before initialization
  let name = 'Heo';
}
  • let은 전역변수로 선언하더라도 전역객체의 프로퍼티가 되지 않는다.

    • 보이지 않는 개념적인 블록(전역 렉시컬 환경의 선언적 환경 레코드) 내에 존재하게 된다.

const 키워드

  • 선언과 초기화를 동시에 해야한다.
  • let 키워드와 같이 블록스코프를 가지며 호이스팅이 발생하지만 발생하지 않는 것 처럼 보인다.
  • 재할당이 금지된다.

    • 이러한 특징으로 인해 상수(재할당이 금지된 변수)를 표현하는데 사용하기도 한다.
let preTaxPrice = 100;

let afterTaxPrice = preTaxPrice + preTaxPrice * 0.1; // 0.1은 쉽게 바뀌지 않는 값이며 프로그램에서 전체에서 고정된 값을 사용해야하므로 상수로 사용하는 것이 유용하다

console.log(afterTaxPrice); // 110
  • const 키워드로 선언된 객체는 값을 변경할 수 있다.

    • 재할당이 금지될 뿐 불변은 아니기 때문에 프로퍼티의 생성, 삭제, 변경이 가능한 것이다.

var vs. let vs. const

  • ES6 를 사용한다면 var 키워드는 사용하지 않는다.
  • 재할당이 필요한 경우 let을 사용하며 변수의 스코프는 최대한 좁게 만든다.
  • 변경이 발생하지 않고 일기 전용으로 사용하는 원시 값과 객체는 const 키워드를 사용한다.


전역 변수의 문제점

변수의 생명주기

변수는 선언에 의해 생성 되고 할당을 통해 값을 받는다. 변수에 생명주기가 존재하지 않는다면 한번 선언된 변수가 프로그램 종료전까지 계속 메모리 공간을 할당하고 있기 때문에 문제가 발생하게된다.

그로인해 변수는 자신이 선언된 위치에서 생성되고 소멸한다.

지역 변수의 생명주기

function foo() {
  let x = 'local';
  console.log(x);
  return x;
}

foo(); // "local"

위 코드에서 지역변수 x는 foo 함수가 호출되기 이전에는 생성되지 않는다. 일반적으로 호이스팅으로 인해 변수의 선언문이 먼저 실행되는 것이냐에 대한 의문이 생길 수 있지만 해당 설명은 전역변수에 한정되는 설명이다.

함수 내부에서 선언한 변수는 함수가 호출된 직후에 실행되기 때문에 이때 호이스팅이 발생하게된다. 그리고 함수가 종료된 이후에는 지역변수x 또한 소멸하게 된다.

즉, 지역 변수는 함수의 생명주기와 일치한다.

var x = 'global';

function foo() {
  console.log(x); // 함수 몸체 내부의 지역변수 x의 호이스팅으로인해 undefined 가 반환됨
  var x = 'local';
}

foo();

console.log(x); // global

전역 변수의 생명주기

var 키워드로 선언한 전역변수는 전역 객체의 프로퍼티가 된다. 이로써 전역변수의 생명주기가 전역 객체의 생명주기와 일치한다는 것을 알 수 있다.

전역객체?

자바스크립트 엔진에의해 어떤 객체보다 먼저 생성되는 개체를 뜻하며 클라이언트 사이드 환경(브라우저) 에서는 window, 서버사이드 환경(Node,js) 에서는 global 객체를 의미한다. 환경에 따라 전역 객체를 가리키는 다양한 식별자(window, self, this, frames, global)이 존재하였으나 ES11부터 globalThis로 통일 되었다.

전역 객체를 표준 빌트인 객체인 `Object, String, Number, Function, Array,…)와 같은 환경에 따른 호스트 객체(Web Api, Node.js의 호스트 API) 그리고 var 키워드로 선언된 전역 변수와 전역 함수를 프로퍼티로 갖는다.

전역 변수의 문제점

  1. 암묵적 결합 전역 변수는 코드 어디서든 참조하고 할당할 수 있기 때문에 암묵적 결합을 허용하는 것이다. 변수의 유효 범위가 커지면 커질 수록 코드의 가독성이 떨어지고 의도치 않게 상태가 변경될 수 있는 위험도 증가한다.
  2. 긴 생명 주기 어플리케이션의 생명주기와 동일하기 때문에 변수가 어플리케이션 종료전까지 계속 존재하게되고 var 키워드는 중복선언 또한 가능하기 때문에 상태관리에 더 큰 어려움을 겪을 수 있다. 또한 메모리 공간또한 계속 사용하고 있기 때문에 메모리 활용면에서도 효율이 떨어진다.
  3. 스코프 체인 상에서 종점에 존재 전역 스코프는 항상 최상위, 즉 종점에 위치해 있기 때문에 전역변수를 참조하게될 경우 항상 마지막에 검색된다. 즉 검색속도가 가장 느리다. 속도의 차이가 눈에띄게 나지는 않지만 분명히 차이가 존재하는 것이다.
  4. 네임 스페이스 오염 파일이 분리되어있더라도 하나의 전역 스코프를 공유하기 때문에 다른 파일 내에서 동일한 이름의 전역 변수나 전역 함수가 같은 스코프내에 존재할 경우 예상치 못한 결과가 발생할 수 있다.

전역 변수의 사용을 억제하는 방법

변수의 스코프는 좁으면 좋을수록 부수효과가 발생하지 않고 메모리 리소스 사용량도 적기때문에 전역변수를 반드시 사용해야할 이유가 아니라면 지역 변수를 사용하는 것을 추천한다.

즉시 실행 함수

함수 정의와 동시에 호출되는 함수이며 즉시 실행 함수 또한 함수 이기 때문에 지역스코프를 갖게 되고 전역변수의 사용을 제한할 수 있따.

(function () {
  var test = '55'; // 즉시 실행 함수의 지역 변수
})();

console.log(test); // ReferenceError: test is not defined

네임 스페이스 객체

전역에 네임 스페이스 역할을 할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티에 추가하는 방식이다.

const OBJECT = {};

OBJECT.name = 'Lee';

console.log(OBJECT.name); // Lee

OBJECT.person = {
  name: 'Kim',
  age: 40,
};

// 네임 스페이스 객체 내에 다른 네임스페이스 객체를 프로퍼티로 추가해서 게층적으로 구성할 수 도 있다.

허나 이 방법은 네임 스페이스 객체가 전역 변수로 할당되므로 좋은 방법은 아니다.

모듈 패턴

클래스를 모방하여 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만드는 패턴이다.

클로저를 기반으로 동작하며 전역 변수의 억제, 캠슐화까지 구현할 수 있다.

캡슐화?

객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작 할 수 있는 메서드를 하나로 묶는 것 특정 프로퍼티와 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉 이라고 함

객체 지향 프로그래밍 언어는 클래스를 구성하는 멤버에 대해 public, private, protected 등의 접근 제한자를 사용해 공개 범위를 한정할 수 있다. 자바스크립트는 public, private, protected 등의 접근 제한자를 제공하지 않기 때문에 모듈 패턴은 한정적인 정보 은닉을 구현하기위해 사용된다.

const Counter = (function () {
  let num = 0;

  return {
    increase() {
      return ++num;
    },
    decrease() {
      return --num;
    },
  };
})();

console.log(Counter.num); // undefined 즉시 실행 함수의 변수기 때문에 외부에서 접근이 불가능

console.log(Counter.increase()); // 1
console.log(Counter.decrease()); // 0

ES6 모듈

ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공하기 때문에 모듈 내에서 var 키워드로 선언한 변수라고해도 전역변수가 아니며 전역 객체의 프로퍼티 또한 아니다.

script 태그의 type=“module” 어트리뷰트를 추가하면 모듈로서 동작한다. 확장자는 mjs 를 권장함

ES6 모듈을 IE와 같은 구형 브라우저에서 동작 하지 않기 때문에 트랜스 파일링이나 번들링이 필요한 기능이다. 그래서 Webpack 등의 모듈 번들러를 사용하는 것이 일반적이다.