원시 타입과 참조 타입

이효범·2022년 4월 29일
0
post-thumbnail

자바스크립트에 있는 두 종류의 타입, 원시 타입과 참조 타입에 대해 다룬다.
두 종류가 서로 어떻게 다른지 알아보면서 자바스크립트를 전체적으로 이해하기 위해 두 타입의 차이를 아는 것이 얼마나 중요한지 설명한다.

자바스크립트는 전통적인 객체지향 언어를 배운 사람도 쉽게 배울 수 있도록 언어의 중심을 객체에 두고 있다. 자바스크립트에서 데이터 대부분은 객체이거나 객체를 통해 접근하는 값이다.
자바스크립트는 함수조차 객체로 표현하며 덕분에 자바스크립트의 함수는 일급 함수이다.

객체를 다루고 이해할 수 있어야 자바스크립트를 전체적으로 이해할 수 있다. 자바스크립트에서 객체는 언제든 만들 수 있고 객체의 프로퍼티 또한 원한다면 언제든 추가하거나 제거할 수 있다. 자바스크립트의 객체는 매우 유연하여 다른 언어에서는 쉽게 사용할 수 없었던 독특하고 흥미로운 패턴도 만들 수 있다.

이 글에서는 자바스크립트에 있는 두 종류의 타입, 즉 원시 타입과 참조 타입에 대해 알아보고 다룰 것이다. 두 타입 모두 객체를 통해 접근하지만 서로 다르게 동작하므로 차이점을 익혀두는 것이 좋다.

타입이란?

자바스크립트에 클래스라는 개념은 ES6 이전까지 존재하지 않았으며 이를 대체할 타입이라는 개념이 존재했다. 타입은 크게 두 종류로 구분한다.
하나는 단순한 데이터를 저장하는 원시 타입이고 다른 하나는 객체로서 저장되는 참조 타입이다. 참조 타입은 사실 메모리상의 주소를 가리킨다.

자바스크립트에서는 일관성을 유지하기 위해 원시 타입도 참조 타입처럼 다룰 수 있도록 되어 있는데 이 때문에 이해하기 어려워진 면이 있다.

다른 프로그래밍 언어는, 원시 타입은 스택에 저장하고 참조는 힘에 저장하여 원시 타입과 참조 타입을 구분하고 있지만 자바스크립트는 그렇지 않다. 자바스크립트에서는 변수 객체의 스코프를 따라 변수를 추적한다.
원시 타입의 원시 값은 바로 변수 객체에 저장하지만 참조 타입에서 변수 객체에 저장되는 참조 값은 메모리에 있는 실제 객체를 가리키는 포인터이다. 원시 값과 참조 값은 비슷해 보이지만 상당히 다르게 동작한다.

원시 타입과 참조 타입에도 여러 종류가 존재한다.


원시 타입

원시 타입(primitive type)은 true와 25처럼 있는 그대로 저장되는 간단한 데이터를 표현한다.
자바스크립트에는 다섯 종류의 원시 타입이 있다.

  • Boolean
    true 또는 false
  • Number
    - 정수 또는 부동소수점 실수 값
  • String
    - 한 쌍의 작은 따옴표 또는 큰 따옴표로 감싼 일련의 문자 (자바스크립트에는 문자 타입이 따로 없다)
  • Null
    - null이라는 값만 있는 원시 타입
  • Undefined
    - undefined라는 값만 있는 타입 (undefined는 아무런 값도 할당되지 않은 변수에 할당되는 값이다)

앞의 세 종류(Boolean, Number, String)는 비슷한 방식으로 동작하지만 뒤의 두 종류(null과 undefined)는 조금 다르게 동작한다.
모든 원시 타입에는 값을 표현하는 리터럴(literal) 형식이 있다. 리터럴이란 코드에 직접 입력된 이름이나 가격처럼 변수에 저장되지 않은 값을 의미한다.

다음은 리터럴 형식을 사용한 각 타입의 예제이다.

// 문자열
let name = "Superman";
let selection = "a";

// 숫자 
let count = 25;
let cost = 1.51;

// Boolean
let found = true;

// null
let object = null;

// undefined
let flag = undefined;
let ref; // 자동으로 undefined가 할당된다.

