Effective JavaScript - 1. 자바스크립트에 익숙해지기

이정우·2021년 9월 26일
1

Effective JavaScript

목록 보기
1/1

자바스크립트는 개발자에게 친근하게 설계되었다. 자바의 문법과 유사하며 다른 스크립트 언어와 많은 공통점을 지녔다.
비교적 빠르게 습득할 수는 있지만, 마스터하는 것은 오랜 시간이 걸린다.

Item 1. 어떤 자바스크립트를 사용하고 있는지 알아야 한다

JavaScript

자바스크립트도 다른 기술들과 마찬가지로 오랜 시간에 걸쳐 진화해왔다. 웹 분야에서는 지배적인 언어가 되었고, 그 인기로 인해 1997년에는 ECMAScript라는 이름으로 전세계 표준이 생겨났다.

자바스크립트의 오랜 역사와 다양한 구현체로 인해 플랫폼마다 사용 가능한 기능이 다른 경우가 있다. 이는 웹 브라우저가 개발자에게 어떤 버전의 자바스크립트를 사용할 수 있을지 제공하고 있지 않기 때문이다. 또한 사용자마다 다른 버전이나 다른 웹 브라우저를 사용하는 경우도 있기에 모든 브라우저에서 동작할 수 있도록 작성해야 한다.

스트릭트 모드

ES5에서는 다른 버전으로 인해 발생하는 문제점을 해결하기 위해 스트릭트(strict) 모드가 추가되었다. 이 기능은 옵션을 통해 적용할 수 있으며, 문제가 일으킬 만한 기능을 사용할 수 없게 만든다.

스트릭트 모드는 프로그램의 최상단이나 함수의 바디 처음 부분에 다음과 같이 명령어를 추가하여 활성화시킬 수 있다.

// 프로그램의 최상단
“use strict”;

// 함수의 바디 최상단
const f = (x)  => {
    “use strict”;}

문자열을 명령어로 사용하는 것이 이상할 수도 있지만, 스트릭트 모드를 사용할 수 없는 오래된 엔진을 사용하는 경우에는 문자열을 읽고 넘어가기 때문에 아무런 문제가 없다는 이점이 있다.

스트릭트 모드 사용 시 주의해야 할 점은 명령어를 상단에 선언했을 때만 인식이 가능하기 때문에 다른 소스 파일을 합할 때, 스트릭트 모드가 제대로 적용이 되지 않을 수 있다는 것이다.


Item 2. 자바스크립트의 부동 소수점 숫자 이해하기

숫자형 데이터 number

대부분의 프로그래밍 언어는 int, double, float 등 여러 가지 숫자 자료형을 가진다. 하지만 자바스크립트에서는 number 라는 하나의 자료형만을 가진다.

typeof 17; // number
typeof 98.6; // number
typeof -2.1; // number

실제로는 number는 64비트의 부동 소수점인 double로 구현되어 있다. 따라서 실수와 정수 간 연산에는 별도의 형변환이 필요하지 않다.

부동 소수점

하지만, 부동 소수점 연산은 부정확하다는 단점이 존재한다. 간단한 연산에서도 잘못된 결과가 나올 수 있으며, 실수에서 항상 성립하는 결합 법칙을 위배하는 경우도 존재한다.

// 잘못된 연산 결과
0.1 + 0.2; // 0.30000000000004

// 결합 법칙 위배
(0.1 + 0.2) + 0.3; // 0.600000000001
0.1 + (0.2 + 0.3); // 0.6

따라서 정확도가 중요한 곳에서는 정수로 변환하여 연산을 하는 등의 주의가 필요하다.


Item 3. 암묵적인 형변환을 주의하라

자료형 오류

자바스크립트는 자료형 오류에 관대한 편이다. 다음과 같은 표현식에서도 오류가 발생하지 않고 당연하다는 듯이 4라는 값을 반환한다.

3 + true; // 4

오류가 발생하는 경우는, 함수가 아닌데 함수처럼 호출하거나 null의 프로퍼티에 접근하는 등 몇몇 경우 밖에 존재하지 않는다.

“hello”(1); // TypeError : “hello” is not a function.
null.x; // TypeError : null is not an object.

자동 형변환

대부분의 경우에서는 오류를 발생시키는 대신, 예상된 자료형으로 형변환을 수행한다.

// 숫자를 문자열로 변환2+ 3; // “23”
2 +3; // “23”

