목차
1. 스코프란?
1-1. 렉시컬 환경
2. 스코프의 종류
2-1. 스코프 체인
2-2. 함수 레벨 스코프
3. 전역 변수
3-1. 문제점
3-2. 해결방법
4. let과 const
4-1. 호이스팅
4-2. 무엇을 사용해야 할까?
1. 스코프란?
ES6의 주요한 변화로 let과 const 키워드로 변수를 선언할 수 있게 되었습니다. 이는 var 키워드의 변수와도 차이점이 있으며, 이러한 스코프는 자바스크립트를 포함한 모든 프로그래밍 언어에서 사용되는 중요한 개념입니다.
모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)은 자신이 선언된 위치에 의해 다른 코드가 식별자인 자기 자신을 참조할 수 있는 유효 범위가 결정되며 이를 스코프라고 합니다. 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙으로 이 스코프 개념을 사용하고 있습니다.
var x = 'global'
function foo() {
var x = 'local'
console.log(x) //'local'
}
foo();
console.log(x) //'global'
위와 같은 코드가 있다면 x라는 이름의 변수가 2개인 것 처럼 보입니다. 이렇듯 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조해야 할 것인지를 결정하는 것을 식별자 결정(Identifier resolution)이라고 합니다.
이 식별자 결정은 자바스크립트 엔진이 코드를 실행할 때 '코드의 문맥' 을 고려하면서 이루어집니다.
여기서 말하는 코드의 문맥이란 바로 렉시컬 환경(lexical environment)이라고 하는데요, 렉시컬 환경에 대해 설명하자면, 렉시컬 환경은 크게 두 개의 컴포넌트로 구성되어 있습니다.
1-1. 렉시컬환경
1) 환경 레코드(Environment Record): 스코프에 포함되어 등록된 식별자에 바인딩 된 값을 관리하는 저장소입니다.
2) 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference): 상위 스코프를 가리키며 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 '상위' 코드의 렉시컬 환경입니다. 상위 스코프를 참조하니 스코프도 계층적으로 연결되어 있겠죠? 그래서 이 참조를 통해 링크드 리스트 구조인 스코프 체인을 구현하게 됩니다.
2. 스코프의 종류
스코프는 전역(Global)와 지역(Local)으로 구분할 수 있습니다. 전역에서 선언된 변수는 전역 스코프를 갖는 전역 변수이고, 지역에서 선언된 변수는 지역 스코프를 갖는 지역 변수입니다.
전역에 변수를 선언하면 전역 스코프를 갖는 전역변수가 됩니다. 이 전역 변수는 어디서든 참조할 수 있습니다(함수 내부에서도 가능.) 반면에 함수 몸체 내부에서 변수를 선언하면 지역 스코프를 만드는 지역변수가 됩니다. 이는 오직 자신의 지역 스코프와 하위 지역 스코프에서 유효합니다.
2-1. 스코프체인
함수는 중첩될 수 있습니다. 이에 따라 렉시컬 환경도 자신의 상위 외부 함수 변수를 참조할 수 있으며 스코프 또한 함수의 중첩에 의해 계층적인 구조를 갖게 됩니다. 이것을 스코프 체인이라합니다.
자바스크립트 엔진이 소스코드를 실행할 때 변수를 참조하는 과정을 필요로 하면 바로 이 스코프 체인을 통해 해당 스코프에서 시작하여 자신의 상위 스코프 방향으로 이동합니다. 이 행위를 검색 identifier resolution이라고 합니다.
이 스코프체인은 실행 컨텍스트의 '렉시컬 환경'을 단방향으로 연결한 링크드 리스트입니다. 변수 식별자들은 이 렉시컬 환경에 key로 등록되고, 변수 할당이 일어나면 이 자료구조의 변수 식별자에 해당하는 값으로 변경합니다. undefined -> 3
당연하게도 상위 스코프의 변수는 하위 스코프에서 참조할 수 있지만, 하위 스코프에서 상위 스코프의 변수를 참조할 수는 없습니다.
2-2. 함수 레벨 스코프
대부분의 프로그래밍 언어는 if, for, while 등 코드 블록 또한 지역 스코프를 만듭니다. 그러나 자바스크립트에서는 var 키워드로 선언된 변수는 오로지 함수의 코드블록(함수 몸체)만을 지역 스코프로 인정하기 때문에 주의해야합니다.
즉, 블록 레벨에서는 지역 스코프가 인정되지 않기 때문에 블록 안에 var를 재선언하면 그 자체로 전역 변수가 된다는것을 의미합니다.
코드를 보면
var x = 1;
if (true) {
var x = 10;
}
console.log(x)
x를 출력하면 10이 나오게 됩니다. if 블록에서 var을 재할당 했지만, 함수 내에서 하지 않았기 때문에 지역 스코프를 생성하지 않고 전역 변수로 재할당이 되어 x는 10이 나옵니다.
3. 전역 변수의 문제점
3-1. 문제점
변수는 선언에 의해 생성되고 할당을 통해 값을 가지게 되기 때문에 생명주기 life cycle을 가지고 있습니다. 이 생명주기가 길수록 메모리 공간을 계속해서 점유하게 되고 이는 효율적이지 못한 코드가 됩니다.
변수는 자신이 선언된 위치에서 생성되고 소멸되는데, 지역 변수는 함수가 호출될 때 생성되고 함수가 종료되면 소멸합니다. (클로저 제외) 호출 되기 전에는 생성되지 않습니다. 그렇다면 전역변수라면 프로그램의 라이프 사이클과 같아서 시작부터 종료까지 쭉 메모리 공간을 점유하게 되겠죠?
var x = 'global';
function foo() {
console.log(x); //'undefined'
var x = 'local'
}
foo();
console.log(x) //'global'
foo() 내부 함수의 console.log(x)가 실행되기 이전에 런타임 전 호이스팅 된 x가 undefined로 초기화 되어있을 것입니다. 따라서 foo() 함수를 실행하면 'local'을 할당하여 'local'을 출력합니다. 호이스팅은 변수 선언이 스코프의 선두로 올려진 것 처럼 동작하는 특징을 말합니다.
반면에 전역변수의 생명주기는 어떨까요? var 전역변수는 전역 객체의 프로퍼티가 되어 전역 객체의 생명주기와 같아집니다. 여기서 전역 객체란 코드 실행 이전에 자바스크립트 엔진에 의해 가장 먼저 생성되는 특수 객체입니다.
전역 객체는 브라우저에서는 window를 의미하며 표준 빌트인 객체 (Object, String, Array ...) 와 web API등의 호스트 객체(browser에 따라 다른 객체) , 그리고 var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖습니다. => 전역 실행 컨텍스트 생성!
브라우저 환경에서 전역 객체는 window 이며 var로 선언한 전역 변수는 window의 프로퍼티가 됩니다. 따라서 이는 웹 페이지를 닫기 전까지 유효하다는 것이고, var 키워드로 선언한다면 이 전역 객체의 생명주기와 일치하기 때문에 메모리 관리에 매우 좋지 않겠죠?
이렇듯 생명주기가 길다는 단점뿐만 아니라, 전역변수처럼 유효범위(스코프)가 크다면 가독성은 나빠지고 의도치않게 상태가 변경될 수 있습니다. 또한, 스코프 체인에서 전역변수는 가장 끝 종점에 위치하므로 전역 변수의 검색 속도가 가장 느립니다.
3-2. 해결방법
코드를 작성할 때 고려해야할 것은 '변수의 스코프는 좁을수록 좋다'라는 것입니다.
전역변수를 써야 할 이유가 없다면 지역변수를 사용해야하고, 즉시 실행함수로 지역변수로 만들어주거나 ((소스코드)()); ES6모듈을 사용할수도 있지만 구형 브라우저에서는 동작하지 않아 모듈 번들러를 사용해야한다는 단점이..있습니다. 이러한 방식은 자바스크립트의 특징이므로 주의가 필요합니다.
그러나 함수 레벨 스코프, 중복 선언 가능 등의 이러한 var 키워드의 단점을 보완하기 위해 let과 const가 도입되어 크게 개선되게 됩니다.
4. let과 const
4-1. 호이스팅
var 키워드로 선언한 변수는 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 '선언단계'와 '초기화단계'가 한번에 진행됩니다. (렉시컬 환경에 식별자를 선언한 후, 즉시 undefined로 초기화 됨)
즉 변수 선언문 이전에 참조할 수 있으며 만약 할당문 이전에 변수를 참조하면 undefined를 반환합니다.
let과 const도 호이스팅이 될까요?
4-2. let 키워드
let은 변수 중복 선언을 금지하기 때문에 같은 이름으로 변수를 더 선언할 수 없습니다. 또한 var 은 제공하지 못했던 블록레벨 스코프를 인정하여 if, for, while 등에서도 지역 스코프를 생성합니다.
let 키워드는 이 '선언 단계'와 '초기화 단계'가 분리되어 진행되기 때문에 할당문 이전에 참조하면 reference error 를 보게 됩니다. 이렇듯 듯 let은 스코프의 시작 시점부터 초기화 시작 시점까지 갭이 있는데 이 구간을 일시적 사각지대라고 부릅니다.
console.log(foo) // reference error
let foo;
console.log(foo) // undefined
foo = 1;
console.log(foo) // 1
이렇게 보면 let은 호이스팅이 되지 않는 것 처럼 보입니다. 그러나 아래 코드를 실행해보면
let foo = 1; // 전역변수
{
console.log(foo); //reference error
let foo = 2; // 지역변수
}
위 블럭에서 console.log는 reference error가 뜹니다. 만약 호이스팅이 되지 않았다면 전역변수의 foo를 참조했겠죠? 그러나 참조 에러가 뜨는 것을 보면 호이스팅이 되었다는 것을 알 수 있습니다. 단, ES6에서 도입된 let, const, class를 사용한 선언문은 호이스팅이 발생하지 않는 것 처럼 동작하는 것일 뿐입니다.
4-3. const 키워드
const 키워드는 상수 constant를 선언하기 위해 사용하나 반드시 그런 것은 아닙니다. const 를 사용할 때 주의할 점은 선언과 동시에 초기화해야한다는 것이고 재할당이 금지됩니다. (상수 = 재할당이 금지된 변수)
특히 원시 값을 할당한 경우 원시 값은 변경할 수 없는 Immutable 한 값이고, 재할당도 금지되므로 변경할 수 있는 방법이 없습니다.
그러나 '객체'를 할당한 경우 값을 변경할 수 있습니다. 이는 재할당 없이도 직접 변경이 가능하기 때문입니다.
const person = {
name: '나영',
age: 19
}
person.age = 24
console.log(person.age)
즉 const는 재할당을 금지할 뿐 '불변'을 의미하지는 않습니다.
무엇을 사용해야할까?
- 재할당이 필요한 경우에 한정해 let을 사용한다. 그리고 스코프는 최대한 좁게!
- 읽기 전용으로만 사용하는 원시값과, 객체에는 const 키워드를 사용한다. 이는 재할당을 금지하므로 var이나 let보다 안전하다. 따라서 우선 const로 사용하고, 이후 재할당이 필요하다면 let으로 바꾸는것도 방법입니다 :)
'Computer Programming > Javascript' 카테고리의 다른 글
json-server 사용해보기 (0) | 2023.06.30 |
---|---|
타입스크립트(TypeScript)의 특징 (0) | 2023.06.27 |
WIL - JS ECMAScript(ES)란? ES5와 ES6의 주요 발전 (0) | 2023.06.18 |
Browser - 브라우저의 구조와 렌더링 과정 (0) | 2023.06.15 |
자바스크립트의 비동기 처리 동작 원리 | 콜스택, 이벤트루프, Promise, fetch, async/await (1) | 2023.06.14 |