RIP Records & Tuples
JavaScript에는 여전히 Record도, Tuple도 없다.
물론 “레코드처럼 생긴 객체”, “튜플처럼 생긴 배열”은 언제든 만들 수 있다.
하지만 그것들은 이름만 비슷할 뿐, 언어 차원의 새로운 복합 자료형과는 전혀 다른 존재다.
한때 TC39는 진짜 Record & Tuple을 표준 자바스크립트에 도입하려 했다.
그리고 이는 단순한 문법 제안이 아니라, JS의 데이터 모델 자체를 재정의하는 수준의 변화였다.
하지만 R&T는 받아들여지지 않았고, Composites라는 새로운 접근이 그 자리를 대신하고 있다.
그렇다면 애초에 R&T는 왜 필요했고, 무엇이 이 야심 찬 시도를 좌초시켰을까? 그리고 Composites는 어떻게 그 자리를 대신하고 있을까?
왜 Record & Tuple이 필요했는가
한 문장으로 말하면 이거다.
자바스크립트에는 “완전히 불변”하고 “구조적으로 비교 가능한” 복합 자료형이 단 하나도 없기 때문이다.
1. JavaScript에는 “진짜 immutable”한 복합 자료형이 없다
JS의 모든 객체, 배열, Map, Set은 기본적으로 mutable이다.
const obj = { a: 1 };
obj.a = 2; // 바뀜Object.freeze()가 있긴 하지만, 그 한계 또한 명확하다.
- 얕은(shallow) 불변만 적용됨
- 내부에 참조 타입이 있으면 여전히 mutable함
- 타입 시스템도 강제하지 못함
- 실수로 mutate하더라도 런타임에서야 에러 발생
즉, 자바스크립트에는 deep immutable을 언어 차원에서 제공하지 않는다.
Record & Tuple은 이 결함을 역사상 최초로 완전히 해결하려는 시도였다.
2. JavaScript는 구조적 비교(structural equality)를 지원하지 않는다
객체/배열 비교는 항상 참조(주소) 비교다.
{ a: 1 } === { a: 1 } // false
[1,2] === [1,2] // falseTypeScript가 구조적으로 같다고 판단해도 JavaScript 런타임에서는 전혀 다른 값으로 간주한다.
Record & Tuple은 이것을 뒤집는 문법을 제공하려 했다.
#{ a: 1 } === #{ a: 1 } // true (Record)
#[1,2] === #[1,2] // true (Tuple)즉, 런타임 수준의 deep structural equality 지원.
JS 역사상 유례없는 기능이었다.
3. 현대 JS 애플리케이션은 immutable + structural equality에 목말라 있다
React 렌더링 최적화, Redux 상태 관리, 데이터 동기화, JSON 비교, 캐싱, 퍼시스턴트 자료구조 구현 등…
현대 프론트엔드 진영에서 “변하지 않는 데이터”는 기본 철학이 되었다.
많은 JS 개발자들은 이미 Immer, immutable.js, deep-equal, lodash, Rambda 등에 의존하고 있었고,
R&T는 이 문제를 라이브러리 레벨에서 언어 레벨로 끌어올리려는 시도를 한 것이다.
4. TypeScript는 ‘정적 타입 시스템’일 뿐, 런타임을 바꾸지 못한다
많은 개발자가 “TypeScript에 tuple 있고 readonly도 있는데?”라고 생각하지만,
이는 완전한 오해다.
const t: readonly [1, 2] = [1, 2];
t[0] = 99;
// TS 에러이지만, JS 런타임에서는 실제로 변경됨TypeScript는 동작을 막지 않는다. 그냥 개발 단계에서 경고할 뿐이다.
무력한 경고..
따라서 “불변이고 구조적으로 비교 가능한 런타임 복합 자료형”을 제공하는 것은
TypeScript로 해결할 수 있는 종류의 문제가 아니었다.
R&T는 “언어의 결핍”을 해결하려는 큰 그림을 그리고자 한 것이다.
JSON의 장점을 유지하면서도 JS 런타임에서 동작하는 구조체,
완전한 deep immutability,
=== 기반의 구조적 비교,
엔진 레벨의 최적화 가능성.
R&T의 기대치는 높을 수밖에 없었다.
그런데 왜 죽었는가?
결론만 말하면 너무 큰 변화였기 때문이다.
1. 새로운 primitive를 추가하는 것은 언어에 있어 너무 큰 변화
Record & Tuple은 완전히 새로운 종류의 값이었다.
이것은 JS의 전체 철학과 엔진 구조에 큰 부하를 준다.
- typeof 추가
- GC 모델 영향
- 최적화 파이프라인 변화
- API 호환성 이슈
JS 진영에서 “새로운 primitive”는 매우 보수적으로 다루는 주제다.
R&T는 그 선을 넘는 제안이었다.
2. === 의미가 뒤틀린다
R&T는 === 연산을 값 기반 비교로 확장하려 했다. 하지만 JS에서 ===는 30년 넘게 “참조 비교”라는 전통을 유지해왔다.
그래서 우려가 컸다.
- ===의 의미가 바뀌면 학습 비용 상승
- deep equality 최적화가 V8 구조와 충돌
- 혼란과 예측 불가한 동작 가능성
결국 TC39는 이것을 받아들이지 못했다.
그리고 등장한 Composites
Record & Tuple은 사라졌지만, “필요성” 자체가 사라진 건 아니다.
그래서 새로운 접근이 등장했다.
Composites의 핵심 목적은 크게 두 가지다.
- JS에서 깊은 비교(Deep Structural Equality)를 제공하는 것
- 불변(immutable) 또는 불변에 가까운(frozen) 데이터 구조를 편하게 다루도록 하는 것
접근 방식은 이전의 R&T 제안과는 완전히 다르다.
Composite는 ‘완전 불변’이 아니라 ‘frozen’이다.
R&T는 원시값, 레코드, 튜플만 담을 수 있기에 구조적으로 깊은 불변성이 강제된 반면,
Composites는 함수, 객체, 심볼, 프록시 등 어떤 값이든 담을 수 있기 때문에
불변성은 내부에 무엇이 들어 있느냐에 따라 달라진다.
내부는 변경 가능하고, Composite라는 껍데기 자체만 freeze되는 개념이다.
즉 “deep immutable”이 아니라 “shallow immutable(frozen)”로 볼 수 있다.
구조적 동등성 또한 === 연산자 오버라이드는 포기하고, equal 메서드를 통해 깊은 비교를 수행한다.
언어 철학을 건드리지 않는 선에서 불변성을 지원한다는 것.
더 유연하고, 더 비싸지 않고, 더 안전한 방식이다.
| 항목 | Records & Tuples | Composites |
|---|---|---|
| 깊은 불변성 | 기본 제공 | 내부값은 제한 없음 |
| 구조 비교 | === 오버로드 | Composite.equal |
| 문법 추가 | #{} / #[] | 없음 |
| 타입 제한 | primitive / R&T만 | 제한 없음 |
| JS 엔진 구현 비용 | 매우 높음 | 낮음 |
| 생태계 충격 | 매우 큼 | 매우 적음 |
결론적으로 R&T가 새로운 원시 타입을 추가하자는 거였다면,
Composites는 그냥 기존의 JS 객체에 특별한 태그를 부여한 형태라고 볼 수 있다.
Record & Tuple은 명백히 더 우아하고 더 정교하고 더 완벽한 해결책이었을지도 모른다.
하지만 새로운 primitive 도입, === 오버라이드, 엔진 구조 변화 등
너무나도 많은 부분을 뒤흔드는 제안이었다.
Composites는 그 비전을 더 현실적인 형태로 계승한다. Composites는 보다 더 유연하다.
그리고 보통, 유연함은 많은 것을 이기는 큰 강점이 된다.
유연하고 귀엽다
B
u
y
M
e
A
C
o
f
f
e
e
☕
️