thenullpage.com

객체는 하루에 제일 많이 만지는 자료형이다. 서버 응답도 객체고 화면 상태도 객체고 설정값도 객체다. 그런데 이 흔한 걸 다루다 난 버그들이 유독 오래 걸렸다. 빨간 에러가 뜨는 게 아니라 값만 슬쩍 틀어져 있는 경우가 많아서다.

구조 분해로 코드를 줄였다가 목록 화면이 통째로 안 떴다. 서버에서 받은 회원 정보를 꺼내는 코드였다.


const res = { id: 7, profile: { city: "서울" } };
const { id, profile: { city } } = res;
console.log(id, city); // 7 서울

const empty = { id: 9 }; // 프로필을 아직 안 채운 회원
const { profile: { city: c } } = empty;
// TypeError: Cannot destructure property 'city' of 'undefined'


중첩 구조 분해는 안쪽으로 한 겹 들어가기 전에 바깥이 객체인지 확인해주지 않는다. profile이 없으면 undefined에서 city를 꺼내려다 그 줄에서 바로 터진다. 프로필 안 채운 회원 한 명 때문에 목록 전체가 죽었다. 중간 단계에 기본값을 깔아두면 막힌다.


const { id: uid, profile: { city: city2 } = {} } = empty;
console.log(uid, city2); // 9 undefined


콜론은 별칭(다른 이름으로 받기)이고 등호는 기본값이다. 생긴 게 비슷한데 하는 일은 완전히 다르다.

동적으로 키를 만들려다 field라는 이름의 속성이 생겼다. 정렬 기준을 변수로 받아 필터 객체를 조립하는 코드였다.


const field = "grade";
const filter = { field: "A" };
console.log(filter); // { field: "A" }, grade는 어디로 갔나


객체를 중괄호로 만들 때 콜론 왼쪽은 언제나 글자 그대로 키가 된다. 변수 이름을 적어도 변수에 담긴 값이 아니라 그 이름 자체가 키로 박힌다. 변수 값을 키로 쓰려면 대괄호로 감싸야 하고, 이걸 계산된 속성명이라 부른다.


const filterFixed = { [field]: "A", [field + "_op"]: "eq" };
console.log(filterFixed); // { grade: "A", grade_op: "eq" }


반대로 키와 변수 이름이 같을 때는 한 번만 써도 되고, 이건 단축 속성이라 부른다. 꺼낼 때도 규칙은 같다. obj.field는 field라는 이름의 속성을 찾고 obj[field]는 변수에 담긴 값을 키로 쓴다.


const name = "박지훈";
const age = 30;
console.log({ name, age }); // { name: "박지훈", age: 30 }

펼쳐서 복사했는데 안쪽은 원본이랑 붙어 있었다. 설정 미리보기를 만들다 저장도 안 한 값이 실제 설정에 반영됐다.


const setting = { theme: "dark", font: { size: 14 } };
const copy = { ...setting };
copy.theme = "light";
copy.font.size = 20;
console.log(setting.theme); // dark, 여긴 안전하다
console.log(setting.font.size); // 20, 안쪽은 같이 바뀐다


점 세 개는 한 겹만 복사한다. theme처럼 값이 직접 들어 있는 속성은 새로 만들어지지만, font처럼 값이 객체인 속성은 그 객체를 가리키는 주소만 옮겨 담긴다. Object.assign({}, setting)도 똑같이 한 겹까지다. 안쪽까지 떼어내려면 깊은 복사를 해야 한다.


const deep = structuredClone(setting);
deep.font.size = 30;
console.log(setting.font.size); // 20, 이제 원본이 안 흔들린다


예전에는 JSON.parse(JSON.stringify(obj))를 관용구처럼 썼는데 이쪽은 손실이 있다. 날짜가 문자열로 변하고 값이 undefined인 속성과 함수는 통째로 사라진다. 가입일이 문자열이 되는 바람에 날짜 계산이 깨진 적이 있다.


const src = { at: new Date(), memo: undefined, run: () => {} };
console.log(JSON.parse(JSON.stringify(src))); // { at: "2026-07-25T..." }
console.log(structuredClone({ at: new Date() }).at instanceof Date); // true


대신 structuredClone도 만능은 아니라, 함수나 화면 요소가 섞여 있으면 복제하지 못하고 에러를 낸다.

속성이 있는지 검사했는데 0이 없는 걸로 취급됐다. 새로 올라온 글에서만 통계 칸이 비어 보였다.


const stat = { views: 0, likes: 12 };
if (stat.views) {
console.log("조회수 표시");
} else {
console.log("조회수 없음"); // 이쪽이 찍힌다
}
console.log("views" in stat); // true, 속성은 분명히 있다


if (obj.prop)은 속성이 있느냐가 아니라 그 값이 참으로 취급되느냐를 묻는다. 숫자 0, 빈 문자열, false는 전부 거짓 쪽이라 값이 멀쩡히 들어 있어도 없는 것처럼 흘러간다. 존재 여부를 묻고 싶으면 in이나 Object.hasOwn을 쓴다. in은 물려받은 속성까지 참으로 치고, Object.hasOwn은 그 객체가 직접 가진 것만 본다.


console.log("toString" in stat); // true, 물려받은 것까지 센다
console.log(Object.hasOwn(stat, "toString")); // false
console.log(Object.hasOwn(stat, "views")); // true

키 순서를 믿었다가 메뉴가 제멋대로 그려졌다. 게시판 번호를 키로 쓴 설정 객체를 순서대로 훑으면 되는 줄 알았다.


const board = { 12: "공지", 3: "자유", first: "즐겨찾기" };
console.log(Object.keys(board)); // ["3", "12", "first"]


객체 키 중에 정수처럼 생긴 것들은 넣은 순서를 무시하고 오름차순으로 앞에 몰린다. 나머지 문자열 키만 넣은 순서를 지킨다. 순서가 의미를 갖는 데이터라면 애초에 객체가 아니라 배열에 담아야 한다. 순서를 안 따져도 되는 경우엔 키와 값을 짝으로 주는 Object.entries가 제일 편하다.


const price = { 사과: 3000, 배: 5000 };
for (const [k, v] of Object.entries(price)) {
console.log(k, v); // 사과 3000 다음 배 5000
}
const doubled = Object.fromEntries(
Object.entries(price).map(([k, v]) => [k, v * 2])
);
console.log(doubled); // { 사과: 6000, 배: 10000 }


Object.keys는 키만, Object.values는 값만 준다. Object.fromEntries는 반대로 짝 배열을 객체로 되돌리는 쪽이라, 값만 일괄로 가공할 때 이 조합이면 충분하다.

돌아보면 객체 버그는 하나같이 조용했다. 에러가 났으면 그날 바로 잡았을 텐데, 값이 살짝 틀어진 채 화면까지 흘러가서 QA 리포트를 받고서야 알았다. 지금은 객체를 손댈 때마다 복사가 한 겹까지인지, 이 키가 글자 그대로인지 변수에 담긴 값인지를 먼저 확인한다. 헷갈리면 붙잡고 고민하지 말고 콘솔에 한 번 찍어보는 게 제일 빠르다.