하지만 연산의 순서에 따라 원하는 결과를 반환하지 않을 수 있기 때문에 주의해서 사용해야 한다. 이는 Left-Associative(좌측 결합성, 왼쪽의 항목부터 연산이 이루어짐)는 속성 때문이다.

1 + 2 +3; // “33”, (1 + 2) + “3” 과 동일
1 +2+ 3; // ”123”, (1 + “2”) + 3 과 동일

NaN

형변환은 오류를 숨길 때도 있다. null이 들어간 연산에서 오류를 발생시키지 않고 0으로 계산하거나, 정의되지 않은 변수를 NaN (Not a Number)로 취급한다. 이런 형변환으로 인해 오류가 발생하지 않기 때문에 예측하기 어려운 결과가 발생한다.

또한, NaN은 자기 자신과 비교하더라도 같지 않다는 이상한 특징을 가지고 있다.

// null을 0으로 취급
1 + null; // 1

// NaN
var x = NaN;
x === NaN; // false 

표준 라이브러리에 존재하는 isNaN 함수는 값을 테스트하기 전에 자동으로 형변환을 수행하여 인자를 숫자로 바꾸기 때문에 원하는 결과를 얻기가 어렵다.

숫자형 이외의 자료형에서 확인하기 위해서는 NaN의 특성을 이용해야 한다. 앞에서도 말했듯 NaN은 자바스크립트에서 유일하게 자기 자신과 동일하지 않은 값이다.

// isNaN으로 NaN을 확인할 수 없음
isNaN(“foo”); // true
isNaN(undefined); // true
isNaN({}); // true

// isNaN을 사용하지 않고 확인하는 방법
var a = NaN;
a !== a; // true;
var b = “foo”;
b !== b; // false;
var c = undefined;
c !== c; // false;
var d = {};
d !== d; // false;

// isNaN을 대신할 함수
const isReallyNaN = (x) => x !== x;

객체의 형변환

객체도 원시 자료형으로 강제 형변환이 이루어질 수 있으며, 암묵적으로 toString 메소드가 호출되어 문자열로 변환된다.

“Math object :+ Math; // “Math object : [object Math]”
Math.toString(); // “[object Math]”

비슷하게 valueOf 메소드를 통해 숫자로 변환될 수도 있다. 따라서 객체에서는 메소드를 정의하여 형변환을 제어할 수 있다.

J+ { toString : () =>S}; // JS
2 * { valueOf : () => 3 }; // 6

하지만 객체가 toStringvalueOf 메소드를 모두 포함한 경우에는 원하는 결과를 얻기가 어렵다.

const obj = {
      toString : () =>[object CoolObject],
      valueOf : () => 17
}

“object :+ obj; // “object : 17”;

이는 자동 형변환을 수행할 때 valueOf를 먼저 수행한 후, toString을 수행하기 때문이다. 따라서 vauleOf는 객체가 숫자로 된 값을 가질 때 사용하거나, toStringvalueOf의 문자열 표현을 나타낼 때만 사용해야 한다.

Truthiness

if, ||, && 등의 연산자는 논리적으로는 불리언 값을 통해 수행되나, 실제로는 모든 값을 사용할 수 있다. 강제 형변환을 통해 불리언 값으로 해석할 수 있기 때문이다. 몇몇 false로 해석되는 값을 제외하고 모든 값은 true로 자동 변환된다.

false로 변환되는 값들

  • false
  • 0, -0
  • “”
  • NaN
  • null
  • undefined

특정 값이 undefined인지 확인할 때는 typeof를 사용하거나 직접 비교를 해야 한다.

const f = (x) => x ? x + 100 : “error!;

f(0); // “error!”
f(1); // 101

// x === undefined 로도 사용 가능
const g = (x) => typeof x ===undefined? “error!: x + 100;
g(0); // 100
g(1); // 101

Item 4. 객체 래퍼보다 원시 데이터형을 우선시하라

객체와 더불어, 자바스크립트는 boolean, number, string, null, undefined의 5가지 Primitive type을 가진다. 또한, 표준 라이브러리는 boolean, number, string을 객체처럼 감싸는 생성자를 제공하기 때문에 값을 감싼 객체를 생성할 수 있다.

const s = new String("hello"); // "hello"
const n = new Number(1); // 1
const b = new Boolean(false); // false

