일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 알고리즘
- 프로그래머스
- 그래프
- 자바
- 프로젝트
- TDD
- DP
- algorithm
- 백준
- 모던자바
- LEVEL2
- 네트워크
- java
- baekjoon
- 스프링
- 자료구조
- programmers
- 코틀린
- 백트래킹
- back-end
- 운영체제
- BFS
- DFS
- kotlin
- lambda
- backtracking
- Spring
- OS
- Java8
- Brute-force
- Today
- Total
요깨비's LAB
[Javascript, ES6] 1. let, const와 블록 레벨 스코프 본문
let foo = 1; // 전역 변수
{
console.log(foo); // Reference Error : foo is not defined
let foo = 2; // 지역 변수
}
ES까지 변수를 선언할 수 있는 유일한 방법은 var 키워드를 사용하는 것이었다. var 키워드로 선언된 변수는 아래와 같은 특징이 있다.
이는 다른 언어들과는 다른 특징이며 이를 간과하고 주의하지 않으면 심각한 문제를 초래한다.
1. 함수 레벨 스코프(Function-level scope)
- 함수의 코드 블록만을 스코프로 인정한다. 따라서 전역 함수 외부에서 생성한 변수는 모두 전역 변수이다. 이는 전역 변수를 남발할
가능성을 높인다. - for 문의 변수 선언문에서 선언한 변수를 for 문의 코드 블록 외부에서 참조할 수 있다.
2. var 키워드 생략 허용
- 암묵적 전역 변수를 양산할 가능성이 크다.
3. 변수 중복 선언 허용
- 의도하지 않은 변수값의 변경이 일어날 가능성이 크다.
4. 변수 호이스팅
- 변수를 선언하기 이전에 참조할 수 있다.
대부분의 문제는 전역 변수로 인해 발생합니다. 전역 변수는 애플리케이션의 경우, 사용이 편리하다는 장점이 있지만 불가피한 상황을
제외하고 사용을 억제해야 합니다. 전역 변수는 스코프가 넓어서 어디에서 어떻게 사용될 것인지 파악하기 힘들고, Impure function에
의해 의도하지 않게 변경될 수 있어서 복잡성을 증가시키는 원인이 됩니다. 따라서 변수의 스코프는 좁을수록 좋습니다.
ES6는 이러한 var 키워드의 단점을 보완하기 위해 let과 const 키워드를 도입하였습니다.
1. let
1.1 블록 레벨 스코프
대부분의 프로그래밍 언어는 블록 레벨 스코프(Block-level scope)를 따르지만 자바스크립트는 함수 레벨 스코프(Function-level scope)를
따릅니다.
함수 레벨 스코프(Function-level scope)
함수 내에서 선언된 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다. 즉, 함수 내부에서 선언한 변수는 지역 변수이며
함수 외부에서 선언한 변수는 모두 전역 변수이다.
블록 레벨 스코프(Block-level scope)
모든 코드 블록(함수, if문, for문, while문, try/catch문 등) 내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드 블록 외부에서는
참조할 수 없다. 즉, 코드 블록 내부에서 선언한 변수는 지역 변수이다.
예제 하나를 보겠습니다.
var foo = 123; // 전역 변수
console.log(foo); // 123
{
var foo = 456;
}
console.log(foo); // 456?!
블록 레벨 스코프를 따르지 않는 var 키워드의 특성 상, 코드 블록 내의 변수 foo는 전역 변수입니다. 그런데 이미 위에 전역 변수 foo가
선언 되어 있는 것을 볼 수 있습니다. var 키워드를 사용하여 선언한 변수는 중복 선언이 허용되므로 위 코드는 문법적으로는 아무런 문제가
없습니다. 단, 코드 블록 내의 변수 foo는 전역 변수이기 때문에 전역에서 선언된 전역 변수 foo의 값 123을 새로운 값 456으로 덮습니다.
ES6는 블록 레벨 스코프를 따르는 변수를 선언하기 위해 let 키워드를 제공한다.
let foo = 123; // 전역 변수
{
let foo = 456; // 지역 변수
let bar = 456; // 지역 변수
}
console.log(foo); // 123
console.log(bar); // ReferenceError: bar is not defined
let 키워드로 선언된 변수는 블록 레벨 스코프를 따릅니다. 위 예제에서 코드 블록 내에 선언된 변수 foo는 블록 레벨 스코프를 갖는
지역 변수입니다. 전역에서 선언된 변수 foo와는 다른 별개의 변수입니다. 또한 변수 bar도 블록 레벨 스코프르 갖는 지역 변수입니다.
따라서 전역에서는 변수 bar를 참조할 수 없다.
1.2 변수 중복 선언 금지
var 키워드로는 동일한 이름을 갖는 변수를 중복해서 선언할 수 있었습니다. 하지만, let 키워드로는 동일한 이름을 갖는 변수를 중복해서
선언할 수 없습니다. 변수를 중복 선언하면 Syntax Error가 발생합니다.
var foo = 123;
var foo = 456; // 중복 선언 허용
let bar = 123;
let bar = 456; // Uncaught SyntaxError: Identifier 'bar' has already been declared
1.3 호이스팅
자바스크립트는 ES6에서 도입된 let, const를 포함하여 모든 선언(var, let, const, function, function*, class)을 호이스팅합니다.
* 호이스팅(Hoisting) : var 선언문이나 function 선언문 등을 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성을 말한다.
하지만, var 키워드로 선언된 변수와는 달리 let 키워드로 선언된 변수를 선언문 이전에 참조하면 Reference Error가 발생합니다.
이는 let 키워드로 선언된 변수는 스코프의 시작에서 변수의 선언까지 일시적 사각지대(Temporal Dead Zone; TDZ)에 빠지기
때문입니다.
console.log(foo); //undefined
var foo;
console.log(bar); // Error: Uncaught ReferenceError: bar is not defined
let bar;
변수가 어떻게 생성되며 호이스팅은 어떻게 이루어지는지 좀 더 자세히 살펴보겠습니다. 변수는 3단계에 걸쳐 생성됩니다.
선언 단계(Declaration phase)
변수를 실행 컨텍스트의 변수 객체(Variable Object)에 등록한다. 이 변수 객체는 스코프가 참조하는 대상이 된다.
초기화 단계(Initialization phase)
변수 객체(Variable Object)에 등록된 변수를 위한 공간을 메모리에 확보한다. 이 단계에서 변수는 undefined로 초기화된다.
할당 단계(Assignment phase)
undefined로 초기화된 변수에 실제 값을 할당한다.
var 키워드로 선언된 변수는 선언 단계와 초기화 단계가 한번에 이루어집니다. 즉, 스코프에 변수를 등록(선언 단계)하고 메모리에
변수를 위한 공간을 확보한 후, undefined로 초기화(초기화 단계)합니다. 따라서 변수 선언문 이전에 변수에 접근하여도 스코프에
변수가 존재하기 떄문에 에러가 발생하지 않습니다. 다만 undefined를 반환합니다. 이후 변수 할당문에 도달하면 그때 비로소
값이 할당 됩니다. 이러한 현상을 변수 호이스팅(Variable Hoisting)이라 합니다.
// 스코프의 선두에서 선언 단계와 초기화 단계가 실행된다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 있다.
console.log(foo);
var foo;
console.log(foo); // undefined
foo = 1; // 할당문에서 할당 단계가 실행된다.
console.lgo(foo); // 1
let 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행됩니다. 즉, 스코프에 변수를 등록(선언 단계)하지만 초기화 단계는
변수 선언문에 도달했을 때 이루어집니다. 초기화 이전에 변수에 접근하려고 하면 Reference Error가 발생합니다.
이는 변수가 아직 초기화되지 않았기 때문입니다. 다시 말하면 변수를 위한 메모리 공간이 아직 확보되지 않았기 때문입니다.
따라서 스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없습니다. 스코프의 시작 지점부터 초기화 지점까지의 구간을
'일시적 사각지대(Temporal Dead Zone; TDZ)'라고 부릅니다.
// 스코프의 선두에서 선언 단계가 실행된다.
// 아직 변수가 초기화(메모리 공간 확보와 undefined로 초기화)되지 않았다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 없다.
console.log(foo); // Reference Error : foo is not defined
let foo; // 변수 선언문에서 초기화 단계가 실행된다.
console.log(foo); // undefined
foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1
결국 ES6에서는 호이스팅이 발생하지 않는 것과 차이가 없어보이지만 아래 예제를 살펴보겠습니다.
let foo = 1; // 전역 변수
{
console.log(foo); // Reference Error : foo is not defined
let foo = 2;
}
위 예제의 경우, 전역 변수 foo의 값이 출력될 것 처럼 보이지만 여전히 호이스팅이 발생하기 때문에 Reference Error가 발생합니다.
ES6의 let으로 선언된 변수는 블록 ㅔ벨 스코프를 가지므로 코드 블록 내에서 선언된 변수 foo는 지역 변수입니다. 따라서 지역 변수 foo도
해당 스코프에서 호이스팅되고 코드 블록의 선두부터 초기화가 이루어지는 지점까지 일시적 사각지대(TDZ)에 빠집니다. 따라서 전역 변수
foo의 값이 출력되지 않고 Reference Error가 발생합니다.
1.4 클로저
블록 레벨 스코프를 지원하는 let은 var보다 직관적입니다.
var funcs = [];
// 함수의 배열을 생성하는 for 루프의 i는 전역 변수다.
for(var i = 0; i < 3; i++) {
funcs.push(function() {console.log(i);}}
}
// 배열에서 함수를 꺼내어 호출한다.
for(var j = 0; j < 3; j++) {
funcs[j]();
}
위 코드의 실행 결과로 0, 1, 2를 기대하겠지만 결과는 3이 세 번 출력됩니다. 그 이유는 for 루프의 var i가 전역 변수이기 때문입니다.
0, 1, 2를 출력하려면 아래와 같은 코드가 필요합니다.
var funcs = [];
// 함수의 배열을 생성하는 for 루프의 i는 전역 변수다.
for(var i = 0; i < 3; i++) {
(function(index){ // index는 자유변수다.
funcs.push(function(){console.log(index);});
}(i));
}
// 배열에서 함수를 꺼내어 호출한다.
for(var j = 0; j < 3; j++) {
funcs[j]();
}
자바스크립트의 함수 레벨 스코프로 인하여 for 루프의 초기화 식에 사용된 변수가 전역 스코프를 갖게 되어 발생하는 문제를 회피하기 위해
클로저를 활용한 방법입니다.(불--편)
ES6의 let 키워드를 for 루프의 초기화 식에 사용하면 클로저를 사용하지 않아도 위 코드와 동일한 동작을 할 수 있습니다!!(편--안)
var funcs = [];
// 함수의 배열을 생성하는 for 루프의 i는 for 루프의 코드 ㅡㅂㄹ록에서만 유효한 지역 변수이면서 자유 변수이다.
for(let i = 0; i < 3; i++) {
funcs.push(function(){ console.log(i);});
}
// 배열에서 함수를 꺼내어 호출한다.
for(var j = 0; j < 3; j++) {
console.log(funcs[j]);
funcs[j]();
}
for 루프의 let i는 for loop에서만 유효한 지역 변수입니다. 또한, i는 자유 변수로서 for 루프의 생명주기가 종료되어도 변수 i를 참조하는
함수가 존재하는 한 계속 유지됩니다.
1.5 전역 객체와 let
전역 객체(Global Object)는 모든 객체의 유일한 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window 객체,
Server-side(Node.js)에서는 global 객체를 의미합니다. var 키워드로 선언된 변수를 전역 변수로 사용하면 전역 객체의
프로퍼티가 됩니다.
var foo = 123; // 전역변수
console.log(window.foo); // 123
let 키워드로 선언된 변수를 전역 변수로 사용하는 경우, let 전역 변수는 전역 객체의 프로퍼티가 아닙니다. 즉, window.foo와 같이
접근할 수 없다. let 전역 변수는 보이지 않는 개념적인 블록 내에 존재하게 됩니다.
let foo = 123; // 전역변수
console.log(window.foo); // undefined
2. const
const는 상수(변하지 않는 값)를 위해 사용합니다. 하지만 반드시 상수만을 위해 사용하지는 않습니다.
2.1 선언과 초기화
let은 재할당이 자유로우나 const는 재할당이 금지됩니다.
const FOO = 123;
FOO = 456; // Type Error : Assignment to constant variable.
주의할 점은 const는 반드시 선언과 동시에 할당이 이루어져야 한다는 것입니다. 또한, const는 let과 마찬가지로 블록 레벨 스코프입니다.
const FOO; // Syntax Error : Missing initializer in const declaration
{
const FOO = 10;
console.log(FOO);
}
console.log(FOO); // Reference Error : FOO is not defined.
2.2 const와 객체
const는 재할당을 금지합니다. const 변수의 타입이 객체인 경우, 객체에 대한 참조를 변경하지 못한다는 것을 의미합니다. 하지만 이때
객체의 프로퍼티는 보호되지 않습니다. 다시 말하자면 재할당은 불가능하지만 할당된 객체의 내용(프로퍼티의 추가, 삭제, 값 변경)은
가능합니다.
const user = { name : 'Lee' };
// const 변수는 재할당이 금지된다.
// user = {}; // Type Error: Assignment to constant variable.
// 객체의 내용은 변경할 수 있다.
user.name = 'Kim';
console.log(user); // {name: 'Kim'}
객체의 내용이 변경되더라도 객체 타입 변수에 할당된 주소값은 변경되지 않는다. 따라서 객체 타입 변수 선언에는 const를 사용하는 것이
좋습니다. 만약 명시적으로 객체 타입 변수의 주소값을 변경(재할당)해야 한다면 let을 사용합니다.
3. var VS let VS const
변수 선언에는 기본적으로 const를 사용하고 let은 재할당이 필요한 경우에 한정해 사용하는 것이 좋습니다. 원시 값의 경우, 가급적 상수,
객체를 재할당하는 경우는 생각보다 흔하지 않기 때문에 const 키워드를 사용하면 의도치 않은 재할당을 방지해 주기 때문에 안전합니다.
var와 let, 그리고 const는 다음처럼 사용하는 것이 좋습니다.
- ES6를 사용한다면 var는 사용하지 않는다.
- 재할당이 필요한 경우에 한정해 let 키워드를 사용한다. 이때 변수의 스코프는 최대한 좁게 만든다.
- 변경이 발생하지 않는(재할당이 필요 없는 상수) 원시 값과 객체에는 const 키워드를 사용한다. const 키워드는 재할당을 금지하므로
var, let 보다 안전하다.
변수를 선언하는 시점에는 재할당이 필요할지 잘 모르는 경우가 많습니다. 그리고 객체는 의외로 재할당을 하는 경우가 드뭅니다. 따라서
변수를 선언할 때에는 일단 const 키워드를 사용하는게 좋습니다. 반드시 재할당이 필요하다면 그때 const를 let 키워드로 변경합시다!
(상수였던게 반드시 재할당이 필요한 경우가 있으려나..?)
해당 내용은 제 개인 학습용 혹은 필요할때 참조용 레퍼런스로 간편하게 찾아보기위해 poiema web의 내용을 그대로 배껴온 것으로
중간중간 저의 느낀점만 숟가락 얹듯이 할 것 같습니다.
https://poiemaweb.com/es6-block-scope
자세한 내용은 해당 링크에 있습니다.
'자바스크립트 > ES6' 카테고리의 다른 글
[Javascript, ES6] 4. 매개변수 기본값, Rest 파라미터, Spread 문법, Rest/Spread 프로퍼티 (0) | 2019.12.11 |
---|---|
[Javascript, ES6] 3. 화살표 함수(Arrow Function) (0) | 2019.12.04 |
[Javascript, ES6] 2. 템플릿 리터럴(Template Literal) (0) | 2019.12.04 |