개발 공부/자바스크립트

You Don't Know JS - 자바스크립트가 이렇게 재미있다고?

5묘 2022. 11. 10. 22:54

You Don't Know JS - 카일 심슨 지음.

우연히 도서관에서 발견한 보물같은 책이다! 보물같다고 하는 이유는, 이 책 덕에 '아, 자바스크립트는 정말 재밌다. 자바스크립트 공부 재밌다.'라는 감정을 처음으로 느꼈기 때문이다.

전에는 언어 문법 공부는 필요하니까 하는 것이고, 그냥 외우는 것으로 생각했다. 하지만 이 [You Don't Know JS] 를 읽으며 내가 외우기만 했던 문법의 동작 원리가 어떤 식으로 이뤄지는지도 알 수 있었고, 내가 잘못 알고 있던 부분을 파악할 수도 있었다.

이 책의 가장 큰 장점은 매끄럽고, 술술 잘 읽히는 서술 + 번역이다. 타 기술서적을 읽을 때와 달리 책을 읽으며 정말로 '술술 읽힌다'고 느껴졌다. 번역 해주신 분들이 정말 번역을 잘 해주셨다고 생각했다. (잘된 기술서 번역은 이렇게 초보 개발자를 구하기도 한다)

원래 나는 공부를 할 때, 손으로 쓰면서 하는 편인지라 처음 1독을 할 때는 노트에 쓰면서 정리를 했다.
다른 분들처럼 글을 읽으며 실시간으로 블로그 글을 작성하는 것도 좋지만, 나는 먼저 1독을 빠르게 하고, 2독을 하며 블로그에 정리하는 쪽이 머리에 남는 것이 더 많을 것 같아 이 편을 선택했다.

(개발새발 글씨가 부끄럽지만) 열심히 공부했던 흔적..

현재까지 우리나라에 번역된 [You Don't Know JS] 시리즈는 '타입/스코프' 편과 'this/비동기' 편 두 권이다. 'this/비동기' 편은 아직 읽고 있는 중이라, 우선 다 읽은 '타입/스코프' 편 부터 챕터마다 새롭게 안 사실이나 중요하게 생각한 내용을 간단히 적어둘까 한다.


타입

- 자바스크립트의 원시 타입(Number, String, Boolean, undefined, null, symbol(++ES6)) 와 참조 타입인 Object로 나뉜다. 
- 참조 타입 중 함수(Function)의 length는 인자 개수이다. 
- typeof 를 찍어보았을 때 나오는 결과값은 항상 문자열이다.(Ex. "string", "number", "object")
- null은 특이하게 typeof를 찍었을 때 "object"가 나온다. 하지만 실제로 null은 원시타입이다.
- undefined 는 2가지 상태로 볼 수 있다. 1) 선언은 됐으나 값이 없는 경우, 2) 선언되지 않은 경우.
- typeof 는 오류 없이 어떤 변수의 존재 여부를 밝히는 안전 장치의 역할을 한다.
- 전역변수는 모두 전역객체(브라우저: window)의 프로퍼티이다.

- Array는 안에 타입이 다른 인자가 와도 OK이다.
- Array는 delete로 값을 지워도, 안에 슬롯은 남아있다. 
- String의 값은 불변(immutable)하므로 문자열의 메서드는 무조건 새로운 문자열을 생성(값 복사) 후 반환한다.
- Number 타입은 정수, 부동 소수점 아우르는 유일한 숫자 타입이다.
- Number 타입에서 소수점 앞 뒤의 0은 생략이 가능하다.
- 8진수를 나타낼 때 이전에는 '0363' 이렇게 표기해도 가능했으나, ES6와 엄격모드에서는 이렇게 표기할 수 없고 무조건 '0o363' 이렇게 알파벳을 붙여줘야 한다.
- 0.1 + 0.2 === 0.3의 결과가 false. 이유는 0.3000000...4라는 아주 미세한 소수점이 합쳐진 값에 가깝기 때문이다. 이런 미세한 오차를 '머신 입실론'이라 하며, 오차가 이 머신 입실론보다 작을 경우 '허용 공차(=오차로 보지 않는다)'로 봐야 한다.
- 자바스크립트의 머신 입실론은 2의 -52승이다. ES6에서는 Number.EPSILON으로 미리 정의된 입실론 값을 가져와 '두 값의 차이의 절대값'이 입실론보다 작은지 여부에 따라 동등함을 판단한다.
- symbol은 ES6에서 새로 도입한 원시 타입이다. 전용 혹은 특별한 객체의 프로퍼티 명으로 사용이 가능하다.
- undefined 타입의 값은 undefined밖에 없고, null 타입의 값은 null밖에 없다.
- void가 붙으면 값이 무조건 undefined로 만든다.
- NaN은 자기 자신(NaN)과 동일하지 않은 Number 타입이다. 즉 NaN !== NaN은 true이다.
- ES6 부터는 Number.isNaN() 유틸리티 함수로 NaN 값 구분 가능
- 다른 언어에서는 0으로 나누면 ZeroDivisionError가 나오지만, JS에서는 에러가 안나오고 Infinity가 나옴. 음수를 0으로 나누면 -Infinity가 된다.
- JS에는 음의 영(-0) 존재한다. 근데 얘는 엄격한 동등비교 했을 때 0과 같다. 따라서 (n===0) && (1/n === -Infinity) 만족해야 -0이다.
- (-0)이 필요한 이유는 값의 크기 뿐 아니로 방향같은 정보를 넘길 때 부호가 필요하기 때문이다.
- ES6에서 Object.is() 유틸리티 함수를 이용해 -0, NaN을 더 쉽게 찾을 수 있어졌다.
- 원시 타입은 값이 복사돼 복사본-원본 관계 없어지지만, 참조타입은 레퍼런스 복사가 이루어져 복사값 변하면 원본도 변한다. 그러나 변수가 가리키는 대상 자체가 달라지면, 원본값과 상관 없어진다.