이렇게 생성한 String 객체는 감싸고 있는 문자열의 값과 비슷하게 동작하며, 인덱스를 통해 문자열의 문자를 추출할 수도 있다.

s + " world"; // "hello world";

s[4]; // "o"

하지만, Primitive Type으로 선언한 문자열과는 다르게 String 객체는 실제로 객체이다. 이러한 차이점은 서로 다른 String 객체를 연산자를 사용해 비교할 수 없다는 결과를 불러온다.

// 서로 다른 타입을 가지는 문자열
typeof "hello"; // "string"
typeof s; // "object"

const s1 = new String("hello");
const s2 = new String("hello");

// s1, s2 모두 "hello"라는 값을 가졌지만 연산 결과는 false
s1 === s2; // false

// == 연산자 역시 false!

자주 사용하지도 않는 이러한 래퍼(Wrapper)들이 존재하는 이유는 여러 유틸리티 메소드 때문이다. 강제 형변환에 래퍼를 사용함으로써 프로토타입 객체의 메소드를 호출하거나 프로퍼티를 추출할 수 있다. 또한, Primitive 데이터 값에 영향을 주지 않고 프로퍼티를 설정할 수 있다.

// String 객체에 존재하는 toUpperCase 메소드
"hello".toUpperCase(); // "HELLO"

"hello".property = 17;
"hello".property; // undefined

암묵적인 감싸기는 실행될 때마다 새로운 객체를 생성하기 때문에 처음의 래퍼 객체를 수정하더라도 영향을 미치지 않는다. 따라서 실제로는 Primity Type의 데이터에는 프로퍼티를 설정할 수 없게 되며, 이 또한 자료형의 오류를 숨기는 다른 사례이다.


Item 5. 혼합된 데이터형을 ==로 비교하지 마라

// 결과가 무엇일까?
"1.0e0" == { valueOf : () => true };

이렇게 말도 안 되는 것 같은 연산도 자바스크립트의 형변환에 의해 true라는 결과가 나온다. "1.0e0"은 숫자 1로, 객체는 valueOf 메소드가 호출되어 반환된 true를 1로 변환하기 때문에 발생한 결과다.

비교를 수행하는 두 인자가 동일한 자료형이라면 ==, === 모두 동일한 결과가 나타난다. 하지만 다른 자료형이라면 ==의 경우에는 자동 형변환을 통해 값을 비교하기 때문에 원하는 결과가 나타나지 않을 것이다.

강제 형변환 규칙
인자 타입1 인자 타입2 강제 형변환
null undefined 없음(항상 true)
null || undefined !null && !undefined 없음(항상 false)
Primitive Type(string, number, boolean) Date 객체 Primitive Type => 숫자
Date => Primitive Type(toString -> valueOf 순서)
Primitive Type(string, number, boolean) Date가 아닌 객체 Primitive Type => 숫자
Date가 아닌 객체 => Primitive Type(valueOf -> toString 순서)
Primitive Type(string, number, boolean) Primitive Type(string, number, boolean) 숫자

대부분의 형변환은 숫자로 값을 변경하려는 시도를 하는데, 객체의 경우는 valueOftoString 메소드를 호출하여 처음으로 얻는 Primitive Type으로 값을 바꾼다.

// 원하는 결과가 나오지 않음
const date = new Date("2021/09/26");

date == "2021/09/26"; // false

date.toString(); // Sun Sep 26 2021 00:00:00 GMT+0900 (한국 표준시)

== 연산자를 사용하여 비교하면 원하는 결과를 얻을 수가 없다. 코드를 작성하는 사람과 읽는 사람 모두 위의 표에 나와있는 형변환 규칙을 외워야하는 불편함까지 생긴다.

따라서 프로그램의 오류를 줄이고 정확하게 비교를 하고 싶다면 명시적으로 형변환을 수행하거나 강력한 비교 연산자인 ===을 사용하자.


Item 6. 세미콜론 삽입의 한계에 대해서 알아두자

자바스크립트의 편리한 점 중 하나는 세미콜론(;)을 생략하더라도 자동으로 삽입된다는 것이다. 이는 특정 문맥에서 생략된 세미콜론을 추론하여 프로그램을 파싱하기 때문에 가능한 일이다. 하지만, 당연하게도 세미콜론을 생략할 수 없는 경우도 존재한다!

세미콜론은 한 줄 이상의 새로운 행이나, 프로그램 입력의 마지막이나 } 전에만 삽입된다.

