자바스크립트 퀴즈북 리마인드 Day 13: 모듈은 파일 묶음이.
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
다음 코드는 무엇을 출력할까요?
const point = {
x: 3,
y: 4,
valueOf() {
return 5;
},
toString() {
return "(3, 4)";
},
};
console.log(point + 1);
console.log(`${point}`);
정답은 이렇습니다.
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와 얽힐 수 있다
}
비교 전에 어떤 변환이 일어나는지 설명할 수 없다면, 비교식을 더 명시적으로 바꾸는 편이 좋습니다.
Day 2에서 본 equality 알고리즘은 “어떤 규칙으로 같은가?”를 묻습니다. Day 3의 coercion은 그보다 한 단계 앞에서 “비교하거나 더하기 전에 값이 무엇으로 바뀌었는가?”를 묻습니다. 실무 버그는 둘이 붙어서 옵니다.
function isSelected(option, selected) {
return option.value == selected;
}
이 함수는 범용 비교 도우미처럼 보이지만, 실제로는 너무 많은 결정을 숨깁니다. option.value가 문자열인지 숫자인지, selected가 URL query에서 온 문자열인지, 빈 문자열을 0처럼 볼 것인지, NaN을 어떻게 다룰 것인지가 모두 == 안으로 들어갑니다.
리뷰에서는 먼저 도메인 값을 정해야 합니다.
function normalizeSelectedId(value) {
const raw = String(value).trim();
if (raw === "") return null;
const id = Number(raw);
return Number.isSafeInteger(id) ? id : null;
}
function isSelected(option, selected) {
const selectedId = normalizeSelectedId(selected);
return selectedId !== null && option.id === selectedId;
}
이 예제의 목적은 유틸 함수를 늘리는 것이 아닙니다. 비교 전에 “이 앱에서 selected id는 안전한 정수인가, 비어 있으면 null인가”를 선언하는 쪽이 더 리뷰 가능합니다. equality 알고리즘을 외우는 이유도 결국 여기에 있습니다. 값의 출처와 변환 경계가 보이면 ===, Object.is, includes, Map 중 어떤 비교가 맞는지도 자연스럽게 고를 수 있습니다.
AI가 만든 코드에서는 이 경계가 자주 사라집니다.
const total = price + fee;
const matched = items.find((item) => item.id == route.params.id);
두 줄 모두 짧고 그럴듯하지만, price와 fee가 숫자인지 문자열인지, route.params.id를 숫자로 바꿔도 되는지, 큰 ID를 Number로 바꾸면 안전한지 설명하지 않습니다. Day 1의 safe integer, Day 2의 equality, Day 3의 ToPrimitive를 따로 외우지 말고 하나의 리뷰 질문으로 묶는 편이 낫습니다.
이 값은 어디서 왔고, 비교·연산 전에 어떤 타입/도메인으로 정규화되었나?
+를 보면 바로 덧셈으로 읽지 말고 두 단계를 나눠야 합니다.
이 흐름을 기억하면 괴상한 퀴즈를 많이 외우지 않아도 됩니다. 코드 리뷰에서 필요한 것은 “이 값이 어떤 primitive가 되는가”를 설명하는 능력입니다.
== 비교가 타입 변환 규칙과 붙는 이유를 먼저 잡아줍니다.any를 밖에 세우고 unknown으로 검증하기는 외부 입력을 숫자·문자열로 해석하기 전에 검증 경계를 세우는 쪽으로 이어집니다.Symbol.toPrimitive와 type conversion 관련 문서const obj = { valueOf() { return 2; }, toString() { return 'two'; } };
console.log(obj + 3);Post Q&A
자바스크립트 퀴즈북 리마인드 Day 3: + 연산자와 ToPrimitive 흐름 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
DOM 이벤트의 capture/bubble, delegation, default action, React synthetic event 경계를 프론트엔드 코드 리뷰 관점에서 정리합니다.
프로토타입 조회, class 문법, 인스턴스 필드와 prototype 메서드 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
고차 함수, 순수 함수, 부수 효과, async 함수의 Promise 반환을 코드 리뷰에서 어떻게 읽을지 정리합니다.
this 바인딩을 정의 위치가 아니라 호출 형태, 메서드 분리, arrow function, bind 기준으로 읽는 방법을 코드 리뷰 관점에서 정리합니다.
콜 스택, 태스크 큐, 마이크로태스크 큐를 기준으로 Promise와 setTimeout 실행 순서를 코드 리뷰 관점에서 정리합니다.
클로저가 값 복사가 아니라 렉시컬 환경 참조라는 점을 stale closure, 루프 콜백, React 이벤트 리뷰 관점에서 정리합니다.
스코프 체인, 렉시컬 환경, var/let/const 호이스팅 차이를 콜백·상태 버그 리뷰 관점에서 다시 정리합니다.
객체 참조와 얕은 복사, Object와 Map/Set의 차이, WeakMap/WeakSet이 필요한 메모리 상황을 프론트엔드 코드 리뷰 관점에서 정리합니다.
객체 프로퍼티를 key/value가 아니라 descriptor로 읽는 법, getter/setter와 defineProperty, preventExtensions/seal/freeze의 경계를 코드 리뷰 관점에서 정리합니다.
객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.
자바스크립트의 Number가 왜 정수처럼 보여도 부동소수점 모델 위에서 움직이는지, NaN과 -0, safe integer, BigInt를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.