javascript 13강~14(전역변수의 문제점, 블록 레벨 스코프)


JavaScript 13강~14강 예습

  • 13강 : 전역 변수의 문제점

    1. 변수의 생명 주기

    2. 전역 변수의 생명 주기

      • 진입점
      • JS의 return 위치
      • 전역의 var 키워드
    3. 전역 변수의 문제점

      • 암묵적 결합 implicit coupling
      • 긴 생명 주기
      • 스코프 체인 상에서 종점에 존재
      • 네임스페이스 오염
    4. 전역 변수 사용 억제 방법

      • 즉시 실행 함수
      • 네임 스페이스 객체
      • 모듈 패턴
      • ES6 모듈

  • 14강 : let, const와 블록 레벨 스코프

    1. var 키워드로 선언한 변수의 문제점
      • 변수 중복 선언 허용
      • 함수 레벨 스코프
      • 변수 호이스팅
    2. let 키워드
      • 변수 중복 선언 금지
      • 블록 레벨 스코프
      • 변수 호이스팅
      • 전역 객체와 let
    3. const 키워드
      • 선언과 초기화
      • 상수
      • const키워드와 객체
    4. var vs. let vs. const


13강

전역 변수의 문제점


변수의 생명 주기

변수는 선언에 의해 생성되고 할당을 통해 값을 갖는다. 그리고 가비지 컬렉터에 의해 언젠가 소멸된다.

즉, 변수는 생명주기 를 지니고 있다.

전역 변수의 생명 주기는 애플리케이션의 생명 주기와 같다.

하지만, 함수 내부에서 선언된 지역 변수는 함수가 호출되면 생성되고 함수가 종료되면 소멸한다.

1
2
3
4
5
6
7
8
function foo() {
var x = 'local';
console.log(x); // local
return x;
}

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

지역변수 x함수 foo가 호출되기 전까지는 생성되지 않는다.
함수를 호출하지 않으면 변수 선언문이 실행되지 않기 때문이다.

정리하면,

  1. 함수가 호출될 경우 함수 내부의 선언문들이 먼저 실행되고 undefined를 할당받는다.
  2. 함수가 종료되면 지역변수도 소멸되어 생명주기가 종료된다.
  3. 즉, 함수가 호출되어 실행되는 동안에만 지역변수는 유효를 가진다. 지역변수의 생명주기 = 함수의 생명주기

변수 호이스팅 : 위 정리 내용을 토대로 보면 변수 호이스팅은 스코프를 단위로 동작한다.


전역 변수의 생명 주기

전역 코드들은 실행(호출 포함)하는 특별한 진입점이 없고 코드가 로드되자마자 곧바로 해석되고 실행된다.


진입점

​ 진입점은 C난 Java등은 프로그램 시작시 main함수를 호출한다. 이를 진입점 또는 시작점이라 한다.


JS의 return 위치

​ return은 함수 몸체 내부에서만 사용할 수 있다. 전역에서 return문을 사용하면 문법에러를 일으킨다.


전역의 var 키워드

var키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다.

브라우저 환경에서 전역 객체는 window이기 때문에 브라우저 환경에서 var키워드로 선언한 전역변수는 window의 프로퍼티가 된다.

이 전역 객체 window는 웹페이지 종료 시점까지 유효하며, 전역변수 var키워드는 웹페이 종료할 때까지 유효하다.


전역 변수의 문제점

1. 암묵적 결합 implicit coupling

​ 전역 변수는 내 의도와는 다르게 어디서든지 전역변수를 참조 및 변경이 가능하다.

이는 암묵적 결합을 허용하는 것으로 변수의 유효 범위가 크면 클 수록 코드의 가독성은 나빠지고 의도치 않게 상태가 바뀔 수 있다.


2. 긴 생명주기

​ 전역 변수는 생명주기가 프로그램의 종료시점과 거의 일치하므로, 이는 상태 변경기회를 늘리게 된다.

또한 메모리에도 게속 상주하게 되는 것이므로 리소스도 오랜 기간 소비한다.

이러한 문제는 오류를 발생시킬 확률을 증가시킨다.


3. 스코프 체인 상에서 종점에 존재

​ 전역 변수는 스코프 체인 상에서 종점에 존재한다.

이는 변수를 검색할 때 전역 변수가 가장 마지막에 검색된다는 것을 의미하는데,

결국, 전역변수의 검색 속도가 가장 느리다는 것을 의미한다.


4. 네임 스페이스 오염