다시 말하자면, 줄/블록/프로그램의 마지막 부분에 있는 세미콜론만 생략이 가능하다는 것이다.

// 생략 가능
const square = (x) => {
  const n = +x
  return n * n
}

const area = (r) => { r = +r; return Math.PI * r * r }

const add1 = (x) => { return x + 1 }

// 생략 불가
const area = (r) => {r = +r return Math.PI * r * r }

세미콜론은 다음 입력 토큰을 파싱할 수 없을 때에만 삽입된다.

세미콜론 삽입은 오류를 보정하기 위한 매커니즘이기 때문에, 다음 문장을 해석할 수 없는 경우 삽입이 된다.

따라서 다음 선언의 시작 부분을 주의해야 한다. 다음 줄의 첫 문자가 이전 줄의 연장으로 해석이 가능할 경우, 세미콜론이 삽입되지 않기 때문이다. 이 때 주의해야 하는 문자는 (, [, +, -, / 다섯 개다. 이 문자들은 문맥에 따라 표현식의 연산자나 선언의 접두어로 사용될 수 있다.

// 세미콜론이 삽입되지 않음
a = b
(f());

a = b(f());

// 세미콜론이 삽입됨
a = b
f()

a = b;
f();

a = b f(); // 세미콜론을 삽입하지 않을 경우, 오류가 발생하기 때문

또한, 오류가 발생하지 않더라도 강제적으로 세미콜론을 삽입하는 경우도 존재한다. 가장 대표적인 예가 return 부분이다.

// 새로운 객체를 반환
return {}; 

// 강제로 세미콜론 삽입
return
{};

return;
{};

이외에도 throw, 이름표가 있는 breakcontinue, ++, -- 등의 연산자 접미어 등이 있다.

a
++
b

// ++ 연산자는 접두어/접미어 모두 가능하나 새로운 행에서는 접미어로 사용이 불가
a; b++;

세미콜론은 for 반복문의 구분자나 빈 선언문으로 절대 삽입되지 않는다.

for 반복문에는 세미콜론을 생략할 수 없다! 반드시 사용하자.

또한, 본문이 빈 루프도 세미콜론을 생략하면 오류가 발생한다.

// for문의 조건에 세미콜론 생략 시 오류 발생
for(let i = 0, count = 1
    i < n
    i++) {
    total *= i
}

// 본문이 빈 루프에 세미콜론 생략 시 오류 발생
const infiniteLoop = () => { while(true) }

Item 7. 문자열을 16비트 코드 단위의 시퀀스로 간주하라

처음 유니코드가 나왔을 때만 하더라도 많은 문자가 쓰이지 않을 것이라 생각해 모든 문자를 16비트에 할당하여 사용하는 UCS-2를 사용하였다. UCS-2 인코딩은 문자열로 변환하는 비용이 저렴하고 수행 시간이 짧기 때문에 많은 언어에서 채택되었는데, 자바스크립트도 예외는 아니었다.

하지만 현재는 코드 포인트의 범위가 확장되어 대리 쌍이라고 불리는 코드 포인트를 인코딩하는 한 쌍의 코드 유닛을 사용하게 되었다. 다시 말해, 예전과는 달리 한 글자를 표현하기 위해 두 쌍의 코드 포인트를 사용한다는 것이다.

대리 쌍을 사용한 문자의 등장으로 인해, 자바스크립트의 프로퍼티와 메소드들은 고장이 났다.

// 대리 쌍을 사용한 특수문자
"𝄞 clef".length; // 7
"G clef".length; // 6

"𝄞 clef".charAt(0); // '\uD834'
"𝄞 clef".charAt(1); // '\uDD1E'
"𝄞 clef".charAt(2); // ' '

눈으로 보기에는 한 글자를 차지하는 특수 문자가, 코드 레벨에서는 2글자를 차지해버리는 것이다. 유니코드 문자열을 처리하는 어플리케이션은 서드파티 라이브러리를 사용하는 것이 정답일 수 있다.

하지만 모든 메소드가 대리 쌍에 대해 처리를 못하는 것은 아니다. 표준 라이브러리에 존재하는 encodeURI, decodeURI 등의 URI 조작 함수는 대리 쌍을 정확하게 처리한다.

따라서 문자열을 처리하는 라이브러리를 사용할 때는 해당 라이브러리의 문서를 반드시 찾아봐야 한다.

0개의 댓글