[기초부터 완성까지, 프런트엔드] 3장. 자바스크립트 기초

Posted by : on

Category : FrontendBook


자바스크립트는 1995년 넷스케이프 커뮤니케이션즈의 개발자인 브렌던 아이크(Brendan Eich)가 개발한 프로그래밍 언어이다. 최초에는 모카(Mocha)라는 이름이었다가, 잠시 라이브 스크립트라는 이름도 가졌다. 이후 자바의 인기에 편승하려는 의도로 자바스크립트라는 이름으로 변경해 현재까지 사용하고 있다. 1996년 넷스케이프는 자바스크립트의 규격을 ECMA International에 제출했다. ECMA International은 ECMA-262라는 명세를 만들었고, 이것이 표준화된 자바스크립트의 출발점이다. 현재 ECMA-262의 명세 관리는 ECMAScript international의 TC39에서 담당하고 있다.
자바스크립트는 ECMAScript의 명세 구현을 목표로 개발되며, ECMAScript의 명세는 ES1을 시작으로 현재는 ES2021까지 나온 상태다. 변수를 선언하고 구문을 작성하는 규칙, 객체와 타입의 정의 등 자바스크립트의 기본 개념들을 ECMAScript의 명세를 기반으로 알아보자!

ECMA International ?
ECMA는 European Computer Manufacturers Association의 약자로, 기구가 국제화됨에 따라 International이라는 이름이 추가되었다. ECMA International은 정보와 통신 시스템의 국제적 표준화를 하는 비영리 기관이며 현재 스위스 제네바에 위치한다.

TC39 ?
Technical Committee 39의 줄임말로, TC39는 정기적인 회의를 통해 새로운 명세에 대한 의견을 공유한다.

ES2021 ?
ECMAScript의 초기 버전은 1씩 늘어나는 숫자로 이름을 지정했지만, 2015부터는 공개 연도를 기준으로 지정한다.

변수 선언

다른 언어와 마찬가지로 자바스크립트도 값을 저장하고 접근하기 위해 변수를 선언해 사용한다. 다만, 정적 타입 언어와는 다르게 자바스크립트는 느슨한 타입 (loose typing)을 가진 언어이기 때문에 데이터 타입을 따로 명시하지 않고 변수를 선언할 수 있다.

느슨한 타입이라고 타입이 존재하지 않는 것은 아니다. 변수를 선언할 때 타입을 명시하지 않는 것일 뿐, 내부적으로는 데이터의 종류에 따른 변수의 타입을 가진다.

자바스크립트에서는 var, let, const 세 가지 키워드를 사용해 변수를 선언하며, 이 변수에 값을 할당한다. (모든 키워드에 대해 값을 할당할 수 있는 것은 아니다.)

var a;
let b;
const c;

// 선언된 변수에 값을 할당할 수 있다.
a = 1;

위 코드처럼 변수를 선언하고 초기화하지 않으면 undefined 값이 자동으로 할당된다. 또한 하나의 선언문에서 여러 개의 변수를 선언하거나, 선언과 초기화를 동시에 하기도 한다.

var a, b, c;
let num1 = 1,
  num2 = 2,
  num3 = 3;

scope ?
> scope(스코프)는 어떤 변수들에 접근할 수 있는지 정의한 범위이다.

var

var 키워드를 사용해 변수를 선언하는 것은 ES2015(ES6)에서 let과 const가 등장하기 전까지 유일한 방법이었다. var로 선언된 변수는 기존에 선언된 변수의 값을 덮어쓰며, 함수 스코프를 기준으로 동작한다.

var a = 1;

if (isSomething()) {
  var a = 2;
}

console.log(a); // 2

a라는 변수를 선언해 1이라는 값을 할당하고, 다음 조건문 내에서 변수 a를 다시 선언했다. 기존 a 변수의 값을 덮어쓰며, 어떠한 에러도 발생하지 않는다.
var로 변수를 선언할 경우 스코프 내에 이미 동일한 식별자를 가진 변수가 존재한다면 해당 변수에 값을 재할당한다. 특히 특정한 조건에 따라 기존에 선언된 변수의 값을 덮어쓴다면 다른 코드에 영향을 주거나 버그가 발생할 때 원인을 찾기 어렵다.

블록 또는 함수에서 선언하지 않은 자바스크립트의 변수는 모두 전역 스코프를 기준으로 선언되며 이를 전역변수라고 부른다. (ES 모듈 기반이 아닌 일반 스크립트 기준) 전역 스코프가 아닌 특정 함수 내에서 var로 선언한 변수는 함수 스코프를 가진다. 변수가 함수 스코프를 가진다는 것은 변수를 선언한 함수 몸체 안에서만 해당 변수에 접근할 수 있다는 의미이다.

function foo() {
  var a = 1;
  console.log(a); // 1
}

console.log(a); // Uncaught ReferenceError: a is not defined

foo() 함수 안에서 변수 a를 선언했다. 이 때 변수 a는 foo() 함수에 대한 스코프를 가진다고 하며, 이는 foo() 함수 내에서만 변수 a에 접근할 수 있다는 의미이다. foo() 함수의 외부 스코프인 전역 스코프에서는 변수 a에 접근할 수 없어 ReferenceError가 발생한다. 하지만 함수 스코프는 블록을 무시해 종종 문제가 되기도 한다.

function foo() {
  for (var i = 0; i < 10; i += 1) {
    // ...
  }
  console.log(i); // 10
}

foo();

for 반복문의 초기값으로 변수 i를 선언했다. 반복문이 종료되면 변수 i에 접근할 수 없을 것 같지만, 변수 i는 블록이 아닌 함수 스코프를 가지기 때문에 foo() 함수의 실행이 종료되기 전까지 접근할 수 있다. 대부분 이러한 접근은 불필요하며 혼란만 초래한다. 만약 전역 스코프 또는 foo() 함수 내에 이미 i라는 변수가 선언되었다면 값이 덮어 씌워져 예상치 못한 문제가 발생하였을 것이다.

var를 사용하면 언급한 바와 같이 개발자의 실수로 예측하기 어려운 문제가 발생할 수 있다. 이런 문제점을 해결하기 위해 ES2015에서 letconst가 등장한다.

let, const

letconst는 ES2015에서 등장한 변수 선언 키워드이다. let과 const는 var와 달리 재선언을 허용하지 않으며, 함수 스코프가 아닌 블록 스코프를 가진다.

let a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared

const b = 2;
const b = 2; // Uncaught SyntaxError: Identifier 'b' has already been declared

let이나 const로 선언한 변수는 재선언을 시도한다면 SyntaxError가 발생한다. 그렇기 때문에 var와 달리 let과 const는 예상치 못하게 기존 변수의 값이 바뀌는 위험없이 안정적으로 변수를 사용할 수 있다.
let과 const는 블록 스코프를 가지는데, 블록 스코프를 가진다는 것은 변수를 둘러싼 블록({}) 안에서만 해당 변수에 접근할 수 있다는 의미이다.

{
  let a = 1;
}

console.log(a); // Uncaught ReferenceError: a is not defined

let은 블록 스코프를 가지기 때문에 블록을 벗어난 외부 스코프에서 접근하려고 하면 위 예제 코드처럼 ReferenceError가 발생한다. 이러한 특징은 for 반복문에서 유용하게 사용할 수 있다.

function foo() {
  for (let i = 0; i < 10; i += 1) {
    // ...
    console.log(i);
  }

  console.log(i); // Uncaught ReferenceError: i is not defined
}

foo();

let은 재선언을 허용하지 않으며, for 반복문의 블록 스코프에 묶여 외부의 변수 값을 덮어쓰거나 불필요한 참조가 되는 문제를 막는다. 또한 반복문이 수행될 때마다 지역 변수로서 새로 선언되어 반복문 내에서 안전하게 값을 사용할 수 있다.

let a = 1;
a = 2;

console.log(a); // 2

const b = 1;
b = 2; // Uncaught TypeError: Assignment to constant variable

constlet과 달리 값의 변경을 허용하지 않는다. 값을 변경하려고 하면 TypeError가 발생한다. 이러한 특징 때문에 const는 상수처럼 변하지 않는 값을 선언할 때 사용한다.

객체와 타입

