서론
아무래도 하늘이 날 버린 것 같다. 작성하던 9일차 글이 날라갔다. 😱
인터넷이 연결되어 있는 줄 알았는데 제대로 연결이 안 되어있던 탓이다. 이제 오늘 중으로 30000~50000자 정도를 전부 써야 TIL을 맞춰서 따라갈 수 있다.
살려ㅈㅜㅓ...
이번 일자는 자바스크립트 기초 - 데이터와 형태다.
메모리와 변수
메모리
데이터를 처리하는 과정에서 처리 흐름마다 값들을 기억해둘 필요가 있다.
컴퓨터는 데이터를 기억하기 위해 메모리를 활용한다.
- 메모리
- 데이터를 담아 기억하는 곳.
- 각 데이터마다 위치 주소값이 존재.
- 위치를 찾는데에는 메모리의 주소값이 사용.
즉, 메모리라는 것은 컴퓨터의 기억장치를 뜻한다.
이러한 메모리는 물리적으로 다양한 종류가 있으며, 용도면에서도 크게는 2가지 종류(주기억장치, 보조기억장치)로 나뉜다.
변수와 식별자
그리고 변수는 어떤 값을 저장하기 위해 확보한 메모리의 공간 or 어떤 메모리 공간을 식별하기 위해 붙인 이름이다.
즉, 값의 위치를 가리키는 '별칭'이라고 할 수 있다.
참고로 식별자(Identifier)는 변수(Variable)와 비슷하면서도 다른 말이다.
정확하게는 변수가 변할 수 있는 데이터(수)의 공간이라면, 변수명은 여기서 해당 데이터 공간에 별도의 이름을 붙인 것이다.
그리고 어떤 값을 구별해서 식별할 수 있는 고유한 이름을 말하는 것이 식별자이다.
그렇기에 '값'이 아닌, 메모리 공간의 위치인 주소를 기억하고 있다.
어떤 값을 '구별', 고유한 '이름'이라는 면에서 여러모로 변수명과 같다고 할 수 있다.
다만, 의미 상으로는 식별자가 조금 더 포괄적인 편이다.
왜냐하면 변수, 함수, 클래스 등 메모리상에 존재하는 어떠한 값을 식별할 수 있는 이름은 모두 식별자라고 하기 때문이다.
혼동하지 않는 것이 좋으나, 변수명을 식별자라고 하는 경우도 많으니 대충 눈치껏 알아듣자.
자바스크립트 변수
let value_name = 123;
// let/const/var: 키워드
// value_name: 변수명
// '=': 할당 연산자
// 123: 값
자바스크립트에서는 변수를 다음과 같은 방법으로 선언한다.
그리고 여기서 키워드라는 것은 자바스크립트 엔진이 수행할 동작을 규정한 명령어라고 할 수 있다.
즉, 어떤 변수 키워드냐에 따라 서로 다른 동작이 이뤄진다는 뜻이다.
그리고 자바스크립트에서 변수라고 할만한 키워드 3가지는 let, const, var이 있다.
그리고 변수에 명시한 고유한 식별자를 변수명이라 하고, 변수로 참조할 수 있는 데이터가 변수값이다.
네이밍 컨벤션
네이밍 컨벤션(Naming Conventions)은 식별자를 만들 때 가독성이 좋도록 단어를 한눈에 구분하기 위해서 규정한 명명 규칙이다. 쉽게 말하자면 변수 이름, 타입, 함수 등의 명칭을 결정하기 위한 규칙이라고 할 수 있다.
이러한 네이밍 컨벤션은 코드 작성 시 일관성 충족 및 재사용시 더 나은 이해를 위해 추구된다. 코드를 다시 보았을 때 어느 정도 이해하기 쉽게 보이기 위한 규칙 중 하나라고 보면 된다.
아래는 이러한 네이밍 컨벤션 표기법(Case) 목록이다.
카멜 케이스(Camel Case)
원문: Camel Case Example
적용: camelCaseExample
Lower Camel Case라고도 부른다.
낙타의 등과 비슷하다고 하여서 이런 이름이 지어졌다고 한다.
기본적으로 식별자에서 띄어쓰기를 없에되, 단어는 소문자로 표기한다.
이후 연결되는 단어부터는 첫 글자를 대문자로 표기한다.
수많은 언어가 권장하는 가장 기본이되는 표기법이다.
파스칼 케이스(Pascal Case)
원문: Pascal Case Example
적용: PascalCaseExample
Upper Camel Case라고도 부른다.
파스칼 프로그래밍 언어의 경우 첫 문자는 모두 대문자로 표기하기에, 거기서 유래되었다고 한다.
기본적으로 식별자에서 띄어쓰기를 없에되, 단어는 소문자로 표기한다.
이후 연결되는 단어부터는 첫 글자를 대문자로 표기한다.
네임스페이스, 이벤트, 프로퍼티, 클래스 네임을 지정할 때 주로 사용한다.
(특히 클래스에 주로 사용된다.)
스네이크 케이스(Snake Case)
원문: Snake Case Example
적용: snake_case_example
Pothole Case라고도 부른다.
코드가 뱀의 형상을 한 것 같다고 하여서 이런 이름이 지어졌다고 한다.
기본적으로 띄어쓰기를 언더바( _ )로 전부 대체하는 방식이라고 할 수 있다.
이외의 부분은 전부 소문자로 표기한다.
데이터베이스, C++, Python에서 주로 권장되는 방식이다.
대문자 스네이크 케이스(Upper Snake Case)
원문: Snake Case Example
적용: SNAKE_CASE_EXAMPLE
상수 케이스(Constant Case), 스크리밍 스네이크 케이스(Screaming Snake Case), 매크로 케이스(Macro Case)라고도 부른다.
스네이크 케이스의 파생에 가까우며, 해외에서는 스크리밍 스네이크 케이스라고 주로 부르는 것 같다.
띄어쓰기를 언더바( _ )로 대체하되.
모든 내용을 소문자가 아닌 대문자로 만드는 방식이다.
각종 언어의 상수(재할당이 불가능한 값)로 주로 사용된다.
이는 자바스크립트도 마찬가지지만, 자바스크립트의 경우에는 const가 있기에 사용을 안 하는 경우도 많다.
이러한 경우에는 대문자 스네이크 케이스 대신.
const + 카멜 케이스의 조합으로 'const camelCase'와 같은 방식으로 사용한다.
케밥 케이스(Kebab Case)
원문: Kebab Case Example
적용: kebab-case-example
꼬챙이라는 의미의 스큐어 케이스(Skewer Case)라고도 드물게 부르는 것 같다.
가운데 줄(하이픈)이 있는 것이 케밥을 꽃아넣은 것과 비슷하여서 이런 이름이 지어졌다.
스네이크 케이스와 비슷하면서도 다르다.
띄어쓰기 대신 하이픈( - )을 사용하며, 이외의 부분은 전부 소문자로 표기한다.
스프링의 yml 파일이나 url 주소에서 주로 사용된다.
HTML 태그의 id 및 class 값에서 쓰이고, 이로 인해 CSS에서도 자주 사용된다.
헝가리안 표기법(Hungarian Notation)
원문: Hungarian Notation Example
적용: strHungarianNotationExample
헝가리안 '케이스'(Hungarian Case)이라고도 부르는데, 헝가리안 '표기법'을 더 많이 쓰는 것 같다.
마이크로소프트 개발자이자, 헝가리 출신인 찰스 시모니가 제안한 표기법이라고 한다.
접두어(앞 부분)에 자료형의 축약어를 붙이는 것이 기본원리이다.
IDE가 변수 종류를 알려주지 않았던 과거에는 애용되었으나, 요즘은 레거시 코드를 빼면 볼 수 없는 추세이다.
사실 해당 표기법이 나온 마이크로소프트에서도 오늘날에는 비권장하고 있는 표기법이다.
변수 생성 방법
변수 생성이라는 것은 변수를 만든다는 뜻이다.
그러나 상세하게는 값을 저장하기 위한 메모리 공간을 확보하고 변수명과 메모리 공간의 주소를 연결하여 값을 저장할 수 있게 준비하는 것이다.
이러한 변수 생성은 선언(Declaration) → 초기화(Initialization) → 할당(Assignment) 단계를 거친다.
다만, var&const&let 키워드별로 각 단계는 다소 다르게 동작하는 측면이 있다.
기본적인 단계
선언 단계
자바스크립트 엔진에 변수 객체를 등록하는 단계.
변수명을 등록하여 스코프(유효 범위)가 참조할 대상을 만든다.
초기화 단계
변수의 메모리 공간이 생성되고, 변수에 메모리 주소값이 저장되는 단계.
암묵적으로 undefined를 할당해 메모리 공간을 초기화한다.
할당 단계
할당 연산자(=)를 통해 변수에 값을 할당하는 단계.
말 그대로 초기화된 메모리 공간에 값을 할당하는 것이라 할 수 있다.
호이스팅
호이스팅(Hoisting)에는 끌어 올린다는 의미가 있다.
이는 인터프리터가 변수와 함수의 메모리 공간을, 선언 전에 미리 할당하는 것을 의미한다.
조금 더 풀어서 쓰자면, 변수 및 함수의 선언부가 스코프(유효범위) 최상단으로 끌어올려지는 것처럼 동작하는 것이라고 할 수 있다.
뒤에서 추가로 설명을 하겠지만, 이러한 호이스팅은 키워드마다 차이가 존재한다.
간단하게 알아보자면 다음과 같다.
console.log(value_one); // undefined
var value_one = "대충 값";
console.log(value_one); // 대충 값
// 에러 처리로 인한 끊김 없이 실행 자체는 됨
변수 선언 이전에 console.log를 통해 참조되었음에도 에러가 발생하지 않는다.
이는 자바스크립트 엔진 자체에서 코드 실행 전에 변수 선언과 함께 undefined로 초기화를 행했기 때문이다.
console.log(value_one); // ReferenceError: value_one is not defined
let value_one = "대충 값";
console.log(value_one); // 대충 값
// 에러로 인해 실행 자체가 안 됨
// 이는 let, const 둘 다 마찬가지임
반면 let과 const는 뒤에서 선언한 변수를 앞서 참조하려고 하니 오류가 나버린다.
이는 코드 실행 전에는 변수 선언만 하고 초기화는 코드 실행 과정에서 변수 선언문을 만났을 때 수행하기 때문이다.
스코프
스코프(Scope)에는 범위 혹은 시야라는 의미가 있다.
이는 변수 접근 규칙에 따른 유효 범위라고 할 수 있다.
더 쉽게 설명하자면 그냥 변수가 어디에서 선언되었으냐에 따른 변수 자체의 유효 범위이다.
반대로 설명하자면 변수에 접근할 수 있는 범위 정도로 할 수 있겠다.
그리고 자바스크립트에서는 각 유효 범위에 따라 스코프 명칭과 작동 방식이 결정된다.
전역 스코프(Global scope): 코드 전체에서 참조되는 것을 의미하며, 어느 곳에서든 참조 가능하다.
지역 스코프(Local scope): 정의된 함수 내에서만 참조되는 것을 의미하며, 밖에서는 참조 되지 않는다.
여기서 지역 스코프의 경우에는 레벨 단위로 또 다시 2종류로 나뉘게 된다.
함수 레벨 스코프: 함수에서 선언한 변수는 해당 함수 내부라면 어디서든 접근 가능하다. 일반적인 의미의 지역 스코프라고 할 수 있다.
블록 레벨 스코프: 함수, if 문, for 문, while 문, try/catch 문 등으로 생성된 블록({}; 중괄호) 내부에서 선언된 변수는 해당 블록에서만 접근 가능하다.
이러한 스코프 중에서는 블록 레벨 스코프가 비교적 더 나은데. 그 이유는 전역 변수를 남발할 가능성을 낮춰 예상치 못한 오류 발생 가능성을 줄여주기 때문이다.
키워드별 단계
let | const | var | |
유효 범위 | 블록/함수 레벨 스코프 | 블록/함수 레벨 스코프 | 함수 레벨 스코프 |
값 재할당 | 가능 | 불가능 | 가능 |
재선언 | 불가능 | 불가능 | 가능 |
var 키워드
원조라 할 수 있는 함수 레벨 키워드.
값 재할당과 재선언이 가능하다.
var로 선언한 변수는 선언 단계와 초기화 단계가 동시에 실행된다.
즉, 변수 선언과 동시에 초기화가 이루어지면서 undefined가 할당되는 것이다.
이런 식으로 초기화가 이뤄지다보니, 변수의 할당문이 실행되기 전에도 참조가 가능(호이스팅)해진다.
다만, 이러한 유연성 아닌 유연성이 치명적인 문제를 야기하기도 한다.
대표적인 문제점은 4가지가 있다.
1. 선언을 중복해서 할 수 있음.
- 말 그대로 같은 이름의 변수를 여러 번 선언이 가능하다는 점이다. 의도치 않은 변수값 변경이 이뤄질 여지가 많다.
2. 스코프의 범위가 전역 혹은 함수로 제한된다.
- var은 함수의 중괄호 블록만을 범위로 인정한다. 그렇다보니 함수의 중괄호 블록 내부에 선언된 것이 아니면 전부 전역 스코프를 갖게 된다. 이러한 문제 때문에 반복문이 끝난 뒤에도 변수의 값이 남아있는 일이 벌어질 수도 있다.
3. 호이스팅이 가능하다.
- 오류가 나야 할 시점에 그냥 초기화 값을 가지고서 작동할 수도 있다.
4. 키워드 생략이 가능하다.
- 변수값 재할당과 구분이 안 될 가능성이 농후하다.
let 키워드 & const 키워드
let로 선언한 변수는 선언 단계와 초기화 단계가 분리되어 있다.
호이스팅으로 선언 단계가 먼저 이뤄지고, 실제 코드에 도달했을 때야 초기화 단계가 이뤄진다.
이러한 이유로 선언 단계와 초기화 단계 사이에서 변수를 참조할 경우 TDZ로 인해 오류가 발생한다.
참고로 여기서 TDZ(Temporal Dead Zone; 일시적 사각지대)는 변수의 선언 단계부터 초기화 단계 사이의 구간을 의미한다.
즉, 다소 소홀하게 여길 수 있는 구간. 초기화되지 않은 변수가 있는 구간이 TDZ이며, 여기서 참조를 강행하면 실행 컨텍스트에는 등록이 되어 있어도 메모리가 할당되지 않아 오류가 발생하는 것이다.
const로 선언한 변수는 선언, 초기화, 할당 단계가 동시에 이뤄진다. 때문에, const는 선언과 함께 초기화를 해야 한다.
그리고 let와 마찬가지로 선언 단계와 초기화 단계는 분리되어 있다.
- let
값 재할당 가능, 재선언 불가능, 블록 레벨 스코프.
재할당이 가능하기에 일반 변수처럼 사용한다. - const
값 재할당 불가능, 재선언 불가능, 블록 레벨 스코프.
보통 수정할 수 없는 상수를 선언할 때 사용한다.
let와 const 두 개는 위와 같은 서로 다른 특징이 있다. 그리고 var보다는 이 둘의 사용이 권장되는 추세이다.
자료형
변수와 상수에 담는 값들은 다양한 형태로 존재한다.
그리고 프로그래머는 컴퓨터에게 데이터의 형태를 지정해주고, 알려주어서 적합한 로직 처리를 할 수 있게 하여야 한다.
이러한 데이터 형태의 종류를 자료형, 데이터 타입(Data Type)이라고 한다.
자바스크립트는 데이터 타입을 크게 2가지 제공한다.
바로 원시 타입과 객체 타입이다.
그리고 원시 타입은 변경이 불가능한(immutable value)값이지만, 반면에 객체 타입은 변경 가능한(mutable value) 값이다.
원시 타입 | 객체 타입 | |
값 변경 여부 | 변경 불가능한 값(immutable value) | 변경 가능한 값(mutable value) |
전달 방식 | 값에 의한 전달(pass by value) | 참조에 의한 전달(pass by reference) |
저장 값 | 원시 값을 변수에 할당하면 변수에는 '실제 값'이 저장됨 | 객체를 변수에 할당하면 변수에는 '참조 값'이 저장됨 |
저장 공간 | 콜 스택(Call Stack) | 힙 메모리(Heap Memory) |
두 개의 차이는 대략 다음와 같다.
원시 타입(Primitive Type)
7가지 원시 타입: String, Number, BigInt, Undefined, Null, Boolean, Symbol
기본 타입이라고도 드물게 부른다. 이는 기본형되는 타입이기 때문이다.
원시 타입은 값에 의한 전달(pass by value)이 이뤄진다.
즉, A변수에 원시 값을 갖는 B변수를 할당하면 B변수에 할당된 값이 복사되어 A변수에 전달된다.
var a = 123;
var b = a;
console.log(a, b); // 123 123
console.log(a === b) // true로 나오는데, 사실 저장된 메모리 공간은 다름.
다만, 완전히 같다는 뜻은 아니다. 복사된 원시 값만 같을 뿐, 실질적인 변수 값은 다른 메모리 공간에 저장된 별개의 값으로 존재하기 때문이다. 그리고 실질적으로 두 변수 간에는... 값이 아닌 메모리 주소가 전달된다.
값이 전달된다는데 메모리 주소라니, 모순이 아니냐고 할 수도 있다. 그러나 엄격하고 깊게 따지자면, 변수에는 값이 아닌 메모리 주소가 전달된다. 변수와 같은 식별자는 값이 아닌, 메모리 주소를 기억하고 있다.
이러한 용어적 문제는 값에 의한 전달이 자바스크립트를 위한 용어가 아니기 때문이다. 결국, 값에 의한 전달이라고 해도 사실은 값보다는 메모리 주소를 전달하는 것이라고 할 수 있다.
복잡하지만, 사실 더 복잡한 면이 하나 더 있다. 다름이 아니고 이러한 메모리 주소 전달 방식이 사실은 자바스크립트 엔진마다 다르기 때문이다.
var a = 456;
var b = a;
대충 이런 코드가 있다고 치자. a에 있는 값을 b로 옮기는 것인데, 해당 코드는 총 2가지의 평가 방식이 존재한다.
1. 456 값이 들어 있는 새 공간을 생성해서 해당 메모리 주소를 b에 전달.
- 당연하지만, 할당되는 시점에서 두 변수가 기억하는 메모리 주소는 서로 다르다.
2. a의 값인 456의 메모리 주소를 그대로 b에 전달.
- 메모리 주소를 그대로 전달했기에, 두 변수가 기억하는 메모리 주소는 서로 같다.
이는 ECMAScript 사양이 명확하게 정의되어 있지 않아, 자바스크립트 엔진 제조사에 따라 내부 동작 방식이 차이가 있을 수밖에 없기 때문이다. 어떤 엔진은 1번일 수도 있고 어떤 엔진은 2번일 수도 있는, 참으로 애매한 상황이라 할 수 있겠다.
또한, 원시 타입의 값(원시 값)은 변경 불가능한 값(immutable value)이다. 즉, 읽기 전용(read only) 값이다.
원시 타입은 변경 불가능한 값이다.
그런데, 변경 불가능한 값이라니. 누군가는 이렇게 분명 생각할 것이다.
변수 내의 값은 언제든지 재할당을 할 수 있지 않나?
이거 혹시 상수를 MZ하게 말하는 건가?
그러면 변경이 불가능하다는 말은 좀 이상한데?
여기서 주의해야 할 점은 '원시 값'이 변경할 수 없다는 뜻이지, '변수 값'을 변경할 수 없다는 뜻이 아니다. 변수는 어떤 값을 어떤 값을 저장하기 위해 확보한 메모리의 공간 or 어떤 메모리 공간을 식별하기 위해 붙인 이름이다. 그리고 값은 변수에 저장된 데이터 그 자체를 의미한다.
여기서 변경 불가능하다는 것은 변수가 아니라 값에 대한 의미이다. 원시 값 자체를 변경할 수 없다는 것이지 변수 값을 변경할 수 없다는 것이 아니다는 것이다.
그래도 원시 값, 이미 메모리에 저장된 값을 변경할 수 없다는데 대체 어떻게?
바로 재할당을 통해 변수의 값을 '교체'함으로써 변경하는 것이다.
교체라니 더 헷갈릴 수도 있다. 그러나 그리 어려운 개념은 아니다. 자바스크립트 내에 있는 모든 변수는 값을 저장하기 위해서 메모리 공간이 필요하다.
그리고 이런 메모리 공간을 필요로 하기에 변수라고 하는 것이다. 상수가 변수의 상대적인 개념에 가까운데도 변수 취급을 받는 이유는 여기에 있다. 어쨌거나 상수도 메모리 공간이 필요하다.
바로 이러한 메모리 공간을 '교체'해주는 것이 재할당이라고 할 수 있다. 즉, 새로운 메모리 공간을 확보하고 재할당한 원시 값을 콜 스택(Call Stack)에 저장한 후, 변수는 새롭게 재할당한 원시 값을 가리키게 되는 것이다.
변수가 참조하던 메모리 공간의 주소가 바뀌는 것. 조금 저렴하게 설명하자면 어떤 박스(메모리 공간)에 있는 구분용 스티커를 떼내서 새로운 박스에 붙이는 꼴...이라고 할 수 있겠다.
이러한 원시 값의 변경 불가능한 특징(사실 '교체'는 할 수 있음)을 불변성(immutability)이라고 한다. 해당 특징이 있기에 예기치 못한 변경에서 자유로우며, 데이터의 신뢰성을 기본적으로 보장할 수 있다.
그리고 쓰다보니 깨달은 건데 이거 포인터의 개념을 알아야 이해하기가 좀 쉽지 않나 싶다. 정 이해가 안 되는 사람들은 C언어의 포인터 및 주소 개념부터 조금 익히고 오자.
그리고 혹시나 햇갈릴까 싶어서 거듭 말하는 것이지만, 상수와 변경 불가능한 값은 다른 것이다. 상수는 메모리 공간이 필요한 변수, 언제까지나 재할당이 금지된 변수에 불과하다.
참고로 내장되어 있는 객체 중 원시 래퍼 객체(Wrapper Object)라는 것도 존재한다. 줄여서 래퍼 객체라고도 한다.
String, Number, Boolean이 원시 래퍼 객체에 속하는데, 원시 타입에서는 이를 상속받아서 사용할 수 있다. 사실상 원시 래퍼 타입은 원시 타입을 객체처럼 메소드를 사용하게 하는 등 더 편리하게 사용하도록 도와주는 것이라고 할 수 있다.
그리고 명백히 구분되어 있는 것이기에, 원시 래퍼 객체와 원시 타입은 같은 것이라고 판단하면 안 된다.
Number Type
자바스크립트에는 정수와 실수 상관없이 하나의 숫자 타입만 존재한다.
그게 바로 Number이다.
정확하게는 자바스크립트에서는 모든 숫자를 실수로 처리한다.
때문에 정수 타입이 없으며, 실수를 연산할 때는 근사값으로 처리하는 경향이 있다.
이러한 실수는 배정밀도 64비트 부동소수점 형식을 따른다.
다만, 앞서 언급했듯 이는 근사값으로 처리하는 경향이 있기에 다소 부정확하다.
0.1 + 0.2 == 0.3
// 오답임
분명 맞아야 할 것 같은 숫자가 오답인 현상이 벌어질 수도 있다는 것이다.
때문에, toFixed() 메소드나 Math.round() 메소드로 반올림을 한 다음에야 비교 연산을 수행해야 한다.
그리고 반올림에서 알 수 있다시피 정밀도가 보장되는 범위가 존재하기는 한다.
정수부는 15자리까지, 소수부는 17자리까지 유효하다.
이러한 Number의 표현 가능한 수의 범위는 -(2^53-1)부터 2^53-1까지이다.
대략 +- 9,007,199,254,740,991라고 보면 된다.
이러한 값의 경우 원시 래퍼 객체에서 상수 변수로 제공해준다.
- 최대 정수: Number.MAX_SAFE_INTEGER; //9007199254740991
- 최소 정수: Number.MIN_SAFE_INTEGER; //-9007199254740991
해당 상수를 이용하면 자바스크립트에서 안전한 최대 정수(MAX_SAFE_INTEGER)와 안전한 최소 정수( MIN_SAFE_INTEGER)를 구할 수 있다.
이외에도 Infinity라는, 무한대를 나타내는 속성 또한 존재한다.
양의 무한대를 나타내는 Infinity와 음의 무한대를 나타내는 -Infinity로 나뉜다.
- 양의 무한: Infinity === Number.POSITIVE_INFINIT
- 음의 무한: -Infinity === Number.NEGATIVE_INFINITY
NaN이라는 값도 Number에 존재한다.
Not A Number를 줄여서 NaN이라고 하며, 자바스크립트 내에서 숫자가 아닌 값을 나타낸다.
isNaN("hello");
// True (숫자가 아니면 true가 나오고, 숫자이면 false가 나오기 때문)
NaN은 특이하게도 자기 자신과 다른 값을 비교하는 것이 불가능하다.
그렇기에 숫자인지 아닌지 판별하려면 InNaN(value) 함수를 사용해서 비교해야 한다.
BigInt Type
임의의 정밀도로 정수를 나타낼 수 있는 자바스크립트 숫자 원시 값이다.
BigInt를 사용하면 안전한 Number 정수 제한을 초과하여 큰 정수를 안전하게 저장하고 조작 할 수 있다.
다만, 다른 값과 혼합하여 연산하는 것 자체가 불가능하다.
당연하지만, 정수인 만큼 실수 부분은 손실이 이뤄진다.
const bigint = 1234567890123456789012345678901234567890n;
BigInt형 값은 정수 리터럴 끝에 n을 붙이면 된다.
String Type
텍스트 데이터를 나타낼 때 사용된다.
UTF-16 코드 단위의 시퀀스로 표현한다.
16bit 정수값의 요소로 구성된 집합이고, 각 요소가 string의 한 자리이며, 0번째 index부터 시작한다.
원시값은 절대로 변하지 않기에, 문자값 또한 변하지 않는다.
즉, 원시값과 문자값은 서로 불변한다.
따옴표(' '), 쌍따옴표(" "), 백틱(` `)으로 감싸서 문자열임을 표현한다.
여기서 백틱(backtick)은 ES6에서 추가된 것으로 따옴표와 쌍따옴표에서 더 발전한 형태라 할 수 있다.
일반적인 키보드의 물결(~) 표시 대신 넣을 수 있으며, 비틀린 따옴표(` `)의 형태를 하고 있다.
var n = 5;
console.log("라면 " + n + " 그릇 주문입니다");
//그냥 쌍따옴표 사용
var n = 5;
console.log(`라면 ${n} 그릇 주문입니다`);
//백틱 사용
백틱과 ${value}를 이용하면 변수를 기존보다 훨씬 더 편하게 넣어 쓸 수 있다.
이러한 기능을 템플릿 리터럴(템플릿 문자열)이라고 부른다.
var a = 3;
var b = 7;
console.log(`A와 B를 더한 값은 ${a+b}라고 합니다.`);
또한, 단순 변수 추가 뿐만 아니라 사칙연산(+ - / *)를 ${value} 내에서 수행할 수 있다.
var str = "자바스크립트의 \n줄바꿈.";
console.log(str);
//여기 위 아래는 서로 같은 출력문이 나옴
var str = `자바스크립트의
줄바꿈.`;
console.log(str);
심지어 따옴표에서는 \n을 통해 개행(줄 바꿈)을 수행해야 했는데.
백틱에서는 그냥 실제적인 개행만 있어도 개행이 이뤄진다. 바로 위의 코드 블럭 예시처럼 말이다.
그리고 함수를 호출할 때 괄호 대신 백틱을 사용해도 된다.
즉 fun_adds`a` 같은 형태로 호출이 가능하다는 의미다. 물론 가독성이 심히 떨어지기에 잘 쓸 것 같지는 않다.
Boolean Type
논리적 데이터 유형이다.
정상적으로는 참(true) 혹은 거짓(false)의 값만 가질 수 있다.
비정상적일 때는 undefined(자리만 있고 값이 없는 초기화 상태)와 null(값이 없음)이 포함될 수도 있다.
그리고 input tag의 checkbox 타입의 checked의 상태값 표현, 특정 UI의 노출여부를 보여주는 변수의 상태 isShow의 플래그값 표현 등에 사용된다.
Undefined
기본적으로 undefined는 ‘아무 값도 할당받지 않은 상태’를 의미한다.
이러한 undefined 타입의 값은 undefined가 유일하다. 선언 이후 값을 할당하지 않은 변수는 undefined 값을 가지며, 존재하지 않는 객체 프로퍼티에 접근할경우 undefined가 반환된다. 또한, var 키워드로 선언한 변수는 암묵적으로 undefined로 초기화된다.
즉, undefined는 개발자가 의도적으로 할당하기 위한 값이 아니다. 자바스크립트 엔진이 변수를 초기화 할 때 사용하는 값이다. 물론 개발자가 의도해서 undefined를 넣을 수는 있지만, 그건 본래의 취지에 어긋나기에 권장되지 않는다.
이러한 undefined 대신 값이 없다는 걸 명시하고 싶으면 아래의 Null을 사용해야 한다.
Null Type
null은 개발자가 변수에 값이 없다는걸 '의도적'으로 알리기 위해 사용한다.
때마침 단어의 의미도 '비어있는, 존재하지 않는 값'을 뜻한다.
이러한 null 타입의 값은 null이 유일하다. 자바스크립트는 대소문자를 구분하므로 null은 Null, NULL 등과는 다르다. 변수에 null을 할당하는 것은 변수가 이전에 참조하던 값을 더 이상 참조하지 않겠다는 의미이다.
참조하지 않겠다는 말은 이전에 할당되어 있던 값 참조를 명시적으로 제거하는 것을 의미한다. 그리고 자바스크립트 엔진은 누구도 참조하지 않은 메모리 공간에 대해 가비지 콜렉션(Garbage Collection)을 수행할 것이다. 사실상 사용하지 않는 메모리를 정리하는 최적화를 수행한다고 보면 된다.
다만, 이러한 null에는 약간의 문제가 있다. 다름이 아니고 null을 typeof로 타입 체크를 해보면 object가 나온다는 점이다. 이건 버그라고 하는데, 사실 엄연히 따지자면 의도적으로 '방치' 중인 버그에 가깝다.
https://web.archive.org/web/20160331031419/http://wiki.ecmascript.org:80/doku.php?id=harmony:typeof_null
typeof 수정안 거부 관련 자료
https://2ality.com/2013/10/typeof-null.html
자바스크립트 null 버그와 완련된 이야기
이는 자바스크립트 첫 번째 버전부터 이어져오던 버그이다.
정확하게는 자바스크립트 초기 버전에서는 값이 기본적으로 32비트 단위로 저장되었었다. 이러한 32비트 저장 데이터는 작은 유형의 태그(1~3비트)와 값의 실제적인 데이터(나머지 비트)로 구성되어 있었다. 여기서 작은 유형의 태그는 다음과 같았다.
000: 객체(object)
1: 정수(int)
010: 더블(double)
100: 문자열(string)
110: 논리형(boolean)
이러한 다섯 개의 태그를 이용해 데이터형을 구성하고 있었다. 그리고 null은 일종의 특별한 값으로 간주되었었다. null 자체가 null 포인터의 표현이었다. 문제가 있다면, 자바스크립트에는 C언어 같은 곳에 있는 포인터가 없었다는 점이었다.
결국, null은 단순히 아무것도 의미하지 않거나 void를 뜻하게 되었다. 그리고 이는 곧 null값의 32비트 전체가 0으로 표시(0x00)되는 것으로 이어졌다. (이는 자바스크립트가 워낙 급하게 만들어진터라 명시적인 검사조차 없던 탓이 크다.)
그리고 위의 표를 보면 알 수 있다시피 작은 유형 태그의 000은 객체(object)이다. null값은 비트마다 0이 박힌 상태이므로, 인터프리터는 null값을 객체로 판단한다. 그렇게 null은 object라는 괴상한 결과로 이어지게 된 것이다.
시간이 지나면서 이를 고치자는 주장이 나오기도 하였으나, 불행하게도 고치려고 했을 때는 시간이 너무 흘렀다는 점이었다. 이러한 버그를 이용하거나 버그에 대응하는 부분을 마련한 코드들이 너무 많아졌고, 고치기에는 너무 먼 길을 와버리면서 방치되고 있다.
var a = null;
console.log(typeof a === null); // false
console.log(a === null); // true
때문에, null타입을 확인하려면 typeof가 아닌 일치연산자(===)를 사용해야 한다.
Symbol Type
ES6에서 도입된 원시 타입으로, 유일무이한 값, 고유한 값을 지정하기 위해 사용된다.
더 정확하게는 명칭의 충돌 위험이 없는 유일한 객체의 속성 키(property key)를 만드는데 사용된다.
이는 여러 사람이 협업하는 개발의 특성상 새로운 속성이나 메소드를 추가할 때, 다른 사람이 쓰고 있는 것이 아닌가 하는 불안을 갖고 있기 때문이다. 기존에 있던 문자열을 이용한 키는 자칫 이미 있는 키를 덮어씌울 수도 있다는 문제점을 갖고 있었다.
그래서 나온 것이 바로 심볼이다. 심볼을 사용하면 다른 개발자들이 해당 키를 사용하는지 걱정하지 않고 마음껏 새로운 기능을 추가할 수 있다.
const v1 = Symbol('hello');
const v2 = Symbol('hello');
console.log(v1 === v2); // false
심볼은 Symbol 함수를 호출함으로써 생성할 수 있다.
객체 타입(Object Type)
대표적인 객체 타입: 배열, 일반 객체, 함수, 날짜, 인덱스 컬렉션, 키 컬렉션 등
참조형 타입(Reference Type)라고도 부른다.
객체(Object)는 실존하는 어떤 무언가의 데이터(속성)와 그 데이터의 관련된 동작(메소드)을 포함하는 개념이다. 그리고 자바스크립트에서는 원시 타입의 값을 제외한 모든 것이 객체이다.
var food = {
name: 'kimchi'
};
객체 타입은 프로퍼티의 개수가 정해져 있지 않으며, 동적으로 추가되고 삭제될 수도 있다. 또한 프로퍼티의 값에도 제약이 없다. 따라서 객체는 원시 값과 같이 확보해야할 메모리의 크기를 사전에 정해둘 수 없다.
사실상 객체 타입은 다양한 타입의 값을 하나의 단위로 구성한 복합적인 자료 구조라고 할 수 있다. 그리고 객체 타입을 관리하는 방식이 원시 타입과 비교했을 때 훨씬 복잡하기에, 구현 방식도 브라우저 제조사마다 다를 가능성이 크다.
여기서 조금 다른 이야기를 하자면, 객체 지향 프로그래밍 언어(Object Oriented Programming, OOP)는 사전 정의된 클래스를 기반으로 객체(인스턴스)를 생성한다. 객체 생성 전에 이미 프로퍼티(속성)와 메서드가 정해져 있으며 그대로 객체를 생성하면 된다. 그래서 객체 생성 이후에 프로퍼티를 삭제, 추가 할 수 없다.
반면 자바스크립트 객체는 프로퍼티 키를 인덱스로 하는 해시 테이블(Hash Table)이다. 또한, 객체를 생성할 때 힙 메모리( Heap Memory)에 객체 인스턴스를 생성하고, 해당 인스턴스가 존재하는 위치를 스택 메모리에 기록하여 사용한다. 즉, 힙 메모리에 저장되어있는 주소를 스택 메모리 영역에 저장하는 것이다.
이러한 저장 구조 덕에, 객체 타입은 객체 생성 이후에도 동적으로 프로퍼티(속성), 메서드를 추가하고 수정하고 삭제할 수 있는 것이다. OOP에 비하면 훨씬 편리하고 간단하다고 할 수 있다.
그러나 명이 있다면 암도 있는 법. 편리성과 별대로 OOP 언어의 객체보다 생성 및 프로퍼티 접근 비용이 더 큰 편이다. 심지어 객체는 원시 값에 비해 관리하는 방법이 복잡하고 비용이 크며, 메모리 소비가 커질 수도 있다.
https://v8.dev/blog/fast-properties
V8 블로그에서 밝히는 프로퍼티 속도 개선
아무튼, 이러한 이유로 V8 자바스크립트 엔진에서는 프로퍼티에 접근하기 위해 동적 탐색(dynamic lookup)이 아닌, 히든 클래스(hidden class) 방식을 사용해 C++ 객체 프로퍼티에 접근하는 정도의 성능까지 보장한다. 나름의 최적화를 가했다고 보면 된다.
사실 이런 문제를 떠안고 있을 바에, 처음부터 원시 값 형태로 구성하면 되지 않느냐고 할 수도 있다. 그러나 객체의 존재 의의를 생각하면 그럴 수 없다. 객체는 근본적으로 크기가 일정하지 않은데다가 크기가 매우 큰 편이다.
원시 값 형태로 구성하면 신뢰성은 보장되겠지만, 그 개수가 많아지는 순간 값 변경 시 소모하는 자원은 어마어마 해질 것이다. 그렇기에 자원을 절약하기 위해 객체는 재활용이 가능하도록, 변경 가능한 값으로 만들어졌다.
즉, 객체 타입은 원시 타입과 다르게 변경 가능한 값(mutable value)이다.
말 그대로 재할당 없이 객체를 곧바로 변경할 수 있다.
그리고 원시 타입이 원시 값 위주으로 접근을 한다면, 객체 타입은 참조 값 위주로 접근한다.
이러한 참조 값은 생성된 객체가 실제로 저장된 메모리 공간의 주소로, 해당 참조 값을 통해 객체에 접근할 수 있다.
그리고 객체 타입에는 얕은 복사(shallow copy)와 깊은 복사(deep copy)라는 개념 또한 존재한다.
- 얕은 복사(shallow copy)
- 객체를 프로퍼티 값으로 가질 때, 1단계까지만 완전 복사하는 것을 말한다.
- 2단계부터는 참조값 복사가 이뤄진다. (해당 참조값 내용물 변경 시 영향 받음)
- 객체에 중첩되어 있는 객체의 경우 참조 값을 복사한다.
- 깊은 복사(deep copy)
- 객체를 프로퍼티 값으로 가질 때, 객체에 중첩되어 있는 객체까지 모조리 완전 복사하는 것을 말한다.
- 객체에 중첩되어 있는 객체까지 모두 복사해서 원시 값처럼 완전한 복사본을 만든다.
둘 다 복사인 만큼 원본과는 참조 값마저 다른 객체라고 할 수 있다. 그저 얕은 복사는 참조 값 주소를 가져오기에, 다른 객체인데도 영향을 받는 것이다. 깊은 복사에는 이러한 문제가 존재하지 않는다.
즉, 식별자 여러 개의 참조값이 같아서 동일한 객체를 공유하고, 그중에서 하나라도 수정하면 나머지들도 줄줄이 영향을 받는다. 이러한 줄줄이 비엔나 변경 사태를 막고자, 주소값 참조가 아닌 객체 내용물을 통째로 복사하는 깊은 복사가 있는 것이다.
한편으로는 이를 이용하면 객체를 할당한 a변수를 b변수에 할당함으로써, 서로 다른 변수임에도 같은 값을 항상(중간에 값이 변경되면 함께 바뀜) 유지할 수 있게 만들 수도 있다. 이는 참조 값이 복사되어 전달되기 때문이다.
이런 식의 주소값 전달 방식은 참조에 의한 전달(pass by reference)이라고 한다.
그리고 원시 타입과 마찬가지로 다소 애매한 측면이 존재한다.
엄격하게 따지자면 메모리 공간에 있는 값을 복사해서 전달하다보니 원히 자체는 값에 의한 전달과 비슷하기 때문이다. 그저 전달되는 값이 참조 값이고, 해당 값으로 같은 객체를 공유할 수 있기에 참조에 의한 전달이라고 하는 것이다.
동적 타입 언어
자바스크립트는 동적 타입 언어다.
동적 타입 언어는 코드를 실행할 때 타입이 알아서 추론되어 결정된다.
따라서 런타임 과정에 타입이 결정되므로, 명시적 타입 선언은 필요가 없어보이지만 의외로 많이 사용된다.
명시적 타입 변환(Implicit Coercion)
123 + '' // 123 (string)
1 + 2 + "3" // 33 (string)
별도의 코드를 작성하여 변환할 의사를 명확하게 표현하는 것이다.
- value.toString(): String 타입으로 변환.
- String(value): String 타입으로 변환.
- Number(value): Number 타입으로 변환.
- Boolean(value): Boolean 타입으로 변환.
- parseInt(value): 문자열을 받아서 Number 타입으로 변환 (일반 문자는 무시).
- parseFloat(value): 문자열을 받아서 Number 타입으로 실수 변환 (일반 문자는 무시).
위와 같은 원시 래퍼 객체를 활용하면 개발자가 의도적으로 타입을 변경할 수 있다.
암묵적 타입 변환(Explicit Coercion)
String(12345) // "12345"
Boolean(0) // false
데이터 타입이 값에 따라 자동으로 변하는 것이다.
- 개발자가 의도하지 않았지만, 타입이 변경될 때가 있다.
- [value + " "] String 타입으로 변환
- [value * 1] Number 타입으로 변환
- [!!value Boolean] 타입으로 변환
값이 전달될 때, 참조되어있는 변수 값 타입이 개발자의 의도와 다르게 암묵적 타입 변환으로 변경될 가능성이 존재한다.
이는 곧 타입 추론이 어려워져, 불필요한 디버깅 시간을 발생시킨다.
이를 해결하려면 전달되는 시점마다 typeof 혹은 일치연산자를 사용하여 Type Guard를 구현해야 한다.
아니면, 자바스크립트의 superSet인 타입스크립트를 사용하는 방법이 있다.
사족
강의 자체는 어렵지 않았다. 한데, 거기서 조금씩 더 알아보려고 하니 걷잡을 수 없을 정도로 내용이 많았다. 또한, 전반적인 내용의 난이도도 꽤 되는 편이었다. (특히 변경 불가능한 값을 이해하는 것이 그러했다.) 일단, 앞으로도 이런 내용이 많을 것 같기에 주의하면서 공부해야 할 것 같다.
그리고 썩 재미없던 것은 아니다. 특히 null의 뒷이야기를 찾아보면서 이런 곳에서 이진법이 나오네 라는 생각도 하고, 이진 계산도 해보면서 잊고 있던 흥미를 느끼기도 했다. 이런 사소한 것이 TIL을 쓰는 원동력이 된다. 😆
다만, 글을 날려먹은 것은 그리 원동력이 되지 않는 것 같다. 다음부터는 저장과 인터넷 확인을 확실하게 해두고 프로그래머스 데브코스 TIL을 쓰도록 하자. 😓
'💻 종합 개발 주제 > 📚 웹앱 데브코스' 카테고리의 다른 글
11일차 데브코스 pt.1 - 클라우딩 어플리케이션 엔지니어링 TIL (2) | 2024.01.11 |
---|---|
10일차 데브코스 - 클라우딩 어플리케이션 엔지니어링 TIL (4) | 2024.01.09 |
8일차 데브코스 - 클라우딩 어플리케이션 엔지니어링 TIL (2) | 2024.01.06 |
7일차 데브코스 - 클라우딩 어플리케이션 엔지니어링 TIL (2) | 2024.01.05 |
6일차 데브코스 - 클라우딩 어플리케이션 엔지니어링 TIL (2) | 2024.01.04 |