네이티브

- 네이티브란? 특정 환경(브라우저, 클라이언트) 에 종속되지 않은 ECMAScript 명세의 내장 객체를 이야기한다. JavaScript의 내장함수라고 볼 수도 있다. 
- 네이티브는 생성자처럼 사용이 가능하다. 원시 값을 감싼 객체 래퍼를 만드는 것이다.
- 객체 래퍼를 사용하게 되면 원시값일 때는 사용할 수 없었던 타입 별 메서드, 프로퍼티를 사용하는 것이 가능해진다.
- 스칼라 원시값을 객체 래퍼로 감싸는 것을 '박싱(boxing)'이라 한다.
- 하지만 원시값을 담은 변수에 직접 .toString()이나 length같이 메서드/프로퍼티를 사용할 수 있는 것처럼 보인다. 이는 사실 JavaScript 엔진이 메서드/프로퍼티가 붙어있는 원시값을 자동으로 래핑하기 때문이다. 즉, 개발자가 굳이  new String() 이렇게 따로 래핑하지 않아도 괜찮다.
- 리터럴은 변수에 저장되는 값이 변하지 않는 데이터를 말한다.
- 객체 래퍼에서 원시값을 추출하는 것을 '언박싱(unboxing)'이라 한다. .valueOf() 메서드를 이용할 수 있고, 암시적 타입변환으로 인해 자동으로 언박싱이 되는 경우도 있다.
- Symbol()은 new를 붙이면 에러가 나는 유일한 네이티브 생성자이다.
- 네이티브 생성자에는 .prototype 객체가 내장되어 있다. 그래서 생성자를 통해 만든 객체들은 이 prototype 객체에 정의된 메서드에도 접근이 가능하다.

강제 변환(자동 타입 변환)

- 이 책에서는 모든 타입 변환을 강제 변환으로 칭하며, '명시적 강제 변환'과 '암시적 강제 변환' 두 가지로 구별한다.
- 명시적 강제 변환은 의도적으로 타입 변환을 일으키는 것이 명확하며, 암시적 강제변환은 다른 작업 중 부수효과로부터 발생하는 타입변환이다.
- 명시적 강제 변환의 방법은 다음과 같다.
1) String(), Number() 같이 네이티브 함수를 이용하기
2) .toString(), toNumber() 메서드를 원시값에 붙여 자동으로 객체 래퍼로 박싱해 타입 변환
3) 단항 연산자 '+' 붙이면 피연산자를 숫자로 강제변환함.
4) 틸드(~)는 32비트 숫자로 강제 변환 후 NOT 연산을 한다(각 비트를 거꾸로 뒤집음.) ! 의 역할과 비슷하다.
5) parseInt(), parseFloat()를 이용해 문자열을 숫자로 파싱한다. 이때 '42px'이런 식으로 숫자가 아닌 문자열이 섞여있어도 NaN 오류 없이 숫자로 파싱한다. 이때, 문자열 아닌 다른 타입 들어가면 문자열로 강제 변환 후 숫자로 파싱하는데 이 과정에서 임의 해석이 발생할 수 있으니 parseInt()에 비문자열을 넣는 것은 지양하도록 하자.
6) !(부정 단항 연산자)를 쓰면 값을 불리언으로 명시적 변환 가능하다. !! 이렇게 이중부정 사용하게 되면 해당 값의 원래 true/false 상태를 불리언 값으로 볼 수 있다.
- 암시적 강제 변환의 방법 중 논리 연산자를 사용할 때, 논리 연산자(&&, ||) 의 결과값은 true/false가 아니라 '두 피연산자 값 들 중 하나가 선택되는 것'이다. ||의 결과가 true면 첫번째, false면 두번째 피연산자가 / &&의 결과가 true면 두번째, false면 첫번째 피연산자가 결과값이 된다. (이는 단락평가의 원리로도 이어진다.)
- symbol 타입은 문자열로 암시적 강제변환 금지, 숫자로 변환 절대 불가, 불리언으로의 변환은 명시.암시 둘 다 가능.
- 느슨한 동등비교(==)는 '강제변환을 허용하는 비교'이고, 엄격한 동등비교(===)는 '허용하지 않는 비교'이다.