​ JS의 가장 큰 문제는 .js 파일이 분리되어 있어도 하나의 전역 스코프를 공유한다는 것이다.

즉, .js 파일을 나누어 협업을 진행하게 될 경우 전역 변수 이름으로 충돌이 생길 확률이 매우 높다.


전역 변수 사용 억제 방법

전역 변수를 사용하여야 할 이유를 찾지 못했다면 지역변수를 사용해야 한다.

변수의 스코프는 좁을수록 좋다.


1. 즉시 실행 함수

​ 즉시 실행 함수는 함수 정의와 호출을 동시에 진행하다.

모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다.

이러한 특성은 전역 변수의 사용을 제한하는 한 방법이다.

이는, 라이브러리 등에서 자주 사용된다.

1
2
3
4
5
6
(function () {
var foo = 10; // 즉시 실행 함수의 지역 변수
// ...
}());

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

2. 네임 스페이스 객체

​ 네임 스페이스 객체는 말 그대로 Namespace 역할을 담당할 객체를 생성하고, 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법이다.

1
2
3
4
5
6
7
8
var MYAPP = {}; // 전역 네임 스페이스 객체

MYAPP.person = {
name: 'Lee',
address: 'Seoul'
};

console.log(MYAPP.person.name); // Lee

네임 스페이스 안에 객체를 만들어서 계층적으로도 활용이 가능하다.

네임 스페이스를 분리하여 식별자 충돌을 방지하는 효과는 있으나 네임 스페이스 객체 자체가 전역 변수에 할당이 되므로 유용하지는 않다.


3. 모듈 패턴

​ 클래스를 모방하여 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 형성한다.

모듈 패턴은 JS의 강력한 기능인 클로저를 기반으로 동작한다. 이는 전역 변수의 억제와 캡슐화까지 구현을 할 수 있다.

캡슐화란 ? 외부에 공개될 필요가 없는 정부를 외부에 노출하지 않고 숨기는 것(정보 은닉 - information hiding)

모듈 패턴은 전역 네임 스페이스의 오염을 막는 기능은 물론 한정적이기는 하지만 캡슐화를 구현하기 위해 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 변수에 즉시 실행 함수 할당
var Counter = (function () {
// private 변수
var num = 0;

// 외부로 공개할 데이터나 메소드를 프로퍼티로 추가한 객체를 반환한다.
return {
increase() {
return ++num;
},
decrease() {
return --num;
}
};
}());

// private 변수는 외부로 노출되지 않는다.
console.log(Counter.num); // undefined

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

4. ES6 모듈

​ 전역 변수의 남발을 억제하기 위해서 ES6에서 도입된 모듈을 사용할 수 있다.

현재 아직까지 모든 브라우저가 ES6를 전부 지원하고 있지 않기 때문에, SystemJS, RequireJS,Webpack등의 모듈 번들러를 이용한다.


14강

let, const 와 블록 레벨 스코프


var 키워드로 선언한 변수의 문제점

1. 변수 중복 선언 허용

1
2
3
4
5
var x = 1;
// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다.
// 아래 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작한다.
var x = 100;
console.log(x); // 100

변수 x가 중복 선언 되었다. 같은 스코프 내에서 변수를 중복 선언하면 나중에 작성된 변수 선언문은 JS엔진에 의해 var 키워드가 없는 것처럼 동작한다. 또한 에러는 발생시키지 않는다.


2. 함수 레벨 스코프

var 키워드로 선언한 변수는 오로지 함수의 코드 블록 만을 지역 스코프로 인정한다.

따라서 함수 외부에서 선언한 변수는 모두 전역 변수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var x = 1;

if (true) {
// var 키워드로 선언된 변수는 함수의 코드 블록 만을 지역 스코프로 인정한다.
// 함수 밖에서 선언된 변수는 코드 블록 내에서 선언되었다 할 지라도 모두 전역 변수이다.
// 따라서 x는 전역 변수이다. 이미 선언된 전역 변수 x가 있으므로 변수 x는 중복 선언된다.
// 이는 의도치 않게 변수값이 변경되는 부작용을 발생시킨다.
var x = 10;
console.log(x); // 10
}

for (var x = 0; x < 5; x++) {
console.log(x); // 0 1 2 3 4
}

console.log(x); // 5

위 예제처럼, 결국 최종 출력은 ‘5’ 가 된다. x가 연속적으로 재할당이 이루어졌기 때문이다.


3. 변수 호이스팅