다른 많은 언어와 마찬가지로 자바스크립트 변수에는 원시 타입의 값이 바로 저장된다(객체의 포인터가 아니다). 원시 값을 변수에 할당하면 값이 변수로 복사된다.
다시 말해 등호 기호를 사용해 어떤 변수를 다른 변수에 할당하면 두 변수 모두 자신만의 데이터를 가지게 된다는 뜻이다. 다음 예를 보자.

let color1 = "red";
let color2 = color1;

이 코드에서 color1에는 "red"라는 값이 할당되었고 color2에는 color1의 값을 할당했다. 따라서 color2에는 "red"가 저장되었다. color1가 color2는 같은 값을 가지고 잇지만 두 값을 완전히 별개의 값이면 color2에 아무런 영향을 주지 않고 color1의 값을 바꿀 수 있다. 물론 color2를 바꿀 대도 마찬가지로 color1에 아무런 영향을 주지 않는다.
이는 두 값이 각 값당 하나씩 할당된 서로 다른 영역에 저장되어 있기 때문이다.

원시 값을 가지고 있는 각 변수는 독립된 저장 공간을 사용하기 때문에 다른 변수의 값을 바꿔도 영향을 받지 않는다. 다음 예를 보자.

let color1 = "red";
let color2 = color1;

console.log(color1);  // "red"
console.log(color2);  // "red"

color1 = "blue";

console.log(color1);  // "blue"
console.log(color2);  // "red"

이 코드에서 color1의 값을 "blue"로 바뀌었지만 color2는 여전히 원래대로 "red"를 유지한다.

원시 타입 종류 확인

원시 타입의 종류는 typeof 연산자를 사용해 확인할 수 있다. typeof 연산자를 변수에 사용하면 변수에 저장된 데이터의 타입을 문자열로 반환한다. typeof 연산자는 문자열, 숫자, Boolean, undefined와 잘 동작한다. 다음은 typeof 연산자를 여러 종류의 원시 값에 사용할 때 볼 수 있는 출력 결과이다.

