*해당 포스팅은 김기수의 '코딩 자율학습 HTML + CSS + 자바스크립트', 정재남의 '코어 자바스크립트' 도서를 참고했습니다.*
(Link) 지난 포스팅에서 자바스크립트의 변수의 정의와 할당/선언, 그리고 메모리 구조에 대해서 알아봤다.
이번 포스팅에서는 자바스크립트의 변수의 종류는 무엇이 있는지, 각 특징은 무엇인지 알아보겠다. 종류는 총 3가지이다.
하지만 내용이 길어지는 관계로 const는 다음 주제에서 다룬다.
자바스크립트 변수의 종류
그 전에, 변수마다 사용되는 스코프가 다르다. 스코프(scope)는 식별자에 대한 유효 범위를 이야기한다. 그 식별자가 어느 범위까지 살아있는가?를 보면 된다.
스코프 (Scope)
스코프는 변수나 함수와 같은 참조 대상 식별자를 찾아내기 위한 규칙이다.
자바스크립트는 기본적으로 스코프에 따라 참조하려는 식별자를 찾는다.
자바스크립트에는 함수 스코프, 블록 스코프, 전역 스코프, 지역 스코프가 있으며, 각 참조 밤위가 달라진다.
함수 스코프
함수에서 정의한 블록문만 스코프의 유효 범위로 인정하는 방식이다. 함수 내부는 지역 스코프, 함수 외부는 전역 스코프 영역이 된다.
let a = 10;
function sum(){
console.log(`함수 내부: ${a}`);
}
sum();
console.log(`함수 외부: ${a}`);
백틱(`): ES6에서 새로 도입된 문자열 표현 방법. 이전에는 줄바꿈을 \n을 사용해야 했다면, 백틱은 자유롭게 표현 가능
또한, 기존 따옴표는 서로 다른 자료형을 +로 이용해서 이어 붙였다면 ${}를 사용해 변수와 함께 사용이 가능하다.
위와 같은 예시에서 결과는
함수 내부: 10
함수 외부: 10
와 같이 나왔을 것이다. a가 함수 바깥의 전역 스코프로 선언 및 할당되어 있고, 전역 스코프는 스코프 상관없이 모두 참조할 수 있기 때문이다. 그렇다면, 지역 스코프의 경우는 어떤 예시가 있을까?
function sum() {
let a = 10; //지역 스코프
console.log(`함수 내부: ${a}`);
}
sum();
console.log(`함수 내부: ${a});
아마 첫번째 출력은 10이 제대로 출력되었으나 두번째 출력은 ReferenceError가 발생할 것이다.
a가 함수 sum 안의 지역 스코프로 선언되었기 때문이다. 따라서 함수 내부가 아닌 외부에서 참조하려고 하면 오류가 발생하는 것이다.
var
앞서 스코프와 함수 스코프를 알아봤다. 이제 자바스크립트 변수의 한 종류이며 가장 오래된 변수 var를 알아보자.
var는 함수 스코프를 가지는데,
function add() {
var i = 100;
for (int i =0;i<=5;i++){
console.log(i);
}
}
위와 같은 경우 함수 첫행에서 쓰인 i가 반복문의 i에 의해 중복된다.
즉, add 함수 내에서 같은 이름인 i를 덮어씌워버리는 것.
왜? - var 호이스팅
그렇다면 왜 var는 같은 함수 블록 내 변수와 중복될까? 여기서 var 호이스팅이 일어나기 때문이다.
호이스팅이란, Hoist(끌어올리다) + ing의 합성이다. 변수 정보를 수집하는 과정을 더욱 이해하기 쉬운 방법으로 대체한 가상의 개념이다. JS 엔진이 실제로는 끌어올리지 않지만 편의상 끌어올린 것으로 간주하는 것.
예제 코드를 살펴보자.
function a (x) {
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
a(1);
만약 이 상황이라면 콘솔이 어떻게 출력될까? 아마 1, undifned, 2처럼 출력되지 않을까? 하지만 그렇지 않다.
function a () {
var x; // 매개변수
var x; // 2번째 변수만 선언한 경우
var x; // 3번째 변수를 2로 선언 및 할당 한 경우
x = 1;
console.log(x); // 매개변수 1
console.log(x); // 2번째 변수만 선언한 경우
x = 2;
console.log(x); // 3번째 변수를 2로 선언 및 할당 한 경우
}
a(1);
이상하게도 변수 선언이 위로 끌어올려졌다. 실행 컨텍스트 중 하나인(다음 포스팅에 다뤄보겠다.) environmentRecord는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 함수에 지정된 매개변수 식별자, 선언한 함수가 있을 경우 그 함수 자체, var로 선언된 변수의 식별자 등이 식별자에 해당한다. 컨텍스트 내부를 처음부터 끝까지 훑으면서 순서대로 수집한다.
즉, 선언된 식별자들에만 관심이 있고 어떤 값이 할당 됐는지는 관심이 없다는 얘기다.
그래서 변수를 호이스팅할 때 변수명만 끌어올리고 할당 과정은 원래 그 자리에 남겨둔다. (매개변수도 마찬가지)
따라서 결과는 1, 1, 2가 출력된다.
함수 선언의 호이스팅의 예를 한가지 더 살펴보자.
function a () {
console.log(b);
var b = 'bbb';
console.log(b);
function b () {}
console.log(b);
}
a();
이 경우 순서대로 읽었을 때 아마 에러, 'bbb', b함수가 출력될 것이라 예상할 것이다.
하지만 호이스팅 과정을 거친다고 생각한다면,
function a () {
var b;
function b () {}
console.log(b);
b = 'bbb';
console.log(b);
console.log(b);
}
a();
b의 선언, 함수의 선언이 가장 먼저 실행되는 것을 볼 수 있다.
하지만 여기서, 호이스팅이 끝난 상태에서 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있다.
function a () {
var b;
var b = function b () {}
console.log(b);
b = 'bbb';
console.log(b);
console.log(b);
}
a();
결과는 'b함수', 'bbb', 'bbb'로 나타난다.
위의 예제들을 보아하니 var 키워드는 중복 선언이 가능함(재선언)을 알 수 있다. 이것은 실제 변수를 사용할 때 기존의 변수를 덮어버릴 수 있다는 장점처럼 보일 수 있다. (실제로는 다른 주소를 가리키게 되고, 이전 것은 참조 카운트가 0이면 GC가 처리한다.)
이런 상황을 가정해보자. 만약 대형 프로젝트를 진행할 때 A개발자와 B개발자가 같은 변수명을 사용한다면?
var 키워드를 사용해서 정의했을 때는 자바스크립트가 문제가 없다고 할 것이다.
A개발자는 sum을 10으로 정의했는데, B개발자가 sum을 40으로 정의해 A개발자가 나중에 꺼내보니 10이 아니라 40이 된다는 것.
때문에 계속해서 다른 변수(i_1, i_1_2, i_1_3 .....)를 써야해 변수 이름이 길어진다. 이름이 길어지면, 당연히 가독성이 저하되고 후에 변수를 검색할 때 매우 불편하다.
이런 현상을 해결하기 위해 아래 2개 종류의 변수가 등장했다. (내용이 길어지는 관계로 const는 다음 포스팅에 다룬다.)
let
let은 ES6(자바스크립트 표준화)에서 새로 추가됐다. let은 함수 스코프가 아닌 '블록 스코프'다.
블록 스코프
var를 보면 알겠지만, 자바스크립트에선 원래 함수 스코프만 사용했다. 하지만 ES6에서 let과 const 키워드가 추가되면서 블록 스코프를 지원하게 됐다. 블록 스코프는 {}로 구성된 블록문 기준으로 스코프의 유효 범위를 나누는 방식이다.
예제로 살펴보겠다.
let a = 10;
{
let b = 20;
console.log(`코드 블록 내부 a: ${a}`);
console.log(`코드 블록 내부 b: ${b}`);
}
console.log(`코드 블록 외부 a: ${a}`);
console.log(`코드 블록 외부 b: ${b}`);
----결과
코드 블록 내부 a: 10
코드 블록 내부 b: 20
코드 블록 외부 a: 10
ReferenceError: b is not defined
a는 전역 스코프에, b는 첫번째 블록 내부에 선언되었다.
a는 내부든 외부든 상관없이 출력할 수 있다. 그러나 b는 블록 내부에 선언되었기 때문에 블록 외부에서 참조하려하면 참조 에러가 발생한다.
이때 전역 스코프에 선언한 변수를 전역 변수, 지역 스코프에 선언한 변수를 지역 변수라고 한다.
그러면 var를 지역 스코프에 선언한다면?
상관없다. var는 함수 스코프 방식으로만 나누기 때문에 전역 스코프에서 선언한 것과 마찬가지다.
var a = 10;
{
var b = 20;
console.log(`코드 블록 내부 a: ${a}`);
console.log(`코드 블록 내부 b: ${b}`);
}
console.log(`코드 블록 외부 a: ${a}`);
console.log(`코드 블록 외부 b: ${b}`);
----결과
코드 블록 내부 a: 10
코드 블록 내부 b: 20
코드 블록 외부 a: 10
코드 블록 외부 b: 20
let과 var는 재할당이 가능하다
앞서 우리는 '할당'이란 메모리의 일정 부분의 몫을 차지해서 데이터를 저장하는 것이라고 익혔다. 약간의 다름은 있으나 구조상 거의 비슷하다. 자바스크립트에서는 첫번째 메모리를 변수 영역, 두번째 메모리를 데이터 영역이라고 한다.
▼이전 포스팅 ▼
https://dataeun.tistory.com/44
[JS] 자바스크립트 변수의 정의, 선언/할당, 메모리 구조
*해당 포스팅은 김기수의 '코딩 자율학습 HTML + CSS + 자바스크립트', 정재남의 '코어 자바스크립트' 도서를 참고했습니다.* 1. 변수란? 변수란 흔히 '변할 수 있는 수'라고 알려져있다. '수'라고 해
dataeun.tistory.com
같은 내용이지만, 도서에서 표현하는 것처럼 다시 한 번 할당에 대해 알아보자.
JS에서는 식별자에 데이터를 할당할 때 변수 영역과 데이터 영역이 따로 있다고 했다.
이름(식별자)은 name, 데이터는 "seo"라는 문자열이다. 변수 영역에 이름과 주소가 저장되었고 주소를 따라가보면 그 주소에 할당한 데이터가 존재한다.
name을 "kim"이라는 데이터로 재할당했다. 앞서 말한 것과 같이, 기존의 데이터는 참조 카운트가 0이 되므로 가비지 컬렉터의 대상이 된다. 빈 공간에 다시 "kim"을 저장한 후, name이라는 이름을 가진 주소가 @8102을 참조하게 된다.
그렇다면, 서로 다른 변수가 같은 데이터를 할당한다면 어떻게 될까?
자바스크립트가 데이터를 할당할때는 메모리에서 같은 값이 있는지 먼저 찾는다. 만약 있다면 그것을 재활용한다.
만약 name을 바꾸려고 한다면, 기존의 kim을 지우는 것이 아니라 같은 것이 있는지 먼저 찾고 없다면 빈공간을 활용해 데이터를 저장한다.
불변하는 값이다
잘 살펴보니, var와 let은 다른 값으로 바꾸려 할 때마다 새로운 메모리를 재할당한다.
다른 말로 몇가지 데이터들을 '불변값'이라고 표현하는데, 여기에는 기본형(원시) 데이터인 숫자, 문자열, boolean, null(없다는 것이 아니다), undefined, Symbol이 있다. 그 외는 모두 변경이 가능한데, 아래에서 알아보겠다.
거의 모든 형들이 객체로 이루어져있기 때문에 객체와 배열 형태만 바꿀 수 있다!라고만 단순하게 알면 위험할 수 있다.
가변값을 알아보자
그렇다면 가변할 수 있는 값을 알아보겠다.
도서에서는 이해하기 쉽게 객체 데이터를 참조형 데이터라고 표현했지만, 엄밀히 따지면 기본형(원시)도 주소를 참조하기는 한다. 그러나 관리하는 방식이 다르기 때문에 차이는 있다.
속성 a와 b를 가진 obj1을 선언했다고 가정하자.
1. 먼저 변수 영역에 obj1을 저장한다. 이 때, 값은 주소 @8100에 저장된다.
2. @8100에 가보니, 임의로 2000부터 임의의 영역을 할당한다. (객체의 프로퍼티를 저장하기 위한 영역은 크기가 정해져 있지 않고 동적이다.)
3. @2000부터는 엄연히 객체의 변수 영역이다. 데이터는 데이터 영역에 저장되기 때문에, 프로퍼티 당 데이터를 8102, 8103에 저장한다.
객체의 저장 방식을 알아보았으니 프로퍼티 재할당에 대해서도 예를 들어보겠다.
그림의 왼쪽 위와 같이 obj1.a를 1에서 2로 변경했다.
변수 영역의 obj1의 주소가 변경이 되지 않았는데, 왜일까?
노랗게 표시된 부분을 보면 알 수 있다. 간단히 설명하자면 기존의 객제 내부의 값만 바뀐 것이다.
그러니까, 이름 obj1이 참조하고 있는 곳의 데이터가 변한 것이 아니라 obj1이 참조하고 그 데이터가 참조하는 객체의 변수가 변한 것이기 때문에 (아주 엄밀히 말하면 아예 안 변한 건 아니긴 하다).
중첩 객체의 경우를 살펴보자
var obj = {
x: 3,
arr: [3,4,5]
}
조금 복잡할 수 있으니 순서대로 기재해보겠다.
1. 우선 빈 공간 @100을 확보하고, 주소의 이름을 obj라고 지정한다.
2. obj의 값을 데이터 영역의 @8100에 저장하려 하는데, 이 데이터는 여러 개의 변수와 값들을 모아놓은 그룹(객체)이다.
이 그룹의 속성들을 저장하기 위해 변도의 변수 영역을 따로 만든다. 동적이기 때문에, @2000~?로 지정한다.
3. @2000에 x를, @2001에 arr을 저장한다.
4. 데이터 영역에 3이 없으므로 빈 공간인 @8101에 3을 저장한다. @2000에 x의 값이 @8100을 참조한다.
5. arr 역시 객체므로 따로 변수 영역을 만든다.(@9200~)
6. 배열의 요소가 총 3개이므로 3개의 공간을 확보하고 이름을 인덱스로 저장한다.
7. 데이터 영역에서 각 숫자들을 검색한다. 있다면 해당 주소를 저장하고, 없다면(4와 5) 데이터 영역의 빈 공간에 저장한다.
객체(참조형 데이터라고 표현된다.)일 경우 할당 과정을 알아보았다. 그렇다면, 기본형과 객체형의 큰 차이는 무엇일까?
아래와 같은 예시를 보자.
var a = 10;
var b = a;
var obj1 = [c: 10, d: 'ddd'}
var obj2 = obj1;
b = 15;
obj2.c = 20;
변수 a는 10으로 선언되었고 이를 변수 b가 복사했다. obj1을 obj2가 복사했다.
변수 b가 15로 변경되었고, obj2의 프로퍼티가 20으로 변경되었다.
우선 마지막 두줄을 제외하고 메모리의 변화를 보자면 위 그림과 같다.
b는 a를 복사하기 때문에 같은 주소를 바라보고 있는 걸 알 수 있고, obj2 역시 마찬가지고 obj1을 복사하기 때문에 같은 데이터 영역을 참조하고 있음을 알 수 있다.
이제 나머지 두 줄, 즉 변경 후에 어떻게 변하는 지 알아보겠다.
파란색으로 표시된 부분은 기본형이었던 숫자, 주황색은 객체다.
분명 두 가지 다 변경이 이뤄진 것은 사실이다. 그런데 기본형인 a,b는 참조하는 주소가 달라졌고 객체인 obj1과 obj2는 참조하는 주소가 여전히 똑같다. 왜 이럴까?
기본형은 한 번, 객체(참조형)은 두 번
기본형은 복사가 한 번 이뤄진다. 객체 변수 영역으로 갈 필요 없이 변수 영역에서 바로 데이터 영역의 값을 서칭하면 된다.
그러나 객체(참조형)는 객체 변수 영역까지 한 단계를 더 거치게 된다.
필자의 생각대로 작성해보자면, 기본형은 날 것 그대로를 변경하는 반면에 객체는 객체가 바뀌는 것이 아니라 프로퍼티(속성)이 바뀌는 것이기 때문이라고 생각한다. 냄비를 만들 때 철을 넣든 알루미늄을 넣든 냄비라는 사실은 바뀌지 않는 것과 같이. 하지만 당근은 당근 그대로다. 당근을 임의로 변경할 수가 없다.
.
.
.
.
.
지금까지 let에 대한 내용인데, 결론적으로 let은 재할당이 가능하다. 그 이유를 알기 위해서 할당했을 때의 메모리 변화를 기록하자니 이야기가 길어졌다.
function add() {
let i = 100;
for (int i =0;i<=5;i++){
console.log(i);
}
}
위의 var와 똑같은 예제에서 let으로만 바꿨다.
앞서 이야기 했던 A개발자와 B개발자의 이야기를 기억할 것이다. var는 재선언이 가능하기 때문에 협업할 때 변수 중복 사용의 우려가 생긴다. 그렇기 때문에 var의 장점인 재할당은 살리고, 단점인 재선언을 드롭한 것이 let이다.
다시 말해, let은 재선언 불가능, 재할당 가능이다.
위 코드에서는 실행 단위인 '블록'을 add함수와 for문 2개로 볼 수 있다. let은 블록 스코프 단위로 유효하다는 사실도 기억을 할 것이다. 그렇기 때문에 add 함수 첫줄의 i와 반복문의 i는 별개로 사용할 수 있다.
물론 add 함수 외부에서 함수 첫줄의 i와 반복문 i를 접근할 수 없다.
let num = 10 + 20;
let num = 50;
The symbol "num" has already been declared
var의 경우였다면 문제가 되지 않았을 것이다. 위와 같이 선언했을 경우 에러가 나타나는데, 잘 읽어보면 num이 이미 선언되었다고 말하고 있다. let은 재선언이 안 되는 것이다. var와 달리 호이스팅도 되지 않는다.
'Programming > JavaScript' 카테고리의 다른 글
[JS] 자바스크립트 실행 컨텍스트, 스코프 체인, environmentRecord/outerEnvironmentReference (0) | 2024.03.26 |
---|---|
[JS] 자바스크립트 조건문, 반복문 (0) | 2024.03.24 |
[JS] 자바스크립트 함수, 순수 함수/일급 함수,함수 표현식/함수 선언식/화살표 함수, 함수 호이스팅 (0) | 2024.03.24 |
[JS] 자바스크립트 const, 깊은 복사/얕은 복사 (0) | 2024.03.23 |
[JS] 자바스크립트 변수의 정의, 선언/할당, 메모리 구조, 리터럴 (0) | 2024.03.23 |