자바스크립트의 타입은 다른 언어들과 달리 매우 느슨하다. 데이터 타입과 상관없이 var, let, const와 같은 키워드로 변수를 선언한다. 엄격한 타입 언어가 아니라서 각 타입의 특징과 타입 변환에 대해 제대로 이해하지 못하면, 원치 않는 동작으로 애플리케이션에 에러가 발생하기도 한다.
자바스크립트의 값은 원시타입(Primitive Type)과 객체(참조형)로 나뉜다. 원시타입은 다음처럼 7가지가 있다.

  • number
  • string
  • boolean
  • null
  • undefined
  • Symbol
  • BigInt (ES2020에서 추가)

원시 타입은 하나의 값만 가지며, 불변 데이터이므로 연산을 해도 기존 값이 변경되지 않는다.

const result = "hello" + "javascript";

‘hello’ 문자열과 ‘javascript’ 문자열을 병합하는 연산을 수행한다. 연산 결과인 ‘hellojavascript’ 문자열이 result라는 변수의 값으로 할당되지만, ‘hello’와 ‘javascript’라는 두 문자열 자체의 값은 변하지 않는다.

원시 타입을 제외한 나머지는 모두 객체이다. 자바스크립트에서는 함수, 배열, 정규식 등도 객체에서 파생된 특수한 타입일 뿐이다.

number

자바스크립트의 숫자 타입은 정수, 실수 구분 없이 숫자 타입 한 가지만 존재한다. 숫자 타입은 모든 데이터를 64비트의 부동 소수점 형태로 저장하기 때문에 진정한 정수 데이터는 존재하지 않는다.
자바스크립트의 숫자는 10진수 리터럴을 기본으로 사용한다. 이 외에도 2진수, 8진수 등 다양한 리터럴을 사용할 수 있다.

const num = 8;
// 2진수 리터럴 (0b로 시작)
const binaryNum = 0b1111;
// 8진수 리터럴 (0o로 시작)
const octNum = 0o17;

NaN

NaN은 Not a Number (숫자가 아님)을 표현하는 값이며, 읽기 전용 속성이다. 숫자로 변환할 수 없는 값을 숫자로 변환하려고 하거나 산술 연산의 결과가 숫자가 아니면 NaN 값이 반환된다.

const a = 3 - "a";
console.log(a); // NaN

NaN은 자기 자신과도 동등하지 않은 결과를 반환하기 때문에, ES2015에서 추가된 Number, isNaN() 메서드를 사용해 판별하는 것을 권장한다.

const a = 3 - "a";
console.log(a === a); // false
console.log(Number.isNaN(a)); // true

정수 범위

숫자의 최댓값은 Number.MAX_VALUE(약 1.798e+308)로 정의한다. 최솟값은 5e-324로 0에 가까운 가장 작은 양의 정수이며 Number.MIN_VALUE로 정의한다. 하지만 안전하게 표현할 수 있는 정수 값의 범위는 Number.MAX_VALUE보다 훨씬 작은 범위이다.
따라서 숫자의 범위는 Number.MAX_SAFE_INTEGER(2<sup>53</sup> - 1)Number.MIN_SAFE_INTEGER(-(2<sup>53</sup> -1)) 사이로 정의하는 것이 좋다. 이 범위를 넘어선 숫자의 연산은 부정확한 결과를 반환할 수 있어 주의해야 한다. 이 문제를 해결하기 위해 EX2020에서 새로 등장한 BigInt 타입을 사용하거나 큰 수에 대한 연산 처리를 하는 라이브러리를 사용해야 한다.

const safeMinNum = Number.MIN_SAFE_INTEGER;

console.log(safeMinNum); // -9007199254740991

// 범위를 벗어나는 연산을 하면 올바른 결과를 얻을 수 없다.
console.log(safeMinNum - 4); // -9007199254740996

소수점 연산

자바스크립트는 부동 소수점의 배정밀도를 이용해 근삿값으로 소수점 연산을 수행한다. 배정밀도는 높은 정밀도를 가지지만 정확한 값을 표현할 수 없어 아래와 같은 문제가 발생한다.

0.1 + 0.2 === 0.3; // 결과는 false!

정확한 값이 아닌 근삿값 연산이기 때문에 결과는 false이다. 실제로 0.1과 0.2의 덧셈 연산 결과도 0.3이 아닌 0.300000000000000004의 결과를 보인다. 이러한 자바스크립트의 숫자 연산 문제는 Number.EPSILON을 사용해 해결할 수 있다.

Number.EPSILON ?
ES2015부터 내장된 속성으로, 컴퓨터가 이해하는 가장 작은 숫자 단위이다.

const x = 0.2,
  y = 0.3,
  z = 0.1;
let equal = Math.abs(x - y + z) < Number.EPSILON;

정밀한 소수점 연산 또는 굉장히 큰 단위의 연산이 필요하지 않다면, 자바스크립트의 숫자 연산은 믿고 사용해도 된다.

string

string은 텍스트를 표현하는 타입이다. 자바스크립트의 문자열은 리터럴 형태로 주로 사용하며, 홑따옴표, 쌍따옴표, 백틱(``)을 이용해 표현한다.

const str1 = "hello";
const str3 = `hello`;

자바스크립트의 문자열은 16비트의 유니코드 문자 집합을 연속적으로 나열한 것이다. 문자열의 길이는 16비트 코드의 개수로, 대부분은 문제가 없지만 특수한 문자열들은 실제 표현되는 문자열의 길이와 다를 수 있다.

const str = 'b"';

console.log(str.length); // 2

이스케이프

문자열을 사용하다 보면 홑따옴표 또는 쌍따옴표를 텍스트 데이터로 표현해야 할 때가 있다. 이럴 때는 역슬래시(/) 문자로 이스케이프 처리해 사용한다.

이 외에도 줄바꿈 문자(\n)처럼 이스케이프 표현을 사용해 나타내는 특수 문자들이 있다.

  • \n : 줄바꿈 문자 (\u000A)
  • \t : 수평 탭 (\u0009)
  • \f : 폼 피드 (\u000C)
  • \r : 캐리지 리턴 (\u000D)
  • \v : 수직 탭 (\u000B)
  • \u : 네 개의 16진수 숫자로 표현한 유니코드 문자 (\u03c0)

템플릿 리터럴

코드를 작성하다 보면 정해진 포멧의 문자열에 조건에 따른 변수나 상수를 조합해야 하는 일이 많다.

const name = "javascript";
const sentence = "I love the" + name;

ES2015 이전에는 문자열을 병합해야만 변수나 상수를 문자열 안에 쓸 수 있었다. 하지만 이 방법은 문자열 안에 써야 하는 변수나 상수가 많다면 쓰기도 번거롭고 가독성이 떨어진다는 단점이 있다.
ESE2015의 템플릿 리터럴 문법이 등장하면서, 문자열 내의 변수나 상수를 간결하게 표현할 수 있게 되었다. 템플릿 리터럴은 홑따옴표, 쌍따옴표를 사용하지 않고 백틱을 사용한다. 템플릿 리터럴 안에서는 달러 기호와 중괄호(${})로 표현식을 감싸 문자열 안에 그 값을 삽입할 수 있다.

const name = "javascript";
const sentence = `I love the ${name}`;

덧셈 연산자를 사용하는 기존 코드보다 훨씬 간결하게 작성할 수 있으며, 템플릿 리터럴은 여러 줄의 문자열을 간결하게 표현할 수도 있다.

// 아래의 템플릿 리터럴의 평가 결과는 'Line 1\nLine 2' 문자열과 같다.
console.log(`Line 1
Line 2`);

boolean

불리언 타입은 true, false 두 가지 값만을 가지며, 참과 거짓을 표현할 때 사용한다. 명시적으로 불리언 값을 할당해 사용하기도 하지만 관계 연산의 결과값 형태로도 자주 사용한다. 참과 거짓을 판단하는 불리언 타입의 특성상 조건문이나 반복문의 조건 표현식 내에서 많이 사용한다.

if (a > 1) {
  b = 1;
} else {
  b = 2;
}

null과 undefined

null은 값이 없음을 나타내는 특별한 타입이다. 대다수 프로그래밍 언어에서 사용하는 것처럼 값이 없음을 나타내고 싶을 때 null 타입을 사용한다. 하지만 자바스크립트에는 값이 없음을 나타내는 또 다른 타입 undefined가 있다. 값이 할당되지 않는 변수나 반환 값이 없는 함수의 결과값은 자동으로 undefined 값이 할당된다.

let v;
function f() {}