console.log(typeof 'Nicholas");  // "string"
console.log(typeof 10);   // "number"
console.log(typeof 5.1);   // "number"
console.log(typeof true);   // "boolean"
console.log(typeof undefined);   // "undefined"

예상했듯이 문자열 값에 typeof 연산자를 사용하면 "string"을 반환하고 숫자 값에 사용하면 "number"을 반환한다. 이때, 숫자가 정수인지 실수인지는 상관없다. Boolean은 "boolean"을 반환하고 값이 undefined일 때는 "undefined"를 반환한다.

다만 null은 조금 까다롭다.
다음 코드의 실행 결과를 처음 본 개발자라면 이해하기 어려운 것이 당연하다.

console.log(typeof null); // "object"

typeof null을 실행하면 결과는 "object"이다. 그런데 null이 객체라는 까닭은 무엇일까?
사실 이는 자바스크립트 언어를 설계하고 관리하는 TC39 위원회에서 실수라고 인정했던 부분이다. 덕분에 객체를 반환해야 할 때 null을 빈 객체 포인터처럼 생각할 수 있지만 헷갈리는 것은 여전하다.

이 같은 특성 때문에 null인지 아닌지를 확인할 때는 다음곽 같이 null과 직접 비교하는 것이 가장 좋다.

console.log(value === null);  // true 또는 false

묵시적 형 변환 없이 비교하기

앞의 코드에서는 등호 두 개 대신 세 개를 사용한 연산자(===)를 사용했다.
등호 세 개를 사용한 연산자는 변수를 다른 타입으로 변환하지 않고 비교하기 때문이다.
다음 예제를 통해이 연산자가 필요한 이유를 살펴보자.

console.log("5" == 5);  // true
console.log("5" === 5); // false

console.log(undefined == null); // true
console.log(undefined === null); // false

등호 두 개를 사용하면 비교를 수행하기 전에 문자열을 숫자로 바꾸는 형 변환이 일어나기 때문에 문자열 "5"와 숫자 5는 서로 같다는 결과가 나온다.
등호 세 개를 사용한 연산자는 이러한 형 변환을 하지 않고 비교하기 때문에 두 값은 서로 다르다는 결과가 나온다.
마찬가지로 undefined와 null를 비교할 때 등호 두 개를 사용하면 두 값이 같은 것으로 보이지만, 등호 세 개를 사용하면 서로 다른 값으로 나타난다. null인지 확인하고 싶을 때는 등호 세 개를 사용해야 타입을 정확하게 알 수 있다.

원시 메소드

앞에서 본 값은 모두 원시 타입이지만, 문자열, 숫자, Boolean에는 사실 메소드가 존재한다(null과 undefined 타입에는 메소드가 없다). 특히 문자열에는 다음 예시에서 보듯이 유용한 메소드가 많다.

let name = "Nicholas";
let lowercaseName = name.toLowerCase(); // 소문자로 변환
let firstLetter = name.charAt(0); // 첫 번째 문자 가져오기
let middleOfName = name.substring(2, 5); // 두 번째부터 네 번째 문자까지 가져오기

let count = 10;
let fixedCount = count.toFixed(2); // "10.00"으로 변환
let hexCount = count.toString(16); // "a"으로 변환

let flag = true;
let stringFlag = flag.toString();  // "true"로 변환

원시 값에도 메소드가 존재하지만 원시 값 자체는 객체가 아니다. 자바스크립트의 원시 값은 다른 언어와 유사한 경험을 주기 위해 마치 객체러럼 다룰 수 있게 되어있다. 이에 대해서는 밑에서 다시 다후도록 한다.


참조 타입

참조 타입은 자바스크립트 객체를 나타내며 참조 값은 참조 타입의 인스턴스이며 객체와 같은 말이다(이후부터는 참조 값을 객체(object)라고 표현한다).
객체는 순서가 없는 프로퍼티로 이루어져 있으면 프로퍼티는 이름(문자열)과 값으로 구성되어 있다. 프로퍼티의 값이 함수일 때 이프로퍼티를 가리켜 메소드(method)라고 부른다.
사실 자바스크립트에서 함수 자체는 참조 값이므로 배열을 포함하고 있는 프로퍼티와 함수를 포함하고 있는 프로퍼티 사이에서 함수는 실행이 가능하다는 점만 빼면 차이가 거의 없다.

프로퍼티나 메소드를 다루려면 먼저 객체를 만들어야 한다.

객체 생성

자바스크립트 객체를 단순 해시 테이블로 생각해보면 이해에 도움이 된다.
객체를 만드는 것을 가리켜 인스턴스화(instantiate)한다고 하는데 자바스크립트에서 인스턴스화 방법은 두 가지가 있다.
첫 번째 방법은 new 연산자와 생성자(constructor)를 사용하는 것이다(생성자는 사실 new 연산자와 함께 사용하여 객체를 만들 수 있는 함수일 뿐이다. 어떤 함수든 생성자가 될 수 있다). 관례상 자바스크립트에서 생성자의 이름은 첫 글자를 대문자로 시작하여 생성자가 아닌 다른 함수와 구분하도록 하고 있다.
예를 들어 다음 코드는 일반 객체(generic object)를 하나 작성하고 객체의 참조를 object 변수에 저장한다.

let object = new Object();

참조 타입은 할당된 변수에 값을 직접 저장하지 않으므로 이 예제에서 object 변수에 저장된 값을 사실 객체 인스턴스가 아니라 객체가 있는 메모리상의 위치를 가리키는 포인터(또는 참조)이다. 이것이 바로 객체와 원시 값의 가장 큰 차이점인데 원시 값은 변수에 직접 값 자체가 저장된다.

객체를 변수에 할당하는 일은 사실 객체의 포인터를 할당한 셈이다. 다시 말해 어떤 변수를 다른 변수에 할당하면 두 변수는 모두 포인터이 복사본만 저장하고 있으므로 메모리상에서 똑같은 객체를 참조하게 된다. 예를 들어 다음 코드를 보자.

let object1 = new Object();
let object2 = object1;

이 코드는 먼저 새로운 객체를 생성한 후(new 연산자 사용) object1 변수에 객체의 참조를 저장한다. 다음으로 object2에 object1의 값을 할당했다. 이를 통해 두 변수 모두 첫 번째 줄에서 생성한 객체의 인스턴스를 가리키게 된다.

객체 참조 제거

자바스크립트는 가비지 컬렉션(동적으로 할당된 메모리 블럭이나 객체 중 더 이상 사용하지 않는 것을 제거하여 사용할 수 있는 메모리로 만드는 기능) 기능이 있는 언어이므로 참조 타입을 사용할 때 메모리 할당에 대해 고민하지 않아도 된다. 하지만 가비지 컬렉터(프로그래밍 언어에서 가비지 컬렉션을 수행하는 부분)가 메모리를 해제할 수 있도록 사용하지 않는 객체에 대해서는 참조 제거(dereference)를 해두는 편이 좋다. 참조 제거를 할 때는 객체 변수에 null을 할당하는 방식이 가장 좋다.

let object1 = new Object();
// 실행할 코드
object1 = null; // 참조 제거

이 코드는 object1 변수를 만들고 사용하다가 변수의 값에 null을 할당했다. 가비지 컬렉터는 메모리 상의 객체 중 전혀 참조되어 있지 않은 것이 있으면 다른 용도로 이 메모리를 사용할 수 있도록 객체가 차지하고 있는 멤리를 없앤다.
객체를 수백만 개씩 사용하는 대규모 어플리케이션에서는 객체의 참조 제거가 특히 중요하다.

프로퍼티 추가 및 제거

자바스크립트의 재미있는 특징 중 하나는 언제든 프로퍼티를 추가하거나 제거할 수 있다는 것이다. 다음 코드를 보자.

let object1 = new Object();
let object2 = object1;

object1.myCustomProperty = "Awesome";
console.log(object1.myCustomProperty); // Awesome

이 코드에서 myCustomProperty는 object1에 "Awesome"이라는 값과 함께 추가되었다.
object1과 object2는 같은 객체를 가리키고 있으므로 이 프로퍼티는 object2에서도 접근할 수 있다.

이 예제는 자바스크립트만의 고유한 특징을 보여주고 있다. 자바스크립트에서는 처음에 정의하지 않았더라도 원한다면 언제든 객체를 수정할 수 있다. 객체 변경을 막는 방법도 몇 가지 있는데 이 내용은 나중에 다룬다.

자바스크립트에는 일반 객체 참조 타입 외에도 자유롭게 사용할 수 이쓴 내장 타입이 몇 가지 더 있다.


내장 타입 인스턴스화

앞서 new Object()를 사용해 일반 객체를 생성하고 다루는 법에 대해 살펴보았다.
사실 자바스크립트에는 Object 타입 외에도 언어에서 기본적으로 제공하는 유용한 내장 참조 타입이 몇 가지 더 있다. 다른 내장 타입은 조금 더 특수한 용도로 사용할 수 있으며 언제든 인스턴스로 만들 수 있다.

내장 타입은 다음과 같다.

  • Array
    - 숫자 인덱스 값을 가진 순차 목록
  • Date
    - 날짜와 시간
  • Error
    - 실행 중 발생하는 에러 (에러의 종류에 따라 더 구체적인 하위 에러 타입이 존재한다)
  • Function
    - 함수
  • Object
    - 일반적인 객체
  • RegExp
    - 정규 표현식

내장 참조 타입은 다음과 같이 new 연산자를 사용해 인스턴스로 만들 수 있다.

let items = new Array();
let now = new Date();
let error = new Error('에러가 발생했습니다.');
let func = new Function("console.log('Hi');");
let object = new Object();
let re = new RegExp("\\d+");

리터럴 형식

일부 내장 참조 타입은 리터럴 형식도 사용할 수 있다. 리터럴은 new 연산자와 객체의 생성자를 사용하여 명시적으로 객체를 만들지 않고도 참조 값을 만들 수 있는 문법이다.

객체 및 배열 리터럴

객체는 중괄호 안에 새로 만들 객체의 프로퍼티를 정의하는 객체 리터럴 문법을 사용해 만들 수 있다. 프로퍼티는 이름, 콜론, 값으로 이루어지며 프로퍼티 사이는 쉼표로 구분한다. 다음 예제를 보자.

let book = {
  name: "객체 지향 자바스크립트",
  year: 2022
};

프로퍼티 이름에는 문자열 리터럴도 사용할 수 있는데 문자열 리터럴을 사용하면 공백과 같은 특수 문자도 프로퍼티 이름에 포함될 수 있다.

let book = {
 "name": "객체 지향 자바스크립트",
 "year": 2022
}

위 코드는 문법적으로는 조금 달라 보여도 앞에서 작성한 코드와 동일하다. 두 예제 모두 논맂거으로는 다음 코드와 동일하다.

let book = new Object();
book.name = "객체 지향 자바스크립트";
book.year = 2022;

앞서 살펴본 세 예제의 결과는 프로퍼티가 두 개 있는 개체가 하나 만들어진다는 점에서 모두 같다. 기능 자체는 결국 똑같기 때문에 어떤 패턴을 선택할지는 우리에게 달려있다.

객체 리터럴을 사용하면 실제로도 new Object()를 호출하지 않는다. 실제로 생성자를 호출하지는 않지만 자바스크립트가 내부적으로 new Object()를 사용했을 때와 동일한 단계를 수행한다. 이 원리는 다른 모든 참조 리터럴에도 마찬가지로 적용된다.

배열 리터럴을 정의하는 방법도 비슷하다. 각 괄호안에 여러 값을 추가하는게 각 값은 쉼표로 구분한다. 다음 예제를 보자.

let colors = [ "red", "blue", "green" ];
console.log(colors[0]); // "red"

이 코드는 다음 코드와 동일하다.

let colors = new Array("red", "blue","green");
console.log(colors[0]); // "red"

함수 리터럴

함수는 보통 리터럴 형식을 사용해 정의된다. 코드 문자열은 실제 코드에 비해 유지보수, 가독성, 디버그 등 여러 면에서 불리하기 때문에 사실 FUnction 생성자를 사용하는 방법은 그리 권장되지 않는다. 이 때문에 Funtion 생성자를 사용한 코드는 찾아보기 어렵다.

리터럴 형식을 사용해서 함수를 정의하면 더 쉽고 에러가 발생할 확률도 줄어든다.
다음 코드를 보자.

function reflect(value) {
 return value; 
}

// 위 코드는 다음과 같다.
const reflect = new Function("value", "return value;");

이 코드는 reflect() 함수를 정의하고 있는데 이 함수는 전달받은 값을 그래도 반환한다.
이처럼 간단한 함수일 때도 생성자를 사용하는 방법보다 리터럴을 사용하는 방법이 작성하기도 편하고 이해하기도 쉽다.
게다가 생성자 형태로 작성한 함수는 디버그하기도 까다롭다.

정규 표현식 리터럴

자바스크립트에는 RegExp 생성자를 사용하지 않고 정규 표현식을 정의할 수 있는 정규 표현식 리터럴도 있다. 정규 표현식 리터럴은 펄(Perl)의 정규 표현식과 매우 비슷하다. 슬래시 두 개 사이에 패턴을 입력하고 추가 설정은 두 번째 슬래시 뒤에 문자 한 개로 표현한다. 다음 예제를 보자.

let numbers = /\d+/g;

// 위 코드는 아래 코드와 같다.

let numbers = new RegExp("\\d+", "g");

자바스크립트에서 사용하는 정규표현식의 리터럴 형식은 문자열 안에 있는 문자를 어떻게 이스케이프 해야 할지 고민할 필요가 없기 때문에 조금 더 다루기 수월하다. RegExp 생성자를 사용하면 패턴을 문자열 인수로 전달해야 하기 때문에 백슬래시도 이스케이프 해주어야 한다(이 때문에 리터럴에서는 \d를 사용하지만 생성자에서는 \d를 사용한다). 자바스크립트에서는 대체로 생성자 형식보다 정규 표현식 리터럴이 더 많이 사용되지만 문자열 어러 개를 합쳐서 정규 표현식을 만들어야 할 때는 생성자 형식이 더 즐겨 사용된다.

Function을 제외하면 내장 타입을 인스턴스로 만드는 방법에는 옳고 그름이 엇다.
리터럴을 선호하는 개발자가 많지만 생성자 형식을 더 좋아하는 이들도 있다.
무엇이든 자신에게 편한 방식을 사용하면 된다.


프로퍼티 접근

프로퍼티는 객체에 저장된 이름/값 쌍이다. 많은 객체지향 언어가 그렇듯 자바스크립트에서 객체의 프로퍼티에 접근할 때는 점(dot) 표기법을 주로 사용하지만 각괄호 표기법과 문자열을 사용해서 접근할 수도 있다. 예를 들어 다음 코드는 점 표기법을 사용해 작성되었다.

let array = [];
array.push(12345);

각괄호 표기법을 사용할 때는 다음과 같이 각괄호 안에 메소드 이름을 문자열로 입력한다.

let array = [];
array["push"](12345);

이 문법은 접근할 프로퍼티를 동적으로 정해야 할 때 유용하다. 예를 들어 다음 코드는 각괄호 안에 문자열 리터럴 대신 변수를 사용해 접근할 프로퍼티를 설정한다.

let array = [];
let method = "push";
array[method](12345);

여기서 변수 method의 값은 "push" 이므로 배열의 push() 메소드가 호출된다.
문법을 제외하면 점 표기법과 달리 각괄호 표기법에서는 프로퍼티 이름에 특수 문자를 사용할 수 있다는 점이 유일한 차이점이다. 개발자에게는 대체로 점 표기법이 더 읽기 편하기 때문에 각괄호 표기법보다는 점 표기법을 더 자주 접할 것이다.


참조 타입 확인

함수는 typeof 연산자를 사용하면 "function" 이라는 문자열이 반환되므로 가장 확인하기 쉬운 참조 타입이다.

function reflect(val) {
 return val; 
}

console.log(typeof reflect); // "function"

다른 참조 타입은 이런 식으로 확인하기가 어려운데, 함수 외의 참조 타입에 typeof 연산자를 사용하면 "object"만 반환되기 때문이다. 따라서 여러 타입을 다룰 때 typeof 연산자는 별로 유용하지 않다. 이 때 자바스크립트의 instanceof 연산자를 사용하면 참조 타입을 더 쉽게 확인할 수 있다.

instanceof 연산자는 객체, 생성자와 함께 사용한다. 사용한 객체가 사용한 생성자의 인스턴스라면 instanceof 연산자는 true를 반환하고 그렇지 않으면 false를 반환한다.

let items = [];
let object = {};

function reflect(val) {
 return val; 
}

console.log(items instanceof Array); // true
console.log(object instanceof Object); // true
console.log(reflect instanceof Function); // true

이 예제에서는 여러 값을 instanceof 연산자 및 생성자와 함께 사용했다. 각 참조 타입에 instanceof 연산자 및 참조 타입을 나타내는 생성자를 사용하면 인스턴스의 정확한 종류를 알 수 있다(변수를 만들 때 생성자가 사용되지 않았어도 가능하다).

instanceof 연산자는 상속된 타입도 확인할 수 있다. 자바스크립트에서 모든 참조 타입은 Object를 상속하므로 사실 모든 객체는 Object의 인스턴스이다.

이를 확인하기 위해 다음 예제에서는 참조 타입 세 종류를 작성한 후 각 객체에 instanceof를 사용했다.

let items = [];
let object = {};

function reflect(val) {
 return val; 
}

console.log(items instanceof Array); // true
console.log(items instanceof Object); // true
console.log(object instanceof Object); // true
console.log(object instanceof Array); // false
console.log(reflect instanceof Function); // true
console.log(reflect instanceof Object); // true

각 참조 타입은 모든 참조 타입이 상속한 Object의 인스턴스로 확인되었다.


배열 확인

배열은 instanceof를 사용해서 확인할 수 있지만 여기에는 한 가지 문제가 있다. 자바스크립트는 같은 웹 페이지 안에 있는 프레임끼리 서로 값을 주고 받을 수 있다. 프레임의 웹 페이지는 각자 고유한 전역 컨텍스트를 가지고 있는데 이는 바꿔 말하면 Object, Array 등을 포함해 각자 고유한 내장 타입을 가지고 있다는 뜻이다. 따라서 다른 프레임에서 전달된 배열은 해당 프레임에서 정의된 Array의 인스턴스이므로 전달받은 배열에 instanceof를 사용하면 의도하지 않은 결과가 나타난다.

이 문제를 해결하기 위해 ECMAScript 5에는 배열 값이 어디에서 왔든 Array의 인스턴스인지 정확히 확인할 수 있는 Array.isArray() 메소드가 추가되었다. 이 메소드는 전달받은 값이 자바스크립트 배열이라면 출처에 상관없이 true를 반환한다. ECMAScript 5와 호환되는 환경이라면 배열인지 확인할 때 가장 좋은 방법은 Array.isArray() 이다.

let items = [];

console.log(Array.isArray(items)); // true

Array.isArray() 메소드는 대부분의 브라우저 및 Node.js 환경에서 지원된다.


원시 래퍼 타입

자바스크립트에서 가장 헷갈리는 부분은 아마도 원시 래퍼 타입(primitive wrapper types)일 것이다. 원시 래퍼 타입은 세 종류가 있는데(String, Number, Boolean), 이들은 마치 객체를 다루듯 원시 값을 쉽게 사용할 수 있도록 지원하기 위해 만들어졌다(원시 값을 다룰 때 다른 문법을 사용해야 했거나 절차식으로 텍스트를 추출해야 했다면 상당히 헷갈렸을 것이다).

원시 래퍼 타입은 문자열, 숫자, 불리언 값을 읽을 때 언어 내부에서 자동으로 만들어진다.
예를 들어 다음 코드의 첫 번째 줄은 원시 문자열 값을 name 변수에 할당한다.
두 번째 줄은 name을 마치 객체처럼 다뤄서 점 표기법을 사용해 charAt(0) 메소드를 호출했다.

let name = 'Nicholas';
let firstChar = name.charAt(0);
console.log(firstChar); // "N"

이때 내부에서 일어나는 일은 다음과 같다.

// 실제로 자바스크립트 엔진이 하는 일
let name = 'Nicholas';
let temp = new String(name);
let firstChar = temp.charAt(0);
temp = null;
console.log(firstChar); // "N"

앞서 두 번째 중에서 원시 문자열을 객체처럼 취급했기 때문에 자바스크립트 엔진은 charAt(0) 코드가 정상적으로 동작하도록 String의 인스턴스를 만든다. 이렇게 작성한 String 객체는 한 문장만 실행하고 곧 다시 파괴되는데 이 과정을 가리켜 오토박싱(autoboxing)이라 한다. 일반적인 객체를 다루듯 문자열에 프로퍼티를 추가해보면 이를 확인할 수 있다.

let name = 'Nicholas';
name.last = 'Zakas';

console.log(name.last); // undefined

이 코드는 name 문자열에 last라는 프로퍼티를 추가하려고 시도한다. 코드 자체만 보면 문제가 없지만 실제로 실행해보면 추가한 프로퍼티가 사라진다. 무슨 일이 일어난걸까? 평범한 객체를 다룰 대는 언제든 프로퍼티를 추가할 수 있으면 추가된 프로퍼티는 일부러 제거하지 않는 한 추가된 상태 그대로 있다. 원시 값에 프로퍼티를 추가하려고 하면 원시 래퍼 타입이 임시로 만들어지고 이 객체에 프로퍼티가 추가된다. 하지만 임시로 만들어진 원시 래퍼 타입은 곧 파괴되기 때문에 마치 프로퍼티가 사라진 것처럼 보인다.

자바스크립트 엔진 내부에서 실제로 일어나는 일을 코드로 표현하면 다음과 같다.

// 실제로 자바스크립트 엔진이 하는 일
let name = 'Nicholas';
let temp = new String(name);
temp.last = 'Zakas';
temp = null;  // 임시 객체가 파괴된다.

let temp = new String(name);
console.log(temp.last); // undefined
temp = null;

위에서 보듯 새로 추가한 프로퍼티는 문자열이 아니라 임시로 만들어졌다가 파괴되는 객체에 추가된다. 추가한 프로퍼티에 접근할 때 내부적으로는 새로운 객체가 만들어지므로 앞서 추가했던 프로퍼티는 존재하지 않게 된다. 원시 값에 대한 참조 값이 자동으로 만들어짐에도 불구하고 원시 값에 instanceof를 사용해 확인하면 결과는 false가 된다.

let name = 'Nicholas';
let count = 10;
let found = false;

console.log(name instanceof String);   // false
console.log(count instanceof Number);  // false
console.log(found instanceof Boolean); // false

임시 객체는 값을 읽을 때만 만들어지기 때문에 instanceof 연산자는 false를 반환한다. instanceof 연산자가 실제로는 아무런 값도 읽지 않기 때문에 임시 객체도 만들어지지 않고 따라서 원시 값은 원시 래퍼 타입의 인스턴스가 아니라는 결과가 반환되는 것이다. 원시 래퍼 타입을 명시적으로 사용해 값을 생성하는 방법도 있지만 이때는 다른 문제가 발생한다.

let name = 'Nicholas';
let count = 10;
let found = false;

console.log(typeof name); // "object"
console.log(typeof count); // "object"
console.log(typeof found); // "object"

보다시피 원시 래퍼 타입의 인스턴스를 만들면 객체가 만들어지기 때문에 typeof 연산자를 사용한 결과는 의도한 것과 다르게 나타난다.

또한 String, Number, Boolean 객체는 원시 값을 사용하듯 사용할 수 없다. 예를 들어 Boolean 객체를 사용한 다음 코드를 보자. 이 코드에서 Boolean 오브젝트의 값은 false지만 console.log("찾았다") 가 실행된다. 조건문에서 객체는 항상 true 처럼 인식되기 때문이다. 객체가 false를 표현하고 있는지 그렇지 않은지는 중요하지 안핟. 이 값은 객체이기 때문에 true처럼 사용될 뿐이다.

let found = new Boolean(false);

if (found) {
 console.log("찾았다");   // 이 줄이 실행된다. 
}

원시 래퍼 타입의 인스턴스를 명시적으로 작성하는 것은 다른 면ㄹ에서도 혼란을 줄 수 있으므로 특별한 목적이 있는 게 아니라면 사용을 피하는 편이 좋다. 원시 값 대신 원씨 래퍼 타입을 사용한다면 십중팔구는 에러를 만나게 될 뿐이다.


요약

자바스크립트에 클래스라는 개념은 ES6 이전까지 존재하지 않았으며 이를 대체할 타입이라는 개념이 존재했다. 각 변수나 데이터에는 그에 해당하는 특수한 원시 또는 참조 타입이 존재한다. 다섯가지 원시 타입(문자열, 숫자, Boolean, null, undefined)은 주어진 컨텍스트 내에서 변수 객체에 바로 저장되는 단순한 값을 의미한다. typeof 연산자를 사용하면 원시 타입의 종류를 확인할 수 있지만 null은 null과 비교해야 알 수 있다.

참조 타입은 클래스가 없는 ES6 이전의 자바스크립트에서 클래스와 가장 가까운 개념이었으며 객체는 참조 타입의 인스턴스이다. new 연산자느 리터럴 형식을 사용하면 새로운 객체를 생성할 수 있다. 프로퍼티와 메소드에 접근할 때는 보통 점 표기법을 사용하는데 각괄호 표기법을 대신 사용할 수도 있다. 자바스크립트에서는 함수도 객체이며, 어떤 값이 함수인지는 typeof 연산자를 사용해 확인할 수 있다. instanceof 연산자는 주어진 객체가 어떤 참조 타입의 인스턴스인지 확인할 때 사용한다.

자바스크립트에는 String, Number, Boolean이라는 세 종류의 원시 래퍼 타입이 있어 원시 값을 마치 참조 값처럼 다룰 수 있다. 자바스크립트는 이러한 원시 래퍼 타입을 자동으로 생성하여 일반적인 객체를 다루듯 원시 값을 사용할 수 있도록 해주지만, 자동으로 생성한 임시 객체는 문장 하나를 완료하자마자 곧 파괴된다. 원시 래퍼 타입의 인스턴스를 명시적으로 만들 수도 있지만 이 방법은 혼란을 야기할 소지가 있어 권장하지 않는다.


profile
I'm on Wave, I'm on the Vibe.

0개의 댓글