객체는 JS에서 데이터를 담는 기본 그릇이다. 다른 언어의 딕셔너리나 해시맵, 구조체를 떠올리면 얼추 맞는데, JS 객체는 그것들과 미묘하게 다르게 논다. 키는 문자열이고 값은 뭐든 들어가며, 없는 키를 꺼내도 에러 대신 undefined가 조용히 나온다. 이 조용함이 편할 때도 있고 사람 잡을 때도 있다.


이번 편은 매일 쓰는 객체 문법을 상황과 실행 결과로 짚는다. 구조분해와 스프레드, Object 메서드 이 셋만 손에 붙으면 객체를 다루는 코드가 절반으로 줄어든다.


점 표기와 대괄호 표기로 꺼낸다. 키 이름을 코드에 직접 적을 땐 점 표기가 짧아서 좋다. 하지만 키에 하이픈이 섞였거나 키 이름을 변수로 받아야 할 땐 대괄호 표기밖에 방법이 없다.


const user = { name: "hana", age: 30, "sign-up": true };
console.log(user.name);
console.log(user["sign-up"]);


user.namehana, user["sign-up"]true다. 하이픈이 든 sign-upuser.sign-up이라고 쓰면 뺄셈으로 해석돼 버리니 대괄호로 꺼내야 한다. 키를 담은 변수로 꺼낼 때도 user[key]처럼 대괄호가 필요하다.


구조분해로 필요한 값만 꺼낸다. 객체에서 값을 하나씩 변수에 담다 보면 같은 말을 두 번 쓰게 된다. 구조분해(destructuring)는 왼쪽에 객체 모양을 그려놓고 필요한 키만 뽑아 변수로 받는다. 없는 키엔 기본값을, 마음에 안 드는 이름엔 별칭을 붙일 수 있다.


const user = { name: "hana", age: 30 };
const { name, age } = user;
const { name: userName, role = "guest" } = user;


nameage에 각각 hana30이 담긴다. 둘째 줄에서 name: userNamename 값을 userName이라는 다른 이름으로 받으라는 뜻이라 userNamehana가 되고, 객체에 없는 role은 기본값 guest가 대신 들어간다. 콜론이 별칭이고 등호가 기본값이라는 이 둘을 헷갈리면 값이 엉뚱하게 꽂힌다.


중첩 객체도 한 번에 푼다. 응답 데이터처럼 객체 안에 객체가 든 구조는 안쪽 모양까지 그대로 그려주면 깊은 값도 바로 꺼낸다.


const res = { data: { id: 7, tags: ["a", "b"] }, ok: true };
const { data: { id, tags } } = res;
console.log(id, tags);


결과는 7 [ 'a', 'b' ]다. 여기서 data:는 별칭이 아니라 그 안으로 파고들라는 표시일 뿐이라, data라는 변수는 생기지 않고 idtags만 남는다. 나는 이걸 모르고 data를 또 쓰려다 data is not defined로 한참 헤맸다.


스프레드로 복사하고 병합한다. 점 세 개 스프레드 연산자는 객체의 키와 값을 낱개로 흩어 새 객체에 담는다. 뒤에 같은 키를 다시 적으면 그 값이 앞을 덮어써서, 기본 설정에 일부만 갈아끼우는 패턴이 한 줄로 끝난다.


const base = { a: 1, b: 2 };
const copy = { ...base, b: 99, c: 3 };
console.log(copy);
console.log(base);


copy{ a: 1, b: 99, c: 3 }가 된다. baseb는 2였지만 뒤에 쓴 b: 99가 이겨서 99로 덮이고, 없던 c는 새로 붙는다. 원본 base{ a: 1, b: 2 } 그대로다. 순서가 승부를 가르니 덮어쓸 값은 항상 스프레드 뒤에 둬야 한다.


스프레드는 얕은 복사다. 방금 원본이 안 바뀌는 걸 봤으니 안심하기 쉬운데, 그건 한 겹까지만이다. 스프레드는 안쪽에 든 객체는 복사하지 않고 그 참조만 옮긴다. 그래서 복사본 안의 객체를 고치면 원본까지 같이 바뀐다.


const orig = { info: { level: 1 } };
const shallow = { ...orig };
shallow.info.level = 5;
console.log(orig.info.level);