console.log(v); // undefined
console.log(f()); // undefined

의도적으로 값이 없음을 나타내고 싶을 때 null을 사용하고, 값이 할당되지 않음을 나타내고 싶을 때 undefined를 할당한다. 하지만 대부분 값이 할당되지 않음을 나타내고 싶을 때는 없기 때문에 주로 null을 사용해 값이 없음을 나타낼 때가 많다. 명확한 규칙을 만들고 사용하면 큰 혼란이 없을 것이다.

Symbol

심볼(Symbol)은 ES2015에서 도입된 원시 타입이다. 데이터의 유일함을 나타낼 때 사용하며, 생성된 심볼은 다른 어떤 심볼과도 일치하지 않는다. 심볼은 다른 원시 타입과 달리 Symbol() 함수를 호출해 생성한다. 여기서 눈 여겨볼 점은 new 키워드를 붙이지 않고 심볼을 생성하는 것이다. 심볼은 객체가 아닌 원시 타입이기 때문에 new 키워드를 사용해 생성할 수 없다.

const sym1 = Symbol("key");
const sym2 = Symbol("key");

console.log(sym1 === sym2); // false

전역 심볼

전역 심볼을 생성해 매번 새로운 심볼을 생성하지 않고 기존 심볼을 검색해 사용하기도 한다. Symbol.for() 매서드를 사용하면 전역 심볼을 생성하며, 전역 심볼을 사용할 땐 다른 라이브러리들과 충돌을 피하도록 prefix를 사용해 구분하는 것이 좋다.

const sym1 = Symbol.for("myApp.key");

console.log(sym1 === Symbol.for("myApp.key")); // true

심볼의 활용

심볼은 객체나 클래스에서 유일한 프로퍼티를 만들 때 사용한다. 심볼을 사용해 프로퍼티를 만들면 유일함이 보장되어 프로퍼티 추가 시 충돌이 날 염려가 없다. 또한 외부에서 직접 해당 프로퍼티에 접근할 수 없어 의도치 않은 프로퍼티 변경을 막을 수 있다.

const user = {
  name: "javascript",
};
const id = Symbol("id");
user[id] = "firstId";

// 심볼은 유일한 값이라 외부에서 직접 프로퍼티에 접근할 수 없다.
console.log(user[Symbol("id")]); // undefined

심볼로 정의한 프로퍼티를 수정할 방법이 없는 것은 아니다. getOwnPropertySymbols() 메서드로 심볼 키 값을 얻어 와서 갱신할 수 있지만, 일반 프로퍼티보다는 변경에 대해 안전하다. 하지만 심볼 키 값을 통해 갱신하는 것 자체가 바람직한 사용 방법은 아니므로 지양하길 바란다.

심볼은 Well-Known Symbols 라는 것을 사용해 기존 객체 동작 알고리즘을 확장할 수 있다. 예를 들어 Symbol.iterator() 메서드를 사용해 iterator 객체를 정의하거나 Symbol.match(), Symbol.replace()와 같은 메서드를 사용해 객체를 대상으로 한 정규 표현식 동작을 정의할 수 있다.

BigInt

BigInt 타입은 ES2020에서 추가된 타입이다. BigInt는 정수 뒤에 n을 붙여 10진수 리터럴로 사용하거나 심볼과 유사하게 BigInt() 함수를 호출해 생성한다.

const safeMaxNum1 = 9007199254740991n;
const safeMaxNum2 = BigInt(9007199254740991);

안전한 정수의 연산

BigInt 타입은 기존의 숫자 타입이 -(253-1) ~ (253-1) 사이의 정수만 안정적으로 표현할 수 있기 때문에 이 범위보다 큰 정수를 안정적으로 표현하기 위해 등장했다.

const safeMinNum = Number.MIN_SAFE_INTEGER;

console.log(safeMinNum); // -9007199254740991

// 범위를 벗어나는 연산을 하면 올바른 결과를 얻을 수 없다.
console.log(safeMinNum - 4); // 9007199254740996

// BigInt 타입으로 변환해 연산하면 올바른 결과를 얻을 수 있다.
console.log(BigInt(safeMinNum) - 4n); // 9007199254740995n

기존의 숫자 연산은 안전한 숫자 범위를 벗어나 부정확한 결과를 반환하지만, BigInt 타입은 정확한 결과를 반환한다. 이처럼 BigInt 타입을 사용하면 큰 정수에 대해서도 안전하게 연산이 가능하다.

BigInt 연산의 주의점

BigInt 타입은 숫자 타입이나 Math 객체의 메서드를 함께 사용해 연산할 수 없다. 기존 숫자 타입과 연산하고 싶다면 반드시 명시적으로 타입을 변환한 후 사용해야 한다.

const bigNum = 1n;
const num = 2;
console.log(bigNum + BigInt(num)); // 3n

명시적으로 타입을 반환하지 않고 연산한다면 TypeError가 발생하니 주의해야 한다.

const bigNum = 1n;
const num = 2;
console.log(bigNum + num); // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

BigInt 타입은 연산 중 숫자 타입으로 변환되는 것을 방지하도록 TypeError를 발생시킨다. BigInt는 기존 숫자 타입과는 다르게 큰 정수(안전한 정수의 범위를 벗어나는 정수)의 연산을 하도록 나온 타입이라 숫자 타입과는 구분되어야 하기 때문이다.

객체

자바스크립트에서 원시 타입이 아닌 모든 값은 객체이다. 객체는 이름(키) : 값 형태로 여러 값을 포함하는 컨테이너이며, 컨테이너 내부의 값은 얼마든지 변경할 수 있다. 이름에 해당하는 프로퍼티 명은 숫자와 문자열, 심볼만 가능하며 값에 해당하는 프로퍼티의 값은 어떤 표현식이든 올 수 있다.

객체의 생성

객체를 생성하는 방법은 세 가지이다. Object() 생성자 함수를 이용하는 방법, 객체 리터럴({})을 사용하는 방법, 직접 정의한 생성자 함수를 이용하는 방법이다.

Object() 생성자 함수 사용

Object()는 객체를 생성하도록 자바스크립트에 내장된 생성자 함수이다.

const obj = new Object();

// 프로퍼티 생성
obj.id = "id";
obj.name = "name";

Object() 생성자 함수를 통해 obj라는 빈 객체를 생성한 뒤 프로퍼티를 추가했다. 하지만 이 방법은 뒤에서 설명할 객체 리터럴을 사용해 생성하는 것이 훨씬 간결하기 때문에 거의 사용되지 않는 편이다.
Object() 생성자 함수는 인자 타입에 따라 다른 타입의 랩퍼 객체를 생성하기도 한다. 하지만 랩퍼 객체 자체를 직접 사용할 일이 드물기 때문에 이렇게 사용할 일도 거의 없다.

랩퍼 객체는 원시 타입의 박싱 과정에서 사용된다. 문자열 원시 타입에서 toUpperCase()와 같은 내장된 메서드를 사용하도록 도와주는 객체라고 이해하면 된다.

const obj1 = new Object("obj");
console.log(obj1); // String 랩퍼 객체 생성

const obj2 = new Object(1);
console.log(obj2); // Number 랩퍼 객체 생성

객체 리터럴

중괄호({})를 사용해 객체를 생성하는 강력한 문법이다. 이름:값 형태로 프로퍼티 이름과 값을 중괄호 안에 정의해 객체를 생성한다. 만약 중괄호 안에 어떠한 프로퍼티도 정의하지 않는다면 빈 객체가 생성된다. Object() 생성자 함수가 아닌 객체 리터럴 방식을 사요하면 간결하게 객체를 생성할 수 있다.

const obj = {
  id: "id",
  name: "name",
};

객체 리터럴은 간단한 문법으로 객체 생성과 동시에 프로퍼티까지 추가할 수 있어 객체를 생성할 때 가장 많이 사용되는 방법이다.

생성자 함수

자바스크립트에서 생성자 함수를 이용하면 동일한 형태의 객체를 쉽게 생성해서 사용할 수 있다. 다른 프로그래밍 언어와는 달리 자바스크립트에서 생성자 함수는 형식이 정해져 있지 않다. 함수를 선언한 후 new 키워드를 사용해 호출하면 해당 함수는 생성자 함수로 동작한다.
하지만 모든 함수를 생성자 함수로 사용하진 않는다. 따라서 일반 함수와 생성자 함수를 구분하기 위해 생성자 함수 이름의 첫 글자를 대문자로 표기하는 것을 권장한다.