문법

- 영어의 문법에 빗대보면 문은 문장, 표현식은 구, 연산자는 구두점이라고 볼 수 있다. 자바스크립트의 표현식은 단일, 특정한 결과값으로 계산된다. 문은 표현식만으로 완성될 수 있으며, 표현식만으로 완성된 문을 '표현식 문'이라 한다.
- 모든 문은 완료값을 가진다.
- 자바스크립트에서 { } 중괄호를 사용하는 경우는 크게 1) 객체 리터럴일 경우, 2) 레이블(프로그램 내의 특정 영역 식별하게 하는 식별자. break, continue 등이 예시.)
- ES6부터는 구조 분해 할당이 가능해져서 var {a, b} = getObject(); 이런 식으로 객채의 프로퍼티에 저장된 값을 나누어서 할당하는 것이 가능해졌다.
- 자바스크립트 프로그램에는 세미콜론이 누락된 곳에 엔진이 자동으로 삽입해주는 'ASI(자동 세미콜론 삽입)' 기능이 있다. 
- 자바스크립트의 에러는 크게 실행 전, 컴파일레이션 과정에서 잡게 되는 조기 에러(SyntaxError처럼 동일인자명, 프로퍼티명이 보이거나 표현식에 이상이 있을 경우.)와 실행되는 동안 try-catch 문으로 잡을 수 있는 런타임 에러로 나뉜다.
- ES6부터는 TDZ(임시 데드 존, 아직 초기화를 하지 않아 변수를 참고할 수 없는 코드 영역)이 도입되어, 선언을 통해 초기화가 되지 않은 변수를 사용하려 하면 에러가 나게 된다. var은 선언과 동시에 초기화가 이루어지지만, let은 호이스팅으로 선언이 컴파일레이션 과정에서 일어나더라도, 초기화는 실제 코드에 도달해야 일어나기 때문에 그 사이에 let으로 선언된 변수는 TDZ 상태에 있으므로, 초기화 전에 호출하면 에러가 나는 것이다.
- switch ... case 문의 default 절은 모든 case를 다 실행한 후에 실행되지만, 꼭 가장 마지막에 코드를 작성해둘 필요는 없다.(중간에 삽입해둬도, 모든 케이스 다 돌아가고 실행된다.) 그리고 필수사항이 아니며 default 절에도 break 키워드를 적지 않으면 코드가 계속 실행된다.

 


스코프

- 스코프는 어디서, 어떻게 저장한 변수(확인자)를 찾을지 결정하는 규칙의 집합이다.
- 자바스크립트는 컴파일레이션 과정을 거치는 컴파일러 언어이다. 
- 자바스크립트의 컴파일레이션 과정은 3단계이다. 토크나이징/렉싱 -> 파싱 -> 코드 생성.
- 변수의 선언은 컴파일레이션 중에 컴파일러가 스코프에 확인하고, 없으면 컴파일러가 변수를 스코프 안에 생성할 것을 요청한다. 값을 할당하는 코드는 엔진이 실행 시에(런타임에서) 스코프에서 변수를 검색하여 값을 대입한다.
- 엔진이 스코프에서 검색하는 방식은 LHS, RHS 두가지가 있다.
- 중첩 스코프에서의 검색은 대상 변수를 현재 스코프에서 발견하지 못할 시 바깥 스코프로 1단계 씩 나가며 가장 바깥쪽의 글로벌 스코프에 도달할 때까지 검색을 계속한다.(변수를 찾으면 멈춘다.)
- 렉시컬 스코프는 스코프의 작동 방식의 하나이며, 컴파일레이션 과정 중 렉서가 문자열 형태의 코드를 토큰으로 인식하는 렉싱 타임에 결정되는 스코프이다. 렉시컬 스코프의 함수, 변수 검색은 해당 함수, 변수가 선언된 위치에 따라 정의된다.
- 호이스팅은 선언문을 컴파일레이션 과정에서 스코프 내의 꼭대기로 끌어올리는 동작이다.
- 클로저는 함수가 속한 렉시컬 스코프를 기억해서 함수가 렉시컬 스코프 밖에서 실행될 때에도, 이 스코프에 접근할 수 있도록 하는 기능이다.
- 모듈은 내부 함수를 마치 객체의 프로퍼티처럼 사용하는 자바스크립트 패턴이다. 이를 응용해 모듈이 오직 하나의 인스턴스만 생성할 수 있게 하는 싱글톤 패턴의 구현이 가능하다.
- ES6에서는 나누어진 개별 파일(.js)을 모듈 API로 불러와 사용할 수 있다. import, module, export 키워드를 이용해 모듈 내의 함수(메서드)와 프로퍼티를 불러와  사용하는 것이 가능하다.