orig.info.level이 5로 나온다. shallow만 고쳤는데 원본이 따라 바뀐 것은 shallow.infoorig.info가 같은 객체를 가리키고 있어서다. 나는 이 함정에 걸려 상태가 유령처럼 번지는 버그를 며칠 쫓았다. 깊이 복사가 필요하면 structuredClone(orig)를 쓴다.


Object.keys, values, entries로 순회한다. 배열엔 map이나 filter가 있지만 객체엔 없다. 그래서 객체를 반복하려면 먼저 키나 값의 배열로 바꿔야 한다. 이 셋이 그 다리다.


const scores = { kor: 90, eng: 80, math: 70 };
console.log(Object.keys(scores));
console.log(Object.values(scores));
const sum = Object.values(scores).reduce((s, v) => s + v, 0);
console.log(sum);


Object.keys[ 'kor', 'eng', 'math' ], Object.values[ 90, 80, 70 ]를 준다. 값 배열을 reduce로 접으면 합계 240이 나온다. 키와 값을 짝지어 다루고 싶으면 Object.entries[ [ 'kor', 90 ], ... ] 꼴의 쌍 배열을 줘서, entriesmap으로 돌리며 키와 값을 동시에 만지는 식으로 쓴다.


단축 속성명과 계산된 속성명. 변수 이름과 키 이름이 같으면 한 번만 적으면 되고, 키 이름 자체를 변수로 정하고 싶으면 대괄호로 감싼다.


const val = "[email protected]";
const key = "email";
const built = { val, [key]: "[email protected]" };
console.log(built);


built{ val: '[email protected]', email: '[email protected]' }가 된다. val 하나만 쓴 건 val: val의 줄임이라 키도 val이 되고, [key]key 변수의 값인 email이 키로 박힌다. 대괄호를 안 씌우고 그냥 key라고 적으면 문자열 "key"가 키가 돼 버리니 주의한다.


옵셔널 체이닝과 널 병합. 없을지도 모르는 깊은 값을 꺼낼 때 중간이 undefined면 프로그램이 터진다. ?.는 중간이 비면 거기서 멈추고 undefined를 주며, ??는 왼쪽이 null이나 undefined일 때만 오른쪽 기본값을 쓴다.


const config = { server: { port: 8080 } };
console.log(config.server?.port);
console.log(config.db?.host);
console.log(config.db?.host ?? "localhost");
console.log(0 ?? "기본값");
console.log(0 || "기본값");


차례로 8080, undefined, localhost, 0, 기본값이 찍힌다. config.db가 없어도 ?. 덕에 에러 없이 undefined가 나오고, ?? "localhost"를 붙이면 기본값이 채워진다. 맨 아래 둘이 핵심이다. 0 ?? 기본값은 0을 살려 0을 주지만 0 || 기본값은 0을 거짓으로 봐 기본값을 준다. 0이나 빈 문자열이 정당한 값일 땐 || 말고 ??를 써야 멀쩡한 값을 안 잃는다.


객체는 참조로 비교한다. 숫자나 문자열은 값이 같으면 같다고 나오지만, 객체는 생김새가 똑같아도 서로 다른 것으로 친다. 같은 것인지 보려면 같은 객체를 가리키는지를 봐야 한다.


console.log({ a: 1 } === { a: 1 });
const p = { a: 1 };
const q = p;
console.log(p === q);


첫 줄은 false, 둘째는 true다. 내용이 같은 두 객체라도 각각 따로 만들어졌으면 다른 주소를 가리켜 false가 된다. q = p는 값을 복사한 게 아니라 같은 객체에 이름표를 하나 더 단 것이라 true다. 그래서 q를 고치면 p도 바뀐다. 객체 두 개의 내용이 같은지 진짜로 알고 싶으면 키를 하나씩 비교하거나 JSON.stringify로 문자열을 만들어 견줘야 한다.


객체를 다룰 때 나는 매번 두 가지를 떠올린다. 지금 이 값은 복사본인가 원본과 이어진 참조인가, 없는 키를 건드리면 에러가 아니라 undefined가 나온다는 것. 이 둘만 붙잡으면 객체가 얽혀 생기는 유령 버그의 대부분이 애초에 생기지 않는다. 헷갈리는 문법은 만날 때마다 작은 객체로 직접 찍어보는 게 제일 빨리 몸에 붙는다.