function Vehicle(type) {
  this.type = type;
}

const car = new Vehicle("Car"); // {type : 'Car'}

Vehicle() 생성자 함수를 new 키워드와 함께 호출해 새로운 객체를 생성했다. 그리고 이 함수를 이용해 동일한 형태의 객체를 필요할 때마다 생성한다. 자바스크립트에서는 위 코드처럼 특정한 형태의 객체를 생성하거나 상속받고 싶을 때 생성자 함수를 사용한다.

단, 화살표 함수는 this 바인딩을 하지 않아 생성자 함수로 사용할 수 없다.

객체의 프로퍼티 읽기

객체의 프로퍼티는 점 표기법 또는 대괄호 표기법으로 접근한다. 점 표기법을 사용하면 점을 찍고 접근하려는 프로퍼티 명을 작성해 사용한다. 만약 접근한 프로퍼티가 객체라면 하위 객체의 프로퍼티에 중첩해 접근한다.

obj.name;
obj.age;

// 아래처럼 중첩해 하위 객체의 프로퍼티에 접근할 수도 있다.
obj.child.name;

대괄호 표기법은 접근하려는 프로퍼티 명을 문자열 형태(정확히는 문자열로 변환되는 표현식)로 만들어 대괄호로 감싼 후 프로퍼티에 접근한다.

obj["name"];
obj["age"];

대부분은 사용하기 간결한 점 표기법을 많이 사용하지만, 대괄호 표기법을 사용해야만 프로퍼티에 접근할 수 있을 때가 있다. 변수를 사용해 프로퍼티에 접근하거나 접근하려는 프로퍼티 명에 연산자가 포함된 경우 반드시 대괄호 표기법을 사용해야 한다.

const prop = "name";
obj[prop];
obj["last-name"];

점 표기법대괄호 표기법 모두 객체에 존재하지 않는 프로퍼티에 접근할 경우 undefined 값을 반환한다.

const obj = { name: "name" };

console.log(obj.age); // undefined
console.log(obj["age"]); // undefined

동적 프로퍼티 생성과 갱신

자바스크립트의 객체는 생성된 후에도 동적으로 프로퍼티를 추가하거나 갱신할 수 있다. 점 표기법과 대괄호 표기법을 사용해 객체의 프로퍼티를 동적으로 변경할 수 있는데, 객체에 프로퍼티가 존재하지 않는다면 새로운 프로퍼티가 추가되고 있다면 기존 프로퍼티 값이 갱신된다.

const obj = { name: "name" };
// name이란 프로퍼티가 존재하므로 기존 프로퍼티 값이 갱신된다.
obj.name = "anonymous";
// age란 프로퍼티가 존재하지 않으므로 age라는 프로퍼티가 새로 추가된다.
obj["age"] = 30;

getter와 setter

앞서 살펴본 방법은 모두 객체의 프로퍼티에 직접 값을 설정하거나 접근하는 것이다. 하지만 어떤 프로퍼티에 접근할 때마다 동적인 계산이 필요하거나 프로퍼티 값이 변경될 때마다 별도의 처리 코드가 필요하다면, gettersetter 접근자 프로퍼티를 사용해 일반 프로퍼티처럼 사용할 수 있다. 접근자 프로퍼티의 값에 접근하면 getter 메서드가 호출되며 이 메서드의 반환값이 접근 표현식의 결과값이다. 만약 접근자 프로퍼티의 값을 변경하려고 하면 setter 메서드가 호출된다.

const obj = {
  myName: "javascript",
  set name(name) {
    if (name !== null) {
      this.myName = name;
    }
  },
  get name() {
    return this.myName;
  },
};

접근자 프로퍼티를 정의하는 가장 간단한 방법은 객체 리터럴 문법을 사용하는 것이다. 일반 프로퍼티와는 다르게 접근자 프로퍼티는 get/set 문법을 사용해 정의한다. getter setter 메서드를 보면 this 키워드를 사용한다. 이는 메서드 안에서 this 키워드가 객체 자신을 가리키기 때문에 this.myName으로 객체의 프로퍼티에 접근할 수 있는 것이다.
접근자 프로퍼티는 프로퍼티의 값을 갱신할 때 유효성을 검증하거나 조건에 따라 다른 값을 반환하는 작업들을 할 때 많이 사용한다.

const obj = {
  count: 0,
  // count가 null이 아니고 0보다 큰 경우에만 값을 갱신한다.
  set count(count) {
    if (count !== null && count > 0) {
      this.count = count;
    }
  },
};

Object.defineProperty()와 프로퍼티 속성

접근자 프로퍼티를 생성하는 또 다른 방법은 정적 메서드 Object.defineProperty()를 사용하는 것이다. 이 메서드는 객체에 직접 새로운 프로퍼티를 정의하거나 이미 존재하는 프로퍼티를 수정한 후 그 객체를 반환한다.

const obj = { myName: "javascript" };

Object.defineProperty(obj, "name", {
  set(name) {
    if (name !== null) {
      this.myName = name;
    }
  },
  get() {
    return this.myName;
  },
});

Object.defineProperty() 메서드는 첫 번째 인자로 대상이 되는 객체, 두 번째 인자로 추가 또는 갱신하려는 프로퍼티 명이나 심볼을 넘긴다. 마지막 인자로 프로퍼티 서술자를 정의한 객체를 넘긴다. gettersetter를 포함해 다음과 같은 프로퍼티 서술자들이 있다.

  • configurable : 프로퍼티의 삭제 및 서술자의 변경 가능 여부를 결정한다. 변경 및 삭제가 가능하다면 configurable 속성값은 true이며 디폴트 값은 false이다.
  • enumerable : 열거 시 프로퍼티의 노출 여부를 결정한다. 열거 시 프로퍼티가 노출된다면 enumerable 속성값은 true이며 디폴트 값은 false이다.
  • writable : 프로퍼티의 수정 가능 여부를 결정한다. 수정이 가능하다면 writable 속성값은 true이며 디폴트값은 false이다.
  • value : 프로퍼티의 값으로 디폴트값은 undefined이다.
  • get : getter 접근자 프로퍼티 메서드로 디폴트값은 undefined이다.
  • set : setter 접근자 프로퍼티 메서드로 디폴트값은 undefined이다.
Object.defineProperty(obj, "name", {
  // name 프로퍼티의 수정을 허용한다.
  writable: true,
  // name 프로퍼티의 값을 설정한다.
  value: "myName",
});

프로퍼티 서술자는 객체 프로퍼티들의 기본 동작들을 제어하거나 나아가 라이브러리를 만들 때 유용하게 사용할 수 있다.

배열

자바스크립트의 배열은 객체의 특별한 형태로 순서가 있는 데이터의 집합이다. 배열 안의 값은 원소라고 하며, 배열의 위치를 가리키는 인덱스로 각 원소에 접근한다. 인덱스는 0부터 시작하는 정수이며, 배열 값은 어떤 타입의 데이터든 사용할 수 있다. 즉 배열은 객체이지만 정수 타입인 인덱스를 프로퍼티로 갖는 특별한 데이터이다.

배열의 생성

배열은 Array() 생성자 함수나 배열 리터럴([])을 사용해 생성한다.

Array() 생성자 사용

객체와 마찬가지로 배열도 자바스크립트에 내장된 생성자 함수 Array()가 있으며, 이를 사용해 새로운 배열을 생성할 수 있다.

const arr = new Array(1, "2", true);

console.log(arr); // [1, '2', true]

Array() 생성자 함수는 새로운 배열을 생성하고 인자로 받은 값들을 배열 원소로 채워 넣어 초기화한다. 만약 생성자 함수의 인자가 1개이고 숫자 값이라면 해당 값을 배열의 length 프로퍼티에 할당해 새로운 배열을 생성한다.

자바스크립트 배열의 length 프로퍼티는 부호 없는 32비트 정수 데이터만 허용하기 때문에 232-1 이하의 정수만 허용한다.

const arr = new Array(3);

// 길이가 3인 빈 배열이 생성된다.
console.log(arr); // [empty, empty, empty]

하지만 Array() 생성자 함수 역시 배열 리터럴 문법이 더 간단하고 명확해 거의 사용되지 않는다.

배열 리터럴

배열 리터럴은 대괄호([])를 사용한다.

