본문 바로가기

도서

YOU DON'T KNOW JS(타입과 문법, 스코프와 클로저)

📚 후기



사실 이 책을 산지는 조금 됐었는데, 이제야 읽게 되었다. 이 책의 다른 시리즈인 'this와 객체 프로토타입, 비동기와 성능 편'도 함께 샀는데, 원래는 타입과 문법 / 스코프와 클로저 / this와 객체 프로토타입 / 비동기와 성능 4가지의 시리즈지만 번역된 책은 2개 시리즈씩 한 권으로 합쳐 판매 중이라 두 권을 구입하였다.

 

이 책의 저자인 카일 심슨의 깃허브에 들어가면 그가 쓴 YOU DON'T KNOW JS 시리즈를(물론 영어로) 모두 무료로 볼 수 있게 해 놓았다. 시리즈 중에 GO&UP은 JS에 대한 맛보기 정도라 그의 깃허브에서 읽고, 번역된 버전으로 두 권을 구입하였다.

 

알고 있었던 내용도 있었고, 전혀 모르고 있었던 부분(알고 있는 부분보다 훨씬 많았다!)도 있었다. 전체적인 평은 코딩하면서 느꼈던 가려웠던 부분을 속 시원하게 긁어주는 책이라고 생각됐다. 이쪽 공부를 하면서 느낀 점은 워낙에 개발자마다 주장하는 what to do와 what not to do가 다르고, 또 주장하는 의견에 대한 근거도 어느 한쪽 편을 들 수 없을 정도로 둘 다 말이 된다. 따라서 이 책에 대해서도 카일 심슨의 주관적인 생각이 많이 들어있다고 평하는 몇 개의 글도 몇 개 보게 되었는데, 그 부분에 대해서는 카일 심슨이 아래와 같이 답변해주었다. 교육자로서 정말 멋진 자세라고 생각이 되었다.

 

Should I listen to Kyle Simpson's opinions on Writing Javascript code?에 대한 카일심슨의 댓글

 

초보자가 읽기에는 많이 어렵게 느껴질 것 같다. 아까 말했듯이 시리즈 두 권으로 나눠져있고 일단 책이 얇기 때문에 책의 두께에서 오는 심리적 압박감(중요)이 덜하다는 장점이 있다. 컬러풀한 그림도 없고 다소 심심한 디자인이긴 하지만 중간중간 섞인 그의 조크때문에 재미있게 읽을 수 있어 좋았다. 또, 번역이 어색한 부분은 주석으로 상세하게 설명해주셔서 이해하는데 큰 어려움이 없었다.

 

특히, 챕터 중에 가장 도움이 됐다고 생각하는 부분은 '강제변환'이다. 여태 작성한 코딩이 돌아가는 걸 보면서 어렴풋이 내부적으로 어떻게 돌아가는 건지 궁금했던 부분이나, 아니면 완전히 잘못 넘겨짚고 있었던 부분을 시원하게 알려준다.

아래에는 알고 있었지만 여전히 중요하다고 느낀 내용, 아니면 몰랐는데 이 책을 통해 알게된 중요하다고 생각한 내용들을 정리해 보았다. 물론 책의 모든 내용을 다 외우고 바로바로 내 코드에 적용하면 좋겠지만 현실적으로 그러기에는 어렵다고 느꼈기 때문에, 책을 읽으면서 밑줄 친 부분이나 따로 표시해둔 부분을 글로나마 간략하게(나름 간략하게 적으려고했는데...) 정리해서 생각날 때마다 다시 보려고 한다.

 

 

📝 정리



 

타입


