Velog
자바스크립트에서 Records & Tuples 제안이 철회된 이유
Records & Tuples 는 "불변 + 깊은 비교" 라는 매력적인 신원시 타입이었지만, 신원시 타입 추가의 엔진 비용 + `===` 의미 일관성 + 깊은 비교의 성능 세 벽에 부딪혀 철회됐다. 대안은 Composite.
핵심 요약
R&T 제안의 골자:
const t1 = #[1, 2, 3];
const r1 = #{ a: 1, b: t1 };
t1 === #[1,2,3]; // true (구조적)
r1 === #{ a:1, b: #[1,2,3] }; // true
Map 의 키로도 사용 가능
매력 포인트:
- 구조적 동등성 —
useMemo/useEffectdeps, 캐시 키, 상태 비교가 단순 - 깊은 불변 — 의도치 않은 변형 방지
철회된 3가지 벽:
| 벽 | 본질 | 영향 |
|---|---|---|
| 신원시 타입 추가 | 엔진 모든 형변환·hot path 에 분기 추가 | R&T 안 쓰는 코드도 느려짐 |
=== 의미 변경 | 4번째 동등성(SameValueNonNumeric 등) 깨짐 또는 5번째 추가 | JS 멘탈 모델 부담 |
| 깊은 비교 성능 | 인터닝 없이 선형 시간 — 최적화 보장 어려움 | 구현자들 commit 회피 |
대안: Composite — 객체 기반(원시 타입 아님) + Composite.equal(a, b) 명시 비교 + Map/Set 은 구조 비교, WeakMap/WeakSet 은 참조 비교(불일치 존재).
이 글의 핵심 교훈은 "언어에 새 원시 타입 을 더하는 비용은 보이는 것보다 훨씬 크다" 다. R&T 는 "#{} / #[] 만 추가하면 끝" 처럼 보이지만, JS 엔진에는 모든 타입 검사·형변환 경로 에 분기를 추가해야 하고, === 동등성의 의미 까지 흔든다. 그래서 "좋은 아이디어" 와 "표준에 들어갈 수 있는 아이디어" 사이에는 언어 코어 영향 이라는 별개 축이 있다.
R&T 가 무엇이었는지보다, 왜 철회되었는지 가 면접에서 더 가치 있다. 다음 세 가지를 자기 언어로 설명할 수 있어야 한다.
- 새 원시 타입이 엔진에 끼치는 전역적 비용 (R&T 안 쓰는 코드도 영향)
- 깊은 비교가 상수 시간 보장 이 안 되어
===를 "가벼운 연산" 이라는 가정에 도전 - 동등성 비교 종류가 4 → 5로 늘어나는 멘탈 부담
그리고 대안인 Composite(객체 기반 + 명시적 비교)이 어떤 다른 트레이드오프를 채택하는지 함께 답할 수 있으면 좋다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
원시 타입 추가는 *언어 전체* 비용
JS 엔진은 모든 연산에서 피연산자 타입 을 확인한다(특히 동적 디스패치). 새 원시 타입이 들어오면 다음이 모두 영향받는다.
typeof,instanceof,===,==- 형변환 (
+,==) - inline cache / hidden class
- 가비지 컬렉션 모델
결과적으로 R&T 를 안 쓰는 코드의 hot path 도 약간씩 느려진다. 이게 V8/JSC 구현자들이 "들이지 말자" 로 기운 결정적 이유 중 하나.
"안 쓰면 비용 0" 이라고 보는 것. 엔진 차원의 분기/검사가 늘어 전역 약간씩 느려진다.
깊은 비교는 "가벼운 연산" 가정과 충돌
JS 의 === 는 상수 시간 가정 위에서 모든 표준 라이브러리·엔진 최적화가 쌓여 있다. R&T 는 깊이 N 의 구조에서 O(N) 비교가 필요하고, 인터닝(intern) 없이 빠르게 하기 어렵다.
const a = #[ /* 1만 항목 */ ];
const b = #[ /* 1만 항목 */ ];
a === b; // 구조 비교 — 1만 회 검사
R&T 가 들어가면 === 가 "비싼 연산" 가능성이 생기고, 이는 루프 안에서 안전하게 === 를 쓴다 는 모든 코드의 가정을 흔든다.
"해시 미리 계산하면 되지 않나?" — 해시는 불일치 빠른 거부 만 도와주고, 일치 시엔 결국 깊이 비교가 필요하다.
동등성 종류의 폭증과 `Composite` 대안
JS 는 이미 4가지 비교가 있다.
==(추상 동등)===(엄격 동등)SameValue(Object.is)SameValueZero(Map/Set/Array.includes)
R&T 는 이를 유지하든 깨든 부담이다. 유지하면 R&T 를 위한 5번째 종류 추가, 깨면 기존 코드 회귀. 이 모순을 비껴가려는 대안이 Composite:
Composite({ ... })로 객체 생성 (원시 아님)===는 여전히 참조 비교 — 기존 의미 보존Composite.equal(a, b)로 명시 비교Map/Set은 구조 비교,WeakMap/WeakSet은 참조 비교 — 불일치는 단점
"Composite 가 R&T 의 완전 대체" 라는 인식. 사실 구조적 키 를 명시 메서드로 제한해 트레이드오프를 다른 방향으로 가져간다.
읽는 순서
- 1이론
JS 의 4가지 동등성(
==,===,SameValue,SameValueZero)을 표로 정리하고, R&T 가 추가되었을 때 어떤 칸이 비는지 채워본다. - 2구현
polyfill 인
@bloomberg/record-tuple-polyfill로 R&T 를 흉내 내 깊은 객체를 키로 쓰는 캐시를 만들어본다. 동일 구조를 1만 번 비교했을 때의 시간을 측정. - 3실무
프로젝트의
useMemodeps/캐시 키로 객체를 쓰는 곳을 골라, 직렬화 키 / immutable.js 인터닝 / Composite 제안 흉내 셋 중 어느 쪽이 더 적합한지 평가. - 4설명
동료에게 "왜 좋아 보이는 R&T 가 표준에 못 들어갔는지" 를 5분 안에 엔진 비용 / 동등성 / 깊은 비교 성능 세 축으로 설명한다.
면접 연결 질문
[감점 답변] "불변이라서 어려웠음". [좋은 답변] 불변 + 구조적 동등성 을 가진 두 신원시 타입(#{}/#[]). 매력은 분명했지만 (1) 신원시 타입이 엔진 hot path 전반에 비용, (2) === 의 "가벼운 연산" 가정과 충돌, (3) 동등성 종류가 4→5 로 폭증 — 이 셋이 표준 위원회/구현자 합의를 막아 철회됐다.
[감점 답변] "JSON.stringify 로 키 만들기". [좋은 답변] (1) 정규화된 직렬화 키 — 키 순서가 일관되도록 canonical-json 같은 라이브러리 사용 + Map<string, V>. 단 깊은 객체에 비용. (2) 인터닝 라이브러리 — immer/immutable-js로 동일 구조에 동일 참조 부여 → === 활용. (3) Composite 제안 — 표준화되면 1차 후보. 각 대안의 생성 비용 vs 비교 비용 vs 직관성 트레이드오프로 답하면 시니어 답.
[감점 답변] "새 키워드라서". [좋은 답변] 클래스는 오브젝트 한 종류 라 엔진 입장에선 기존 객체 경로만 따른다. 반면 원시 타입은 (1) typeof, instanceof, ===, == 등 전역 연산, (2) 형변환 규칙, (3) inline cache/hidden class 모델 까지 분기를 추가해야 한다. 결과적으로 R&T 안 쓰는 코드도 비용을 부담 한다 — 이게 결정적 차이.
자기 점검
"=== 는 항상 참조 비교" 라는 단정. R&T 가 들어오면 일부 값에서는 구조 비교 로 동작해 의미가 분기된다.
"Composite 가 항상 더 좋다" 는 인식. 트레이드오프가 다를 뿐 R&T 의 문법적 매력 은 잃는다.