const arr = [1, "2", true];

배열 원소 타입으로 숫자, 문자열 그리고 불리언을 볼 수 있다. 이 타입들 뿐만 아니라 객체, 배열 등 모든 타입의 값을 원소로 사용할 수 있다. 이런 배열을 비균질적 배열이라고 한다. 하지만 이런 배열 데이터는 일관성 있게 처리하기 힘들고, 다른 개발자가 어떠한 데이터인지 파악하기도 어려우므로 자바스크립트 배열이 비균질적인 특성을 가진다는 것만 이해하고 실제 배열 데이터를 다룰 때는 가급적 통일된 타입의 데이터를 사용하는 것이 좋다.

원소 접근과 동적인 원소

배열 원소는 대괄호([]) 안에 인덱스값을 넣거 접근한다. 만약 배열 길이보다 큰 인덱스값을 넣어 접근한다면 값이 할당되지 않음을 표현하는 undefined 값을 반환한다. 정의되지 않은 프로퍼티명(배열의 길이보다 큰 인덱스)으로 객체에 접근한 것과 동일하다고 생각하면 쉽게 이해될 것이다.

const arr = ["f", "o", "o"];

console.log(arr[0]); // 'f'
console.log(arr[1]); // 'o'
console.log(arr[2]); // 'o'
console.log(arr[3]); // undefined

배열도 객체이기 때문에 동적으로 배열 원소를 추가할 수 있다. 특히 자바스크립트의 배열은 순차적으로 값을 넣을 필요 없이 아무 인덱스 위치에나 값을 동적으로 추가할 수 있다. 이때 값을 추가한 인덱스의 위치에 따라 length 프로퍼티도 갱신된다. 자바스크립트 배열의 length 프로퍼티는 배열의 최대 인덱스보다 항상 크기 때문이다.

const arr = [];

arr[0] = 1;
arr[2] = 2;

console.log(arr); // [1, empty, 2]
console.log(arr.length); // 3

인덱스 1을 건너뛰고 2에 원소를 추가했다. 이 때 배열의 최대 인덱스는 2이고 그 인덱스를 기준으로 length 프로퍼티가 갱신된다.
배열 역시 일반 객체처럼 이름:값 형태의 데이터도 추가할 수 있다. 다만, 이 때 배열의 length 프로퍼티에는 변화가 없으니 주의해야 한다.

const arr = [];

arr.foo = "foo";

console.log(arr.length); // 0

하지만 배열에 이름:값 형태의 데이터를 추가하는 것은 권장하지 않는다. length 프로퍼티는 갱신되지 않고 프로퍼티만 추가되기 때문이다. 또한 이러한 프로퍼티는 일반 배열 원소처럼 다룰 수 없어 배열을 순회하거나 정렬하는 내장 메서드와 함께 사용할 수 없다. 꼭 이렇게 사용해야 한다면 일반 객체를 사용하고 배열에선 정수 형태의 인덱스만 프로퍼티로 사용하는 것이 혼란을 막는 방법이다.

희소 배열

const arr = [];

arr[0] = 1;
arr[2] = 2;

console.log(arr); // [1, empty, 2]
console.log(arr.length); // 3

인덱스 0과 인덱스 2에만 원소를 할당했고, 인덱스 1에는 어떠한 값고 할당되지 않아 빈 상태로 표시된다. 이 빈 상태는 명시적으로 배열의 원소에 undefined를 할당한 것과 다르다. 이처럼 배열 원소가 연속적이지 않고 중간에 빈 배열을 희소 배열이라고 한다. 희소 배열의 빈 원소는 forEach(), map(), filter()와 같은 배열의 내장 메서드에서 무시된다. 반면 find()findIndex() 메서드는 빈 원소를 무시하지 않고 모두 탐색한다. 이처럼 희소 배열은 어떠한 메서드를 사용하는가에 따라 일관적이지 않은 동작을 수행하며, 코드 가독성이나 데이터 구조 파악에도 좋지 않으니 특수한 경우가 아니라면 사용을 지양해야 한다.

length 프로퍼티

자바스크립트 배열의 length 프로퍼티는 배열의 길이를 반환한다. length 프로퍼티는 부호 없는 32비트 정수 데이터만 사용하기 때문에 232-1 이하의 정수만 설정할 수 있다. 또한 항상 배열의 최대 인덱스보다 크다.

const arr = [];
console.log(arr.length); // 0

// push 메서드는 배열의 맨 끝 인덱스에 새로운 원소를 추가한다.
arr.push(1);

// length 값이 1로 갱신되었음을 알 수 있다.
console.log(arr.length); // 1

length 프로퍼티를 직접 수정해 배열의 길이를 늘리거나 줄일 수도 있다. 이 때 수정한 배열 길이에 맞게 배열의 원소도 추가(빈 원소)되거나 삭제된다.

const arr = [1, 2, 3, 4];
arr.length = 2;
console.log(arr); // [1, 2]

배열 조작

배열은 원소를 조작할 수 있는 내장 메서드를 갖는다. 배열 조작 메서드는 배열의 원본 데이터를 직접 수정하는 메서드기존 배열 데이터를 기반으로 조작해 새로운 배열을 생성하는 메서드 두 가지로 나뉜다.

원본 배열 데이터 수정 메서드

  • shift() : 배열의 첫 번째 원소를 삭제하고, 결과값으로 삭제된 원소를 반환한다.
const arr = [1, 2];

console.log(arr.shift()); // 1
console.log(arr); // [2]
  • unshift() : 배열의 첫 번째 인덱스에 원소를 추가하고, 결과값으로 배열의 새로운 길이를 반환한다.
const arr = [1, 2];

console.log(arr.unshift(-1, 0)); // 4
console.log(arr); // [-1, 0, 1, 2]
  • push() : 배열의 마지막 인덱스에 하나 이상의 원소를 추가하고, 결과값으로 배열의 새로운 길이를 반환한다.
const arr = [1];

console.log(arr.push(2, 3)); // 3
console.log(arr); // [1, 2, 3]
  • pop() : 배열에서 마지막 인덱스에 해당하는 원소를 삭제하고 그 값을 결과값으로 반환한다.
const arr = [1, 2];

console.log(arr.pop()); // 2
console.log(arr); // [1]
  • splice() : 배열의 원소를 추가하거나 교체 또는 삭제해 배열 데이터를 변경한다. 결과값으로 삭제된 원소의 배열을 반환한다.
const arr = [1, 2, 3];

console.log(arr.splice(1, 2)); // [2, 3]
console.log(arr); // [1]
  • sort() : 배열의 원소를 인자로 넘긴 비교 함수를 사용해 정렬한다. 비교 함수를 생략할 때 각 문자의 유니코드 포인트 값에 따라 정렬된다. 단, 숫자도 문자로 변환하여 정렬한다는 것을 유의해야 한다. 문자 정렬 방식이 아닌 별도의 비교 방식을 정의하고 싶다면 비교 함수를 정의해 전달하여 사용한다.
const arr = [3, 2, 4, 21, 55];
arr.sort();

console.log(arr); // [2, 21, 3, 4, 55]

새로운 배열 생성 메서드

  • concat() : 인자로 넘긴 배열 또는 값들을 기존 배열에 합쳐 새로운 배열을 생성해 반환한다.
const arr = [1, 2, 3];
const newArr = arr.concat([4, 5]);

console.log(arr); // [1, 2, 3]
console.log(newArr); // [1, 2, 3, 4, 5]
  • slice() : 배열에서 특정 범위의 원소를 복사해 새로운 배열을 생성해 반환한다. 단, 얕은 복사를 수행하기 때문에 배열의 원소가 객체이면 참조가 유지된다.

얕은 복사 ?
> 얕은 복사(Shallow Copy)는 대상 객체를 새로 생성하지만 내부에 중첩된 객체는 새로 생성하지 않고 동일한 객체를 참조한다. 반대로 중첩된 객체까지 모두 새로 생성하는 복사를 깊은 복사(Deep Copy)라고 한다.

const obj = {};
const arr = [1, obj, 3];
const newArr = arr.slice(1, 2);

// slice() 메서드는 얕은 복사를 하기 때문에 배열 내에 중첩된 객체의 참조가 유지된다.
console.log(newArr[0] === obj); // true

배열을 복사하는 또 다른 방법은 펼침 연산자(Spread Operator)를 사용하는 것이다. slice() 메서드와 마찬가지로 얕은 복사를 수행한다.