typeof 값이 없는 변수 or 선언되지 않은(undeclared) 변수의 결과는 "undefined"로 나온다.

  • undefined - 접근 가능한 스코프에 변수가 선언되었으나 현재 아무런 값도 할당되지 않은 경우
  • undefined - 접근 가능한 스코프에 변수 자체가 선언되지 않은 경우

 


  • 문자열은 불변 값이지만 배열은 가변 값이다. 따라서 문자열 메서드는 항상 새로운 문자열을 생성한 후 반환한다. 반면에 대부분의 배열 메서드는 그 자리에서 곧바로 원소를 수정한다.

  • NaN은 '숫자 아님'보다는 '유효하지 않은(Invalid) 숫자', '실패한(Failed) 숫자', 또는 '몹쓸 숫자'라고 하는게 차라리 더 정확하다.

  • typeof NaN === "number" //true

  • NaN은 어떤 NaN과도 동등하지 않다.(자기 자신과도 같지 않다.) 따라서, NaN !== NaN 이다. NaN의 여부는 Number.isNaN() 함수(ES 6부터)를 사용하면 된다.

  • +0과 -0이 존재한다. -0은 문자 열화 하면 항상 "0"이 된다.

  • null, undefined, string, number, boolean 그리고 ES6의 symbol 같은(스칼라 원시 값)은 언제나 값-복사 방식으로 할당/전달된다. 반면에 객체나 함수 등 합성 값은 할당/전달 시 반드시 레퍼런스 사본을 생성한다.

 

네이티브


  • 네이티브(Natives)는 여러 가지 내장 타입을 말한다. 다음은 가장 많이 쓰는 네이티브 들이다.

    • String(), Number(), Boolean(), Array(), Object(), Function(), RegExp(), Date(), Error(), Symbol()
  • 원시 값엔 프로퍼티나 메서드가 없으므로, .length, .toString() 으로 접근하려면 원시 값을 객체 래퍼로 감싸줘야 한다. 하지만 오래전부터 브라우저는 이런 흔한 경우를 스스로 최적화하기 때문에 개발자가 직접 객체 형태로 pre-Optimize 할 필요가 없다. 프로그램이 더 느려질 수도 있으므로 필요시 엔진이 알아서 암시적으로 박싱 하게 하는 것이 낫다.

  • 객체 래퍼의 원시 값은 valueOf() 메서드로 추출한다.

  • date 객체는 유닉스 타임스탬프 값(1970년 1월 1일부터 현재까지 흐른 시간을 초 단위로 환산)을 얻는 용도로 가장 많이 쓰일 것이다. data 객체의 인스턴스로 부터 getTime()을 호출하면 되지만, ES6에 정의된 정적 도우미 함수, Date.now()를 사용하는 것이 더 쉽다.

  • error 객체의 주 용도는 현재의 실행 스택 콘텍스트를 포착하여 객체에 담는 것이다. 사람이 읽기 편한 포맷으로 에러 메세지를 보려면 error객체의 toString()을 호출하는 것이 가장 좋다.

  • Symbol은 ES6에서 처음 선보인, 새로운 원시 값 타입이다. 충돌 염려 없이 객체 프로퍼티로 사용 가능한, 특별한 '유일 값'이다. Symbol()은 앞에 new를 붙이면 에러가 나는, 유일한 네이티브 '생성자'다.

  • 내장 네이티브 생성자는 각자의 .prototype 객체를 가진다. prototype 객체에는 해당 객체의 하위 타입별로 고유한 로직이 담겨있다.

    • String.prototype.XYZ는 String#XYZ로 줄여 쓴다.

 