1
2
3
4
5
6
7
8
9
10
11
// 이 시점에는 변수 호이스팅에 의해 이미 변수 foo가 선언되었다. (1. 선언 단계)
// 변수 foo는 undefined로 초기화된다. (2. 초기화 단계)
console.log(foo); // undefined

// 변수에 값을 할당 (3. 할당 단계)
foo = 123;

console.log(foo); // 123

// 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 실행된다.
var foo;

변수 선언문 이전에 변수를 참조하는 것은 변수 호이스팅에 의하여 에러를 유발하지 않는다.

다만, 이러한 방법은 프로그램의 흐름 상 가독성을 떨어뜨리고 예상치 못한 에러를 발생시킬 여지를 준다.


let 키워드

var 키워드의 이러한 문제들로 하여, ES6에서 let과 const를 도입하였다.

변수 중복 선언 금지

var 키워드와는 다르게 let 키워드는 변수를 중복 선언시 문법 에러를 발생시킨다.

1
2
3
4
5
6
7
8
var foo = 123;
// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다.
// 아래 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작한다.
var foo = 456;

let bar = 123;
// let이나 const 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용하지 않는다.
let bar = 456; // SyntaxError: Identifier 'bar' has already been declared

블록 레벨 스코프

poiema스코프

var 키워드 : 함수의 코드 블록( { } )만을 지역 스코프로 인정한다. 함수 레벨 스코프

let 키워드 : 모든 코드 블록 ( 함수, if, for, while, try/catch 등등) 을 지역 스코프로 인정하다. 블록 레벨 스코프

1
2
3
4
5
6
7
8
9
et foo = 123; // 전역 변수

{
let foo = 456; // 지역 변수
let bar = 456; // 지역 변수
}

console.log(foo); // 123
console.log(bar); // ReferenceError: bar is not defined 참조 불가

변수 호이스팅

1
2
console.log(foo); // ReferenceError: foo is not defined
let foo;

var 키워드변수 선언시 암묵적으로 선언 - 초기화 + 할당(undefined)가 모두 이루어진다.

let 키워드변수 선언시 암묵적은 선언 단계만 진행되며 초기화 단계는 변수 선언문에 도달 했을 때 실행된다.

따라서 스코프의 시작 지점부터 초기화 단계 시작 지점(변수 선언문)까지는 변수를 참조할 수 없다.

호이스팅즈

스코프 시작 지점부터 let 키워드의 초기화 시작 지점 전까지의 구간을 일시적 사각지대(TDZ) 라고 부른다.

1
2
3
4
5
6
7
8
9
// 런타임 이전에 선언 단계가 실행된다.
// 아직 변수가 초기화되지 않았다. 따라서 변수 선언문 이전에 변수를 참조할 수 없다.
console.log(foo); // ReferenceError: foo is not defined

let foo; // 변수 선언문에서 초기화 단계가 실행된다.
console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1

그렇다면 let 키워드로 생성된 변수는 호이스팅이 되지 않는걸까?

정답은 : 아니다.

이유는 다음 예제를 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
let boo = 1;

{
console.log(boo); // 1
}

//위 / 아래 비교
let foo = 1;

{
console.log(foo); // Reference Error : foo is not defined
let foo = 2;
}

boo는 전역변수로 선언을 하였기에 그룹 연산자{ }에서 console 을 찍어도 1이 출력된다.

그런데, foo는 전역변수로 선언했음에도 그룹 연산자{ }에서 console 을 찍었을 때 Reference Error를 유발했다.

이유는 그룹 연산자{ } 내부에 있는 foo가 호이스팅되었기 때문이다.


전역 객체와 let

velrlett

var 키워드 로 생성된 전역 변수는 window(최상위 객체)에 암묵적으로 프로퍼티가 된다.
전역 객체의 프로퍼티를 참조할 때 window를 생략할 수 있다.


let 키워드로 생성된 전역 변수는 window의 프로퍼티가 아니다.
따라서, window.x와 같이 접근할 수 없다.
let 전역 변수는 보이지 않는 개념적인 블록 내에 존재하게 된다.


const 키워드

상수 : 변하지 앟는 고정된 값

const 키워드는 상수를 선언하기 위해 사용된다.

하지만, 반드시 상수만을 위해 사용되는 것은 아니며, let 키워드와 대부분 동일한 특징을 지니고 있다.


let 키워드와 비슷한 점이 많이 있기 때문에, 다른점을 언급한다.

선언과 초기화

let 키워드로 선언된 변수는 재할당이 자유롭다. (재선언은 X) const 키워드로 선언된 변수는 재할당이 금지 다.