const arr = [1, 2, 3];
const newArr = [...arr];

console.log(newArr); // [1, 2, 3]

map(), forEach(), filter()

이 메서드들은 익명 함수를 사용해 간결하게 배열의 원소를 다룰 수 있다.

익명 함수 ?
익명 함수는 말 그대로 이름이 없는 함수이다. 주로 콜백 함수 또는 함수 표현식을 정의할 때 사용한다.

  • map() : 배열의 모든 원소를 인자로 받은 함수를 실행해 특정한 형식으로 변경한다. 변경한 원소들로 새로운 배열을 생성해 반환한다.
const arr = [1, 2, 3];
const newArr = arr.map((x) => x + 1);

console.log(newArr); // [2, 3, 4]
  • forEach() : 인자로 받은 함수를 배열의 모든 원소를 대상으로 실행한다.
const arr = [1, 2, 3];
const newArr = arr.forEach((x) => console.log(x));
  • filter() : 인자로 받은 함수의 테스트를 통과하는 원소들로 새로운 배열을 생성해 반환한다.
const arr = [1, 2, 3];
const newArr = arr.filter((x) => x !== 1);

console.log(newArr); // [2, 3]

유사 배열 객체

자바스크립트에서는 일반 객체를 배열처럼 사용할 수 있다. 이러한 객체를 유사 배열 객체라고 하며, 유사 배열 객체는 length 프로퍼티로 양의 정수 값을 가진 객체여야만 한다.

const arr = {
  0: "Hi!",
  1: "My",
  2: "name is",
  3: "javascript",
  length: 4,
};

유사 배열 객체의 대표적인 예는 자바스크립트 함수의 arguments 객체이다.

arguments 객체는 함수에 전달한 인자를 유사 배열 객체로 만든 데이터이다.

function foo(a, b, c) {
  console.log(arguments[0], arguments[1], arguments[2]); // 'a', 'b', 'c'
  console.log(arguments.length); // 3
}

foo("a", "b", "c");

arguments 객체는 마치 배열처럼 인덱스로 프로퍼티에 접근할 수 있으며, length 프로퍼티를 가진다. 하지만 배열이 아닌 유사 배열 객체이기 때문에 배열의 내장 메서드를 사용할 수 없다.

function foo(a, b, c) {
  // 아래 코드는 typeError가 발생한다.
  arguments.forEach((arg) => {
    console.log(arg);
  });
}

foo("a", "b", "c");

이럴 때 배열의 내장 메서드를 call() 또는 apply() 함수와 결합해 사용한다.

function foo(a, b, c) {
  // a, b, c가 순차적으로 출력되는 것을 볼 수 있다.
  Array.prototype.forEach.call(arguments, (arg) => {
    console.log(arg);
  });
}

foo("a", "b", "c");

유사 배열 객체는 DOM을 다루다 보면 자주 만날 수 있는 형태의 데이터이므로 프론트엔드 개발을 하기 위해서 제대로 이해하고 사용할 수 있어야 한다.

랩퍼(Wrapper) 객체

const str = "javascript";

console.log(str.length); // 10

str 변수는 문자열 원시타입인데도 마치 객체처럼 length 프로퍼티에 접근한다. 자바스크립트는 문자열의 프로퍼티에 접근할 때 내부적으로 문자열 값을 가지고 임시 객체로 변환한다. 그리고 프로퍼티 접근이 종료되면 생성된 객체는 메모리에서 제거된다. 자바스크립트에서 이런 과정을 박싱(Boxing)이라고 부른다. 숫자와 불리언 타입 역시 프로퍼티에 접근할 때 동일한 과정을 실행한다.
박싱 과정에서 생성되는 임시 객체를 랩퍼 객체라고 한다. 랩퍼 객체는 문자열, 숫자, 불리언 타입만 존재하며, 각각에 대응하는 네이티브 생성자로 String(), Number(), Boolean()이 있다. 랩퍼 객체는 원시 타입과는 다르게 객체이다.

const str = new String('javascript')

console.log(type of str) // object

하지만 자바스크립트 엔진 내부적으로 박싱 과정을 최적화하기 때문에 예제 코드처럼 굳이 랩퍼 객체를 생성해 사용할 일은 거의 없다.

언박싱(Unboxing)

언박싱은 박싱과는 반대로 랩퍼 객체를 원시 타입으로 변환한다. 언박싱은 명시적으로 valueOf() 메서드를 호출해 수행할 수 있다.

const num = new Number(11);

console.log(num.valueOf()); // 11

하지만 언박싱은 valueOf() 메서드를 호출해 수행할 때보다는 값을 다른 타입으로 변경하는 타입 변환을 할 때 수행되는 경우가 많다.

구문과 연산자

프로그래밍 언어에서 구문(Syntax)은 애플리케이션 또는 프로그램을 만들기 위해 언어가 작동하는 규칙을 정의하며, 정형화된 문법에 따라 작성해야 한다. 즉 올바른 구문을 작성하려면 언어의 문법과 구성 요소에 대해 명확하게 이해해야 한다.

표현식 (Expression)

표현식은 값으로 평가되는 구문이다. 예를 들어 임의의 숫자나 문자열은 모두 표현식이다.

값을 반환한다는 표현을 자바스크립트에서는 보통 값을 평가한다고 표현한다.

// 아래 문자열은 결과값으로 'javascript'를 반환하는 표현식이다.
"javascript";

변수에 값을 할당하는 것도 표현식이다.

var a;
a = 1;

변수 a에 숫자 1을 할당하는 것과 같은 구문을 할당 표현식이라고 하며, 할당된 값이 결과값으로 평가된다. 예제 코드에서 할당된 값이 1이기 때문에 할당 표현식은 1이라는 값으로 평가된다.
표현식의 결과는 항상 값이 되므로 이 결과를 다른 표현식과 결합해 다른 결과값을 얻을 수 있다. 이렇게 여러 표현식을 결합하는 것을 복합 표현식이라고 한다. 복합 표현식을 작성하는 가장 간단한 방법은 연산자를 사용하는 것이다.

var x = 1;
var y = 2;

x + y;

표현식 x와 표현식 y를 덧셈 연산자로 결합해 새로운 값으로 평가한다. 즉 x와 y 값의 덧셈 연산 결과가 복합 표현식의 결과값이다. 또한 x+y 복합 표현식을 할당 표현식과 결합해 결합해 사용(z = x + y)하기도 한다. 다시 이야기하지만, 표현식이 값으로 평가되기 때문에 이러한 동작이 가능한 것이다.

문 (Statement)

자바스크립트의 은 일종의 지시를 내리는 것이다. 예를 들어 A라는 값과 B라는 값을 더해라처럼 지시를 내리는 것을 자바스크립트에서는 문이라고 한다. 표현식이 값으로 평가되는 결과를 가져온다면, 은 표현식 또는 다른 문을 조합해 동작을 수행하는 지시를 내린다. 문 또한 완료 값이라는 결과값이 있지만, 표현식처럼 다른 변수가 객체의 프로퍼티에 할당할 수 없다.

let a;
// 아래의 조건문은 결과값으로 '1'을 가지지만, 변수에 할당할 수 없다.
const b = if (true) {
  a='1'
} // Uncaught SyntaxError: Unexpected token 'if'

문을 만드는 간단한 방법은 부수 효과(Side-Effect)가 있는 표현식을 실행하는 것이다. 부수 효과가 있는 표현식은 독립적인 문으로서 사용할 수 있다. 앞서 표현식에서 봤던 할당 표현식이 대표적인 예시이다. 할당 표현식은 변수에 다른 값을 할당해 변수값을 변화시킨다. 즉 변수값의 변화라는 부수 효과가 발생한다. 이렇게 문으로도 사용할 수 있는 표현식을 표현문이라고 한다. 자바스크립트에는 표현문을 포함해 다음과 같은 문들이 있다.

표현문

부수 효과가 있는 표현식으로 할당 표현식 또는 함수 호출과 같은 표현식이다.

function doSomething() {
  // ...
}

// 함수의 호출도 부수 효과를 발생시키기 때문에 표현문이다.
doSomething();

선언문

변수 또는 함수를 정의한다.

var a = 1;

조건문

조건부에 해당하는 표현식의 값에 다라 문을 실행하거나 건너뛴다.