강제 변환


  • 어떤 값을 다른 타입의 값으로 바꾸는 과정이 명시적이면 타입 캐스팅, 암시적이면 강제 변환이라고 한다.

  • JSON.stringify()는 인자가 undefined, 함수, 심벌 값이면 자동으로 누락시키며 이런 값들이 만약 배열에 포함되어 있으면(배열 인덱스 정보가 뒤바뀌지 않도록) null로 바꾼다.

  • toJSON()을 (어떤 타입이든) 적절히 평범한 실제 값을 반환하고 문자 열화 처리는 JSON.stringify()이 담당한다. 다시 말해 toJSON()의 역할은 '문자열 화하기 적당한 JSON 안전 값으로 바꾸는 것'이지 'JSON 문자열로 바꾸는 것'이 아니다.

  • '숫자 아닌 값 -> 수식 연산이 가능한 숫자' 변환 로직은 ToNumber 추상 연산에 정의되어 있다. true는 1, false는 0, undefined는 NaN, null은 0으로 바뀐다.

  • 'falsy' 목록 : undefined, null, false, +0, -0, NaN, ""

  • indexOf()에 ~를 붙이면 어떤 값을 '강제 변환'(실제로는 단순히 변형)하여 불리언 값으로 적절하게 만들 수 있다.

  • ~은 indexOf()로 검색 결과 '실패'시 -1을 falsy 한 0으로, 그 외에는 truthy 한 값으로 바꾼다.

  • ! 부정 단항 연산자도 값을 불리언으로 명시적으로 강제 변환한다. 문제는 그 과정에서 truthy, falsy까지 뒤바뀐다는 점이다. 따라서 명시적인 강제 변환을 할 땐 !! 이중 부정 연산자를 사용한다.

  • && 또는 || 연산자의 결괏값이 반드시 불리언 타입이어야 하는 것은 아니며 항상 두 피연산자 표현식 중 _어느 한쪽 값으로 귀결_된다. '평가 결과'가 아니다.

  • 동등함의 비교 시 ==는 강제 변환을 허용하지만, ===는 강제 변환을 허용하지 않는다.

  • !=의 결괏값은 == 연산자의 동등 비교 수행 후 그 결과를 그대로 부정한 값이다. 엄격한 비 동등 연산자 !== 역시 마찬가지이다.

  • ==의 피연산자 한쪽이 불리언 값이면 예외 없이 그 값이 먼저 숫자로 강제 변환된다.

  • null과 undefined를 == 비교하면 서로에게 강제 변환한다. null <-> undefined 강제 변환은 안전하고 예측 가능하며, 어떤 다른 값도 비교 결과 긍정 오류를 할 가능성이 없다.

  • 피연산자 중 하나가 [], "", 0이 될 가능성이 있으면 가급적 == 연산자는 쓰지 않는 것이 좋다.

 

문법


  • try-finally 블럭에서 try에 return 문이 있다면, finally 블럭의 코드가 실행이 끝나고 완료 값이 반환된다.

  • try에 throw 가 있어도 위와 비슷하다. 만약 finally 절에서 예외가 던져지면, 이전의 실행 결과는 모두 무시한다. 즉, 이전의 try 블록에서 생성한 완료 값이 있어도 완전히 사장된다.

  • finally 절의 return 은 그 이전에 실행된 try나 catch 절의 return을 덮어쓰는 특출한 능력을 가지고 있는데, 단 반드시 명시적으로 return 문을 써야 한다.

    • switch 표현식과 case 표현식 간의 매치 과정은 ===알고리즘과 똑같다.

  • switch 문에서 default 절은 선택 사항이며 꼭 끝 부분에 쓸 필요는 없다. 그런데 default에서도 break를 안 써주면 그 이후로 코드가 계속 실행된다.

 