1
2
3
4
5
6
7
// 0.1은 변해서는 않되는 상수로서 사용될 값이다.
// 변수 이름을 대문자로 선언하여 상수를 저장하고 있음을 명확히 나타낸다.
const TAX_RATE = 0.1;

// const 키워드로 선언한 변수는 재할당이 금지된다.
// 상수는 재할당이 금지된 변수이다.
TAX_RATE = 0.2; // TypeError: Assignment to constant variable.

​ 또한, const 키워드선언한 변수는 선언과 동시에 할당이 이루어져야 한다. ( 에러를 유발한다. )

1
2
3
// const 선언만 할 경우, 에러유발
const FOO;
// SyntaxError: Missing initializer in const declaration

const 키워드로 선언한 변수는 let 키워드로 선언한 변수와 마찬가지로 블록 레벨 스코프를 갖는다.


상수

​ 상수는 가독성, 유지보수의 편의를 위하여 적극적으로 사용해야 한다.

상수로 이루어진 변수의 이름은 대문자로 선언하여 상수임을 확실하게 드러낸다.

또한 여러단어가 겹칠 경우, _(언더 스코어) 로 단어의 네이밍을 구분짓는다.

1
2
3
4
5
6
7
8
9
10
// 세율
const TAX_RATE = 0.1

// 세전 가격
let preTaxPrice = 100;

// 세후 가격
let afterTaxPrice = preTaxPrice + (preTaxPrice * TAX_RATE);

console.log(afterTaxPrice); // 110

세율은 쉽게 바뀌지 않는 값이며 프로그램 전체에서 고정된 값을 사용해야 한다.

상수로 세율을 정의하면 값의 의미를 쉽게 파악할 수 있다.

또한 상수는 프로그램 전체에서 공통 사용하므로 나중에 세율이 변경되면 상수만을 변경하면 되기 때문에 유지보수성이 대폭 향상된다.


const 키워드와 객체

const 키워드로 선언된 변수는 재할당이 금지된다.

더 정확히 말하면, 원시 값을 할당한 경우이다. 원시 값은 변경 불가능한 값이기에 상수에 할당된다면 재할당이 금지되므로 변경할 방법이 없다.

반면에, 객체를 할당한 경우 역시 재할당은 금지되어 있다. 하지만 객체는 변경 가능한 값이므로 객체 자체를 변경하는 것은 가능하다.

1
2
3
4
5
6
7
8
const person = {
name: 'Lee'
};

// 객체는 변경 가능한 값이다.
person.name = 'Kim';

console.log(person); // {name: "Kim"}

const 키워드는 재할당을 금지할 뿐 “불변(immutable)”을 의미하지는 않는다.

다시 말해, 새로운 객체를 재할당하는 것은 불가능하지만 객체의 내용(프로퍼티의 추가, 삭제, 프로퍼티 값의 변경)을 변경하는 것은 가능하다. ( 배열을 추가하거나 삭제하거나 변경하는 것도 포함된다. )

객체의 내용이 변경되더라도 변수에 할당된 주소값은 변경되지 않는다.


var vs. let vs. const

변수 선언에는 기본적으로 const를 사용하고 let은 재할당이 필요한 경우에 한정해 사용하는 것이 좋다.

원시 값의 경우, 가급적 상수를 사용하는 편이 좋다.
그리고 객체를 재할당하는 경우는 생각보다 흔하지 않다.
const 키워드를 사용하면 의도치 않은 재할당을 방지해 주기 때문에 보다 안전하다.

var와 let, 그리고 const 키워드는 다음처럼 사용하는 것을 추천한다.

  • ES6를 사용한다면 var 키워드는 사용하지 않는다.
  • 재할당이 필요한 경우에 한정해 let 키워드를 사용한다. 이때 변수의 스코프는 최대한 좁게 만든다.
  • 변경이 발생하지 않는(재할당이 필요 없는 상수) 원시 값과 객체에는 const 키워드를 사용한다. const 키워드는 재할당을 금지하므로 var, let 보다 안전하다.

변수를 선언하는 시점에는 재할당이 필요할지 잘 모르는 경우가 많다. 그리고 객체는 의외로 재할당을 하는 경우가 드물다.
따라서 변수를 선언할 때에는 일단 const 키워드를 사용하도록 하자.

반드시 재할당이 필요하다면(반드시 재할당이 필요한지 한번 생각해 볼 일이다.)
그때 const를 let 키워드로 변경해도 결코 늦지 않는다.


Share