// 표현식 a의 평가 결과가 true일 때만 조건문 안에 있는 문을 실행한다.
if (a) {
  console.log("실행 완료");
}

반복문

조건 표현식의 평가 결과가 true라면 반복문 내의 코드를 실행한다. 이 과정을 조건 표현식의 결과가 false가 나오지 않는 한 계속 반복 실행한다.

// 표현식 a의 평가 결과가 true라면 반복문 안에 있는 문을 실행한다.
while (a) {
  console.log("반복 수행");
}

점프문

특정 위치로 건너뛰어 문을 실행한다. break문, continue문, return문 등이 있다.

// return 문을 만나면 doSomething() 함수가 호출된 부분으로 건너 뛰고 호출부에 값을 전달한다.
function doSomething() {
  return "foo";
}

console.log(doSomething()); // 'foo'

연산자 (Operator)

표현식과 표현식을 결합해 복합 표현식을 만들려면 연산자를 사용해야 한다. 표현식이 값으로 평가된다면 연산자는 값을 만들어내는 방법이다. 덧셈을 하고 싶다면 + 산술 연산자를 사용하여 표현식의 값들을 더한 새로운 값을 만들어낼 수 있다.

const a = 1 + 2;

산술 연산자

산술 연산자는 말 그대로 숫자 연산에 필요한 연산자를 의미한다.

연산자들을 사용한 대부분의 표현식들은 부수 효과가 없다. a+3과 같은 덧셈 연산을 해도 a의 값을 변경하지는 않으며 단지 덧셈 연사의 값이 평가되어 반환될 뿐이다. 하지만 전치, 후치 증감 연산자는 부수 효과를 가진 연산자이다.

let a = 1;
let b = a++;

console.log(a, b); // 2, 1

let c = ++a;

console.log(a, c); // 3, 3
  • 후치 증가 연산자(a++) : a를 평가해 값을 반환한 후 a의 값을 1만큼 증가시켜 변경한다. 증가된 값을 할당하는 것이 아니라, 값이 먼저 평가되기 때문에 증가된 값이 아닌 기존 값이 할당된다. 따라서 예제 코드의 b에 할당되는 값은 1이다.

  • 전치 증가 연산자(++a) : 전치 증가 연산자는 후치 증가 연산자와 반대로 동작한다. 값을 먼저 1만큼 증가시킨 후에 평가된 값을 반환한다. ++a는 기존의 a값(2)을 1만큼 증가시킨 3을 반환하며, 최종적으로 c에 이 값(3)이 할당된다.

전치, 후치 감소 연산자는 증가 연산자와 동일한 단계로 동작하되, 1을 증가시키는 것이 아닌 감소시키는 부수 효과를 발생시킨다.

let num1 = "1";
let num2 = 2;

// 단항 + 연산자를 사용해 문자 '1'을 숫자 1로 변환한다.
const a = +num1;

// 단항 - 연산자를 사용해 숫자 2의 부호를 변경한다.
const b = -num2;
  • 단항 + 연산자 : 단항 + 연산자는 피연산자가 숫자로 변환할 수 없는 문자열일 때 결과값으로 NaN을 반환한다.
a = x + y - z;
// 위의 할당문은 아래와 동일하다.
a =
  x +
  y -
  z -
  +a -
  // 위의 단항 연산자는 아래처럼 우측부터 평가를 진행한다.
  +a;
  • 산술 연산자의 결합 방향 : 연산자의 결합 방향에 따라 연산들 간의 수행 순서가 결정된다. 결합 방향이 좌측에서 우측이라면 연산은 좌측에서 우측으로 일어나며, 반대이면 연산은 우측에서 좌측으로 일어난다.

논리 연산자

논리 연산자 &&, ||, !는 보통 참과 거짓을 나타내는 불리언 값을 이용해 연산을 수행하며, 이 경우 결과값도 불리언 값이다. 하지만 자바스크립트에서는 불리언 값이 아닌 값도 논리 연산자의 피연산자가 될 수 있다.
만약 논리 연산자의 피연산자로 불리언이 아닌 값이 온다면, truthy 값과 falsy 값이라는 개념을 사용해 피연산자를 평가한다.

  • truthy 값과 falsy 값 : truthy값은 참과 같은 값이다. 이 값들은 불리언 타입은 아니지만, 논리 연산자와 함께 사용하면 참으로 평가된다. 이와 반대로 falsy값은 거짓과 같은 값으로 논리 연산자와 함께 사용하면 거짓으로 평가된다. 자바스크립트의 모든 값은 truthy 값과 falsy 두 가지로 나뉘며, falsy 값에는 false, null, undefined, NaN, '', 0, 0n이 있다. 이 외엔 모두 truthy 값이다.

    • 빈 배열, 빈 객체 : 배열이나 객체는 항상 참으로 평가된다. 만약 빈 배열이 거짓으로 평가되길 원한다면 length 프로퍼티를 사용한다.
    • 공백 또는 줄 바꿈 문자열 : 공백과 줄 바꿈을 표현하는 문자열 값이 있으므로, 참으로 평가된다.
    • 문자열 ‘false’ : const a = 'false'라는 문자열 값이 있어 참으로 평가된다.
  • &&, ||, ! 연산자

    • AND 연산자(&&) : 피연산자가 모두 참으로 평가될 때 참으로 평가된다. 피연산자 x는 truthy 값이므로 참으로 평가되지만, 우측 비교 연사의 결과값이 거짓으로 평가되어 결과값으로 false를 반환한다.
    const x = 1;
    const y = 2;
    const z = 3;
    
    x && y > z; // false
    
    • OR 연산자(||) : 피연산자가 모두 거짓으로 평가될 때만 거짓으로 평가되며, 피연산자 중 하나라도 참으로 평가되는 값이 있다면 참으로 평가된다. x가 truthy 값이기 때문에 결과값으로 true를 반환한다.
    const x = 1;
    const y = 2;
    const z = 3;
    
    x || y > z; // true
    
    • NOT 연산자(!) : 단항 연산자로 피연산자의 불리언 값을 반대로 반전시킨다. NOT 연산자는 !! 처럼 연속해서 사용하기도 하는데, 이런 연산을 부정을 두 번 한다고 해서 이중 부정 연산자라고 부른다. 주로 기존 값을 불리언 타입으로 변환할 때 사용한다.
    const x = "";
    const y = !!x;
    
    console.log(y); // false
    
  • 단락 평가(Short Circuit) : AND 연산자와 OR 연산자는 단락 평가라는 특징을 갖는다. AND 연산자의 경우 앞의 평가 결과가 거짓이라면 뒤의 평가 결과와는 상관없이 false를 반환한다. 이런 경우 뒤의 표현식을 평가할 필요가 없기 때문에 평가하지 않는다. OR 연산자의 경우 앞의 평가 결과가 참이라면 뒤의 평가 결과와 상관없이 true를 반환한다. 마찬가지로 뒤의 표현식은 평가하지 않는다. 이러한 방식을 단락 평가라고 한다.
    예제코드의 마지막 줄을 보면 좌측 비교 연산의 결과가 참으로 평가될 때만 doSomething() 함수를 호출한다. 만약 반대로 좌측의 결과가 거짓으로 평가된다면 doSomething() 함수는 실행되지 않는다.

if (x > y) {
  doSomething();
}

// 아래 코드는 위의 코드와 동일한 동작을 한다.
x > y && doSomething();

관계형 연산자

코드를 작성하다 보면 두 피연산자의 관계를 검증하고 싶을 때가 있다. 두 값이 같은지 한 값이 다른 값보다 큰지 검사해 관계가 성립하는지 알아보는 것이다. 이럴 때 사용하는 연산을 관계형 연산이라고 한다. 관계형 연산의 결과는 항상 불리언 값이며, 주로 조건문이나 반복문 내의 조건을 정의할 때 사용된다.

  • 비교 연산자 : 비교연산자는 수학에서 익숙하게 접했던 연산자이다. 두 피연산자 값의 상대적인 순서를 판단하며 숫자(Number 또는 BigInt)와 문자열의 순서만 비교할 수 있다.
    자바스크립트의 문자열은 16비트 유니코드 값의 집합이므로 문자열 비교는 이 코드의 순서를 비교해 연산을 수행한다. 나라별 언어마다 유니코드의 인코딩 순서가 다르고, 대소 문자를 구분하기 때문에 문자열을 비교할 때 이 부분을 유의해야 한다.

