자바스크립트 퀴즈북 리마인드 Day 3: + 연산자와.
객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
다음 코드는 무엇을 출력할까요?
const point = {
x: 3,
y: 4,
valueOf() {
return 5;
},
toString() {
return "(3, 4)";
},
};
console.log(point + 1);
console.log(`${point}`)
Post Q&A
자바스크립트 퀴즈북 리마인드 Day 3: + 연산자와 ToPrimitive 흐름 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
정답은 이렇습니다.
6
(3, 4)
둘 다 객체를 문자열이나 숫자처럼 사용한 것처럼 보이지만, 내부에서 선택한 변환 경로가 다릅니다. point + 1은 기본 hint로 원시값을 만들고, 일반 객체에서는 보통 valueOf() 쪽이 먼저 의미를 가집니다. 템플릿 리터럴은 문자열 문맥이므로 toString() 쪽으로 가는 감각이 더 강합니다.
오늘의 핵심은 “JavaScript는 이상하게 변환한다”가 아닙니다. 코드 리뷰에서 더 실용적인 질문은 이겁니다.
이 값은 언제, 어떤 hint로, 어떤 primitive가 되는가?
+는 숫자 덧셈이다+는 가장 헷갈리는 연산자입니다. 숫자끼리 만나면 덧셈입니다.
1 + 2; // 3
하지만 둘 중 하나라도 문자열이 되면 연결입니다.
"1" + 2; // "12"
1 + "2"; // "12"
여기까지는 많이 압니다. 진짜 문제는 객체가 끼는 순간입니다.
[] + {}; // "[object Object]"
{
}
+[]; // 문맥에 따라 다르게 보일 수 있음
new Date(0) + 1; // 문자열 연결처럼 보이는 결과
이런 예제를 외우는 것은 별로 도움이 되지 않습니다. 대신 한 단계 앞을 봐야 합니다. 객체는 +에 바로 참여하지 않고 먼저 primitive로 변환됩니다. 그 결과가 문자열이면 연결, 아니면 숫자 쪽으로 갑니다.
객체가 원시값으로 바뀌는 흐름은 대략 이렇게 읽으면 됩니다.
Symbol.toPrimitive가 있으면 가장 먼저 호출된다.valueOf()와 toString()이 시도된다.+에서는 결과 중 하나가 문자열이면 문자열 연결, 아니면 numeric 연산으로 간다.중요한 점은 변환과 연산을 분리해서 읽는 것입니다.
const obj = {
valueOf() {
return 10;
},
};
obj + 5;
// obj -> 10
// 10 + 5 -> 15
const obj = {
toString() {
return "10";
},
};
obj + 5;
// obj -> "10"
// "10" + 5 -> "105"
같은 obj + 5라도 객체가 어떤 primitive를 반환하느냐에 따라 완전히 다른 코드가 됩니다.
Symbol.toPrimitive: 변환 지점을 직접 선언하기Symbol.toPrimitive는 객체가 primitive로 바뀔 때 호출되는 well-known symbol입니다.
const price = {
amount: 12000,
[Symbol.toPrimitive](hint) {
if (hint === "string") return "12,000원";
return this.amount;
},
};
Number(price); // 12000
`${price}`; // "12,000원"
price + 1000; // 13000
이 API를 실무에서 자주 직접 작성하진 않습니다. 그래도 알아야 하는 이유가 있습니다. 라이브러리나 도메인 객체가 “숫자처럼”, “문자열처럼” 동작할 때 변환 규칙이 객체 내부에 숨을 수 있기 때문입니다.
리뷰에서는 이런 질문을 하면 됩니다.
이 객체는 문자열 문맥과 숫자 문맥에서 같은 의미인가?
Symbol.toPrimitive/valueOf/toString이 비즈니스 규칙을 숨기고 있지는 않은가?
예를 들어 금액 객체가 문자열로는 "12,000원"을 반환하고 숫자로는 12000을 반환하는 것은 그럴듯합니다. 반대로 객체를 더했더니 내부 ID를 숫자로 바꾸는 식이라면, 읽는 사람이 연산 결과를 추적하기 어렵습니다.
프론트엔드에서는 객체 변환보다 폼 입력 변환이 더 자주 나옵니다.
Number(" "); // 0
Number(""); // 0
Number("3.14"); // 3.14
Number("3,000"); // NaN
Boolean({}); // true
Boolean([]); // true
공백 문자열이 0이 되는 이유는 숫자 변환 과정에서 공백이 trim된 뒤 빈 문자열처럼 취급되기 때문입니다. 빈 객체와 빈 배열은 Boolean 문맥에서 모두 truthy입니다. 그래서 이런 코드는 위험합니다.
const value = Number(input.value);
if (!value) {
showEmptyMessage();
}
0, NaN, 빈 입력이 한꺼번에 섞입니다. 리뷰에서는 변환과 검증을 분리하는 편이 낫습니다.
const raw = input.value.trim();
if (raw === "") {
showEmptyMessage();
} else {
const value = Number(raw);
if (Number.isNaN(value)) showInvalidNumberMessage();
}
이 코드는 길어졌지만 의도는 더 선명합니다. “비어 있음”과 “숫자로 파싱 실패”를 같은 falsey 처리에 맡기지 않습니다.
+를 리뷰할 때 보는 네 가지const label = count + "개";
동작은 하지만 의도는 템플릿 리터럴이 더 잘 보입니다.
const label = `${count}개`;
단순 취향 문제가 아닙니다. 템플릿 리터럴은 “문자열을 만들고 있다”는 문맥을 코드에 남깁니다.
const total = input.value + 1000;
input.value는 문자열입니다. 여기서는 덧셈이 아니라 문자열 연결이 됩니다. 먼저 숫자 변환과 실패 처리를 분리해야 합니다.
const next = money + fee;
money가 숫자인지, 금액 객체인지, BigInt인지 모르면 이 한 줄은 안전하지 않습니다. 도메인 객체라면 money.add(fee)처럼 의도를 드러내는 API가 더 나을 수 있습니다.
== 비교와 변환이 붙어 있나Day 2에서 본 equality 문제는 변환 문제와 붙습니다.
if (input.value == 0) {
// "", " ", false와 얽힐 수 있다
}
비교 전에 어떤 변환이 일어나는지 설명할 수 없다면, 비교식을 더 명시적으로 바꾸는 편이 좋습니다.
+를 보면 바로 덧셈으로 읽지 말고 두 단계를 나눠야 합니다.
이 흐름을 기억하면 괴상한 퀴즈를 많이 외우지 않아도 됩니다. 코드 리뷰에서 필요한 것은 “이 값이 어떤 primitive가 되는가”를 설명하는 능력입니다.
Symbol.toPrimitive와 type conversion 관련 문서const obj = { valueOf() { return 2; }, toString() { return 'two'; } };
console.log(obj + 3);==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.