자바스크립트의 Object
타입에는 toString()
이라는 메서드가 정의되어 있다. 이름에서 보듯이 어떠한 타입을 문자열로 변환해준다. Object
가 자바스크립트에서 모든 타입의 조상이라는 걸 생각하면 자바스크립트의 모든 타입은 어떤 형태로든 toString()
이라는 메서드가 있다는 의미가 된다.
이 글에서는 이렇게 널리 퍼져있는 toString()
을 조금 더 잘 이해하고 이용하는 방법에 대해 다루어 보겠다.
1. Object.prototype.toString()
제일 단순한 형태부터 살펴보자. 새로운 Object
인스턴스를 작성하고 toString()
을 호출할 수 있다.
var o = new Object();
o.toString(); // [object Object] 반환
이 경우에는 [object Object]
라는 문자열이 반환되는데 추가된 프로퍼티가 있든 없는 항상 같은 문자열만 반환한다. 그다지 쓸모있는 정보는 아닐 것이다. toString()
은 아마 명시적으로 호출하기 보다는 문자열과 합칠 때 자동 변환 용도로 더 많이 사용될 것이다.
var o = new Object();
console.log('--> ' + o); // --> [object Object]
예를 들어, 위 예제는 "--> "
라는 문자열에 o
라는 객체를 더했는데 두 변수의 타입이 다르므로 에러가 나거나 자동 형변환이 되어야 할 것이다. 이 경우에는 o
객체가 문자열로 자동 변환되는데, 이 때 객체의 toString()
이 자동으로 호출된다. 이렇게 자바스크립트에서는 문자열과 더하기 연산을 하면 문자열이 앞에 있든 뒤에 있든 문자열 아닌 타입이 문자열로 변환을 시도한다.
2. 사용자 정의 클래스
사용자 정의 클래스의 인스턴스 역시 Object
타입을 상속하기 때문에 따로 정의하지 않아도 toString()
메서드가 정의되어 있다. 당연히 Object
에 있는 것과 같은 것이다.
class C{}
var c = new C();
c.toString(); // [object Object]
Object.prototype.toString === C.prototype.toString // true
물론, 사용자 정의 클래스 고유의 toString()을 정의할 수도 있다.
class D{
toString() {
return '[class D]';
}
}
var d = new D();
console.log('Hello, ' + d); // Hello, [class D]
3. 배열의 toString()
Array
타입에는 toString()
이 Object
와는 다르게 정의되어 있다.
var arr = [1, 2, 3];
arr.toString(); // 1,2,3 반환
보다시피 배열 각 원소를 쉼표로 구분하여 연결한 것을 반환한다. 배열의 원소를 살펴보기에는 좋은 방법이기는 한데, 혹시라도 [object Object]
와 같은 형태로 표현할 수는 없을까? 당연히 있다. Object.prototype.toString()
을 호출하면서 this
객체를 배열의 인스턴스로 바꾸어주면 된다.
"함수를 호출하면서 this
를 바꾸는" 이 작업은 함수, 즉 Function
타입의 인스턴스에 있는 call
또는 apply
를 사용하면 가능하다. 두 메서드에 관한 자세한 내용은 관련 문서를 각자 확인해보고, 여기서는 위에서 질문한 내용을 해결해보자.
var arr = [1, 2, 3];
Object.prototype.toString.call(arr); // [object Array]
배열은 Array
타입의 인스턴스이기 때문에 Object
와는 다른 결과가 나타났다. 예전에는 이를 활용해서 배열인지 아닌지 확인하기도 했다. 지금은 Array.isArray
라는 전용 함수가 존재하지만 모든 브라우저가 해당 함수를 지원하지 않았던 시기에는 toString()
을 사용해 폴리필을 만들기도 했었다.
if (typeof Array.isArray !== 'undefined') {
Array.isArray = function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
}
4. null, undefined, 다른 타입
배열에서 다루었던 것과 같은 방식을 사용하면 null
과 undefined
도 비슷한 결과를 얻을 수 있다.
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(undefined); // [object Undefined]
물론, 두 값은 그냥 일치 연산자(===)를 사용해서 확인하는 게 가장 편하다. 그 외의 타입, Math
, Date
, String
, RegExp
도 같은 방식을 사용해 결과를 얻을 수 있으니 한 번 테스트 해보자.
5. 숫자
당연히 Number
타입의 인스턴스인 숫자에도 toString()
은 존재한다. 다만, 아래와 같이 직접 toString()
을 호출하면 에러가 발생한다.
100.toString(); // Uncaught SyntaxError
자바스크립트의 숫자 타입은 정수와 소수를 모두 표현한다. 따라서 자바스크립트 파서는 정수 숫자 뒤에 점이 있으면 소숫점 이하의 수가 표현될 것이라고 기대한다. 그런데 갑자기 숫자가 아닌 toString()
이라는 문자열이 나타나니 문법적으로 맞지 않다고 오류를 발생시키는 것이다. 이럴 때는 두 가지 방법이 있는데, 하나는 점을 두 번 사용해서 첫 번째 점으로는 소숫점을 표현하며 두 번째 점으로 메서드를 표현하는 방법이고, 다른 하나는 숫자를 괄호로 묶어서 숫자의 영역을 명확히 하는 방법이 있다. 물론, 변수라면 두 방법 모두 필요없다.
100..toString(); // 100
(100).toString(); // 100
숫자 타입의 toString()
은 다른 타입과 다르게 인수를 전달할 수 있다. 이 인수는 주어진 숫자를 어느 진법을 사용해서 변환할 것인지 정한다. 기본값은 10, 즉 10진법을 사용하는 것이지만 2~36까지 다양한 값을 사용할 수 있다. 16진수가 9를 넘는 한자리 숫자에 a-f까지의 알파벳 문자를 할당하듯이 36진수에서는 9를 넘는 한자리 숫자에 a-z까지의 알파벳 문자를 사용한다.
(100).toString(8); // 144 <- 8진수
(100).toString(2); // 1100100 <- 2진수
(100).toString(36); // 2s <- 36진수
16진수가 필요한 경우도 많지 않을텐데 36진수까지 사용할 일이 얼마나 있을까 생각할 수도 있다. 개인적으로는 랜덤한 아이디를 만들 때 잘 사용하고 있는데, 큰 범위의 숫자를 압축해서 보여주고 있어서 중복되기 힘들어서 코드의 양에 비하면 꽤 괜찮은 방법이라 생각한다.
function uniqueId() {
const min = 10**6; // 최소한의 길이를 보장하기 위한 최솟값
return Math.floor(Math.random() * (Number.MAX_SAFE_INTEGER - min) + min ).toString(36);
}
랜덤함을 순수하게 Math.random()
에만 의존하고 있으므로 당연히 중복이 발생할 수 있지만 간단히 테스트 해본 결과 100만 번 내에서는 중복이 거의 발생하지 않았다. 혹여라도 고유성이 굉장히 중요한 애플리케이션이라면 아이디를 만든 후 중복을 확인하는 별도의 로직이 필요할 것이다. 혹은 정적인 변수를 두고 순차적으로 증가하게 하는 방법도 있을 것이다.
let uniqueIdCounter = 10**6;
function uniqueId() {
return Math.floor(uniqueIdCounter++).toString(36);
}
결과는 직접 사용하면서 확인해보자.
마치며
어떤 기능은 오랫동안 당연하게 사용하고 있어서 팁이라는 생각도 못하는 경우가 있다. 마지막에 언급한 숫자의 toString()
이 바로 그렇다. 이 글을 작성하면서 중복된 자료는 없는지 검색을 해보았다. toString()
을 다루는 글은 많았고, 숫자의 toString()
에 전달하는 인수에 관해 다루는 글도 있었지만 16진수보다 큰 진법을 다루거나 위처럼 고유 아이디를 만드는 데 사용하는 글은 찾지 못했다. (물론, 검색 능력의 문제일 가능성도 있다) 내가 사용해 온 기간을 생각하면 그런 팁을 쉽게 찾을 수 없다는 게 오히려 특이한 느낌이다.
가끔 기회가 될 때마다 "매뉴얼 읽기"를 강조하는데 이 글에서 설명한 기술은 모두 매뉴얼만 정독해도 알 수 있는 그리 어렵지 않은 것뿐이다. 때로는 전혀 예상하지 못한 곳에서 재밌는 기능을 발견하는 경우도 있으므로 기회가 된다면 공식 레퍼런스를 재차 읽어보기를 추천하고 싶다.
좋은 글 감사히 잘 읽었습니다