ASCII 대문자는 ASCII 소문자보다 작다.

console.log(1 < 2); // true
console.log("ab" <= "b"); // true
  • 동등 연산자 : 동등 연산자는 값의 동등함을 판단하는 연산자이다. 자바스크립트에서는 일반적인 동등 연산자(==)엄격한 동등 연산자(===) 두 가지로 동등함을 판단할 수 있다.
    일반적인 동등 연산자는 객체의 참조 값이 동일하거나 피연산자들의 타입이 달라도 같은 값을 반환할 수 있다면 동등하다고 판단한다.
    숫자 1과 불리언 값 true가 동등하다는 결과가 나오는 이유는 불리언 값 true가 숫자 1로 변환되어 같은 값을 반환하기 때문이다. 이 동작 원리를 암시적 강제 변환이라고 한다. 암시적 강제 변환은 연산 중에 내부적으로 타입을 변환해 연산을 수행하는 것을 의미한다. 정리하자면 일반적인 동등 연산자는 암시적 강제 변환을 통해 값의 타입을 변환해 비교하는 것이다.
    엄격한 동등 연산자는 암시적 강제 변환을 허용하지 않는다. 일반적인 동등 연산자와 달리 타입 변환이 발생하지 않기 때문에 타입이 다르다면 무조건 다른 값으로 판단한다. 피연산자들의 타입과 값이 모두 일치해야 동등하다고 판단하기 때문에 ‘엄격한’ 이란 표현을 붙여서 사용하며 일치 연산자라고도 한다.
console.log(1 == true); // true

console.log(1 === true); // false
console.log(1 === 1); // true

in, instanceof 연산자

in 연산자는 객체에 특정 프로퍼티가 있는지 확인하는 연산자이며, 우측에는 반드시 객체 타입이 와야 한다. 좌측 평가값이 우측 객체의 프로퍼티라면 참으로 판단하며, 아니라면 거짓으로 판단한다.

const obj = { a: 1, b: 2 };
console.log("a" in obj); // true
console.log("c" in obj); // false

instanceof 연산자는 좌측 피연산자가 우측 피연산자의 인스턴스인지 판단하는 연산자이다. 우측에는 반드시 생성자 함수 또는 클래스(정확히는 호출 가능한 객체)가 와야 한다. 좌측 평가값이 우측 클래스의 인스턴스라면 참으로 판단하고 그렇지 않다면 거짓으로 판단한다.

const arr = [1, 2];

console.log(arr instanceof Array); // true
console.log(arr instanceof Date); // false

instanceof 연산자는 내부적으로 프로토타입 체인이라는 메커니즘으로 인스턴스인지 판단한다.

기타 연산자

  • 비트 연산자 : 비트 연산자는 숫자의 비트를 조작하는 연산자이며, 이진 연산을 수행하지만 결과값은 자바스크립트 숫자 값을 반환한다. 비트 연산자의 피연산자는 32비트 정수 표현법을 따르므로 연산을 수행할 때 피연산자를 32비트 정수로 변환해 수행한다.
    왼쪽 시프트는 말 그대로 비트를 왼쪽으로 옮기는 것이며, 왼쪽으로 초과되는 비트들은 버려진다. 왼쪽 시프트의 연산 결과는 이진수 101000이며, 이를 10진수로 변환하면 40이다. 즉 왼쪽 시프트 연산은 2를 곱하는 효과가 있다.
    오른쪽 시프트는 반대로 비트를 오른쪽으로 옮기고 오른쪽으로 초과되는 비트들은 버려진다. 오른쪽 시프트 연산의 결과는 이진수 1010이며, 이를 10진수로 변환하면 10이다. 왼쪽 시프트 연산과는 다르게 2로 나눈 효과가 있다.
const a = 20; // 10100(2진수) => 20(10진수)

console.log(a << 1); // 101000(2진수) => 40(10진수)
console.log(a >> 1); // 1010(2진수) => 10(10진수)
  • 조건 연산자 : 조건 연산자는 자바스크립트에서 유일하게 피연산자를 세 개 갖는 3항 연산자이다. 조건 연산자는 ‘if else’ 조건문의 축약형이다.
    물음표 앞에 있는 첫 번째 피연산자(a > 1)가 참으로 판단되면 이 표현식의 값은 두 번째 피연산자가 (a)로 평가되며, 거짓으로 판단된다면 세 번째 피연산자(2)로 평가된다.
if (a > 1) {
  b = a;
} else {
  b = 2;
}

// 아래 조건 연산자를 이용한 표현식은 위의 조건문과 동등하다.
b = a > 1 ? a : 2;
  • 할당 연산자 : 할당 연산자는 변수에 값을 할당할 때 사용하는 연산자이다. 우측 피연산자의 값을 좌측 피연산자의 값에 할당하며, 좌측 피연산자의 값은 반드시 변수나 객체의 프로퍼티 또는 배열의 원소처럼 값을 할당할 수 있는 요소(이러한 값을 l-value(좌변값)라고 부른다.) 중 하나여야 한다.
    할당 연산자의 결합 방향은 우측에서 좌측이다. 여러 개의 할당 연산이 발생하면 우측부터 연산을 시작해 좌측으로 평가된다. 자바스크립트에서는 연산과 할당을 동시에 수행하는 할당 연산자도 있다.
let a, b;
a = b = 2;
  • 쉼표 연산자 : 두 개의 피연산자를 가지며 피연산자는 어떤 타입이든 올 수 있다. 좌측부터 우측으로 피연산자를 평가하며, 우측 피연산자의 평가값을 최종 결과로 반환한다. for 반복문의 루프 변수를 갱신할 때 사용할 수 있다.
// 루프 변수 i, j를 한꺼번에 갱신할 때 쉼표 연산자를 사용한다.
for (let i = 0, j = 10; i < j; i++, j--) {
  // ...
}
  • typeof 연산자 : typeof 연산자는 피연산자의 타입을 문자열로 반환하는 단항 연산자이다.
    typeof null의 결과가 null이 아닌 object이다. 초창기 자바스크립트 설계의 오류로 생긴 버그이며, null 타입인지 확인하고 싶으면 엄격한 동등 연산자를 사용해 확인해야 한다. 또한 typeof는 함수 외의 객체들은 모두 ‘object’ 문자열을 반환해 정확하게 구분하지 못한다. 만약 객체의 클래스를 구분하고 싶다면 instanceof 연산자를 사용해야 한다.
const vehicle = new Vehicle();

console.log(vehicle instanceof Vehicle); // true
console.log(typeof vehicle); // 'object'
  • 연산자 우선순위
const a = 1 + 2 * 5; // 11

덧셈과 곱셈 연산은 좌측에서 우측으로 평가된. 이 규칙대로라면 1+2의 결과인 3과 5를 곱해 최종적으로 15가 되어야 하지만, 결과는 11이다. 우측의 곱셈 연산을 먼저 수행한 후 그 결과값으로 덧셈 연산을 하기 때문이다. 이처럼 연산자가 여러 개일 때 어떤 순서로 처리할지 정의한 규칙을 연산자 우선순위라고 한다.

const x = true;
const y = false;
const z = false;

console.log(x || (y && z)); // true

좌측부터 논리 OR 연산을 실행한 후 AND 연산을 실행할 것 같지만, 이 표현식은 우측의 AND 연산을 먼저 실행하고 좌측 OR 연산을 실행한다. 연산자 우선순위 규칙에 따르면 논리 AND 연산자OR 연산자보다 우선순위가 높기 때문이다.

let x = 2,
  y;

(y = x++), x;

console.log(y); // 2

2가 할당된 이유는 쉼표 연산자는 할당 연산자보다 우선순위가 낮기 때문이다. 예제 코드의 표현식은 (y = x++), x와 동등한 표현식이다. 할당 표현식이 먼저 평가되기 때문에 2가 할당된다.
연산자 우선순위를 변경하고 싶은 경우 괄호를 사용해 연산 순서를 원하는대로 변경할 수 있다.

let x = 2,
  y;
// 괄호를 사용해 쉼표 연산이 먼저 수행되도록 변경했다.
y = (x++, x);

console.log(y); // 3

About Hyerin Jeon

안녕하세요, 전혜린 (Hailey) 입니다! 주니어 프론트엔드 개발자이며, 리액트를 사용하고 있습니다 :D

-->
Useful Links