스코프


  • 스코프는 어디서 어떻게 변수(확인자)를 찾는가를 결정하는 규칙의 집합이다. 변수를 검색하는 이유는 변수에 값을 대입하거나(LHS 참조) 변수의 값을 얻어오기 위해서다(RHS 참조).

  • 자바스크립트는 일반적으로 '동적' 또는 '인터프리터'언어로 분류하나 사실은 '컴파일러 언어'이다. 물론 자바스크립트가 전통적인 많은 컴파일러 언어처럼 코드를 미리 컴파일하거나 컴파일한 결과를 분산 시스템에서 이용할 수 있는 것은 아니다. 전통적인 컴파일러 언어의 처리 과정에서는 프로그램을 이루는 소스 코드가 실행되기 전에 보통 3단계를 거치는데, 이를 컴파일레이션(Compilation)이라고 한다.

    1. 토크나이징/렉싱(Tokenizing/Lexing) : 문자열을 나누어 '토큰'이라 불리는 의미 있는 조각으로 만드는 과정

    2. 파싱(Parsing) : 토큰 배열을 프로그램의 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정 -> 결과로 AST(Abstract Syntax Tree)를 만든다.

    3. 코드 생성(Code-Generation): AST를 컴퓨터에서 실행 코드로 바꾸는 과정이다.

  • 자바 스크립트 엔진이 기존 컴파일러와 다른 점은 자바스크립트 컴파일레이션을 미리 수행하지 않아서 최적화할 시간이 많지 않다는 것이다. 자바스크립트 컴파일레이션은 보통 코드가 실행되기 겨우 수백만 분의 일초 전에 수행한다. 자바스크립트 엔진은 가능한 한 가장 빠른 성능을 내기 위해 이 책에서 다룰 범위를 가볍게 넘어서는 여러 종류의 트릭을 이용한다.

  • 간단히 말하자면, 어떤 자바스크립트 조각이라도 실행되려면 먼저(보통 바로 직전에!) 컴파일되어야 한다는 것이다.

  • LHS와 RHS 참조 검색은 모두 현재 실행 중인 스코프에서 시작한다. 그리고 필요하다면(대상 변수를 차지 못했을 경우) 한 번에 한 스코프씩 중첩 스코프의 상위 스코프로 넘어가며 확인자를 찾는다. 이 작업은 글로벌 스코프(최상위 층)에 이를 때까지 계속하고 대상을 찾았든, 못 찾았든 작업을 중단한다.

  • 렉시컬 스코프는 프로그래머가 코드를 짤 때 변수와 스코프 블록을 어디에서 작성하는가에 기초에서 렉서(lexer)가 코드를 처리할 때 확정된다. 컴파일레이션의 렉싱 단계에서는 모든 확인자가 어디서 어떻게 선언됐는지 파악하여 실행 단계에서 어떻게 확인자를 검색할지 예상할 수 있도록 도와준다.

  • 스코프를 이용해 코드를 숨기는 방식은 소프트웨어 디자인 원칙인 '최소 권한의 원칙'과 관련이 있다. 이 원칙은 모듈/객체의 API와 같은 소프트웨어를 설계할 때 필요한 것만 최소한으로 남기고 나머지는 '숨겨야'한다는 것이다.

  • 위의 '숨기기'의 또 다른 장점은 같은 이름을 가졌지만 다른 용도를 가진 두 확인자가 충돌하는 것을 피할 수 있다는 점이다.

  • let은 var같이 변수를 선언하는 다른 방식이다. 키워드 let은 선언된 변수를 둘러싼 아무 블록(일반적으로 {})의 스코프에 붙인다.

  • let을 사용한 선언문은 속하는 스코프에서 호이스팅 효과를 받지 않는다. 따라서 let으로 선언된 변수는 실제 선언문 전에는 명백하게 '존재'하지 않는다.

  • const 역시 블록 스코프를 생성하지만, 선언된 값은 고정된다.

 

호이스팅


  • 변수와 함수 선언문은 선언된 위치에서 코드의 꼭대기로 '끌어올려'진다. 이렇게 _선언문_을 끌어올리는 동작을 '호이스팅(hoisting)'이라고 한다.

  • 호이스팅은 스코프 별로 작동한다.

  • 함수와 변수 선언문 모두 끌어올려지지만, 먼저 함수가 끌어올려지고 다음으로 변수가 올려진다.

  • 따라서 스코프의 모든 선언문은 어디서 나타나든 실행 전에 먼저 처리된다.

 

스코프 클로저


  • 클로저는 함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻한다.

  • 즉, 함수를 렉시컬 스코프 밖에서 호출해도 함수는 자신의 렉시컬 스코프를 기억하고 접근할 수 있는 특성을 의미한다.

  • 클로저는 다양한 형태의 모듈 패턴을 가능하게 하는 매우 효과적인 도구이기도 하다. 모듈은 아래와 같은 두 가지 특징을 가져야 한다.

    1. 최외곽 래퍼 함수를 호출하여 외곽 스코프를 생성한다.

    2. 래핑 함수의 반환 값은 반드시 하나 이상의 내부 함수 참조를 가져야 하고, 그 내부 함수는 래퍼의 비공개 내부 스코프에 대한 클로저를 가져야 한다.

'도서' 카테고리의 다른 글

그림으로 공부하는 IT 인프라 구조  (0) 2019.08.30
그림으로 배우는 Http & Network Basic  (0) 2019.08.08