Skip to content
This page has been auto-translated and may contain errors.View in English

객체

docs.scrimba.com

한 명의 사용자를 표현한다고 해봅시다. 이름, 이메일, 나이, 로그인 여부 같은 정보가 필요합니다. 이 정보들을 네 개의 개별 변수로 흩어놓을 수도 있지만, 이 값들은 원래 한 묶음이고, 따로 떼어놓으면 "그 사용자"를 가리킬 때마다 매번 네 가지를 함께 넘겨야 합니다. 객체를 쓰면 이 모든 정보를 하나의 값으로 모을 수 있고, 각 조각에는 찾아볼 수 있는 이름이 붙습니다. 여러분이 다룰 데이터 대부분, 사용자든 상품이든 장바구니든, 바로 이런 모양으로 도착합니다.

객체 만들기

**객체**는 이름이 붙은 값들의 모음으로, 중괄호로 작성합니다. 중괄호 안에는 key: value 쌍을 쉼표로 구분해서 나열합니다. 키는 이름이고, 값은 그 이름이 담고 있는 내용입니다:

js
const user = {
  name: "지민",
  age: 27,
};

console.log(user.name); // 지민

값을 다시 읽어올 때는 점과 키를 사용합니다: user.name. 이것을 점 표기법이라고 부르며, 앞으로 가장 자주 쓰게 될 방식입니다. 왼쪽에 키, 오른쪽에 그 키가 담고 있는 값이라고 생각하면 됩니다.

값을 읽는 두 번째 방법도 있는데, 키를 문자열로 넣는 대괄호 표기법입니다:

js
console.log(user["name"]); // 지민

대부분의 경우 점 표기법이 더 깔끔하게 읽히므로 그쪽을 사용하세요. 대괄호 표기법은 키가 일반적인 단어 형태가 아닐 때(예: 공백이 들어 있을 때)나, 키를 미리 알 수 없어서 다른 변수에 담겨 있을 때 제 역할을 합니다.

**객체**는 관련된 값들을 문자열 키 아래 묶으며, 중괄호 안에 key: value 쌍으로 작성합니다. 배열이 위치로 접근하는 순서 있는 목록을 담는 것과 달리, 객체는 이름으로 접근하는 값을 담습니다:

js
const user = {
  name: "지민",
  age: 27,
  isActive: true,
};

console.log(user.name); // 지민
console.log(user["age"]); // 27

값을 읽는 방법은 두 가지입니다. 점 표기법(user.name)이 기본이며, 더 짧고 명확합니다. 대괄호 표기법(user["age"])도 같은 일을 하지만 키를 문자열로 받으며, 이를 통해 점 표기법으로는 할 수 없는 두 가지가 가능해집니다.

키가 유효한 식별자가 아닐 때(예를 들어 공백이 들어 있거나 숫자로 시작할 때), 그리고 키가 리터럴로 직접 쓰이지 않고 변수에 담긴 동적인 값일 때는 대괄호 표기법이 반드시 필요합니다:

js
const field = "name";
console.log(user[field]); // 지민, `field`에 담긴 키를 읽습니다
console.log(user.field); // undefined, "field"라는 이름의 리터럴 키를 찾습니다

이 차이 때문에 헷갈리기 쉽습니다: user[field]field의 값을 사용하고, user.fieldfield라는 이름의 키를 찾습니다. 존재하지 않는 키를 읽으면 오류가 아니라 undefined가 반환됩니다.

**객체**는 값을 키 아래 묶는 자바스크립트의 핵심 구조입니다. 키는 문자열(또는 Symbol이지만 지금은 넘어가도 됩니다)이고, 값에는 다른 객체나 함수를 포함해 무엇이든 올 수 있습니다. key: value 쌍을 중괄호 안에 작성합니다:

js
const user = {
  name: "지민",
  age: 27,
  isActive: true,
};

console.log(user.name); // 지민
console.log(user["age"]); // 27

점 표기법이 관용적인 기본 방식입니다. 대괄호 표기법은 키를 표현식으로 계산한다는 점만 다른 동일한 조회 방식이며, 정확히 두 가지 경우에 반드시 필요합니다: 키가 유효한 식별자가 아닐 때(user["first name"], user["2fa"]), 그리고 키가 동적일 때입니다:

js
const field = "name";
console.log(user[field]); // 지민, 먼저 `field`를 "name"으로 평가합니다

기억해둘 만한 동작이 두 가지 있습니다. 존재하지 않는 키를 읽으면 오류를 던지지 않고 undefined를 반환하므로, 오타는 조용히 실패할 뿐 큰 소리를 내지 않습니다. 그리고 내부적으로 키는 항상 문자열입니다: user[27]user["27"]은 같은 자리를 가리키는데, 숫자가 문자열 키로 강제 변환되기 때문입니다. 이 강제 변환은 객체 키와 배열 인덱스가 비슷하게 보이기 시작할 때 흔한 혼란의 원인이 됩니다.

js
const product = {
  title: "책상 램프",
  price: 40,
  inStock: true,
};

console.log(`${product.title} costs ${product.price}`); // 책상 램프 costs 40

객체도 다른 것과 마찬가지로 하나의 값입니다. const에 저장할 수도 있고, 배열에 넣을 수도 있고, 함수에 넘기거나 반환할 수도 있습니다. 각 부분에 이름을 붙여주는 것이 바로 객체의 핵심입니다: product.price는 그 값이 무엇인지 말해주지만, 변수에 그냥 담긴 40은 그렇지 않습니다.

Juno객체 만들기 객체는 관련된 값들을 이름 아래 모으며, 중괄호 안에 key: value 쌍으로 작성합니다. 값은 점과 키로 읽습니다, 예를 들면 user.name처럼요. 키에 특이한 문자가 들어 있거나 변수에 담겨 있을 때만 대괄호 표기법을 쓰세요.
Juno객체 만들기 객체는 배열이 위치로 값을 찾는 것과 달리 이름으로 값을 찾습니다. 점 표기법이 기본이고, 대괄호 표기법은 키를 문자열로 받으므로 일반적인 단어가 아닌 키나 변수에 담긴 키에 사용하세요. user[field]field에 담긴 키를 읽고, user.field는 리터럴 field 키를 찾는다는 점, 그리고 존재하지 않는 키는 오류 대신 undefined로 돌아온다는 점을 기억하세요.
Juno객체 만들기 내부적으로 키는 문자열이므로 user[27]user["27"]은 같은 자리를 가리키고, 존재하지 않는 키는 오류 대신 undefined를 반환합니다. 점 표기법이 관용적인 방식이고, 대괄호 표기법은 키를 계산해서 쓰는 같은 조회 방식으로, 식별자가 아닌 키나 동적인 키에 필요합니다. 오타에도 조용히 undefined가 나오는 편리함은 버그를 숨겨버리기 전까지만 편리합니다.

객체 바꾸기

객체는 한번 만들었다고 고정되는 것이 아닙니다. 새로운 키를 추가하거나, 기존 키를 바꾸거나, 완전히 지울 수도 있습니다. 추가하거나 수정하려면 키에 값을 대입하세요:

js
const user = {
  name: "지민",
};

user.age = 30; // 새로운 키 추가
user.name = "김지민"; // 기존 키 수정

console.log(user); // { name: "김지민", age: 30 }

키가 이미 있으면 대입할 때 값이 교체됩니다. 없으면 새로 생깁니다. 키를 지우려면 delete를 사용하세요:

js
delete user.age;
console.log(user); // { name: "김지민" }

여기서 사람들이 놀라는 부분이 있습니다. const로 만든 객체라도 속성은 여전히 바꿀 수 있습니다. constuser라는 이름이 다른 객체를 가리키도록 재할당할 수 없다는 뜻이지만, 그 이름이 가리키는 객체 안의 내용은 자유롭게 바꿀 수 있습니다:

js
const cart = { total: 0 };
cart.total = 25; // 괜찮습니다, 내부 값을 바꾸는 것뿐이니까요
// cart = { total: 25 }; // 이건 오류입니다, 이름 자체를 재할당하는 것이니까요

객체는 변경 가능합니다: 만든 뒤에도 키를 추가, 수정, 삭제할 수 있습니다. 키에 값을 대입하면 새로운 키면 생성되고 이미 있는 키면 덮어씌워집니다. 키를 지우는 일은 delete가 맡습니다:

js
const user = { name: "지민" };

user.age = 30; // 추가
user.age = 31; // 수정
delete user.age; // 삭제

console.log(user); // { name: "지민" }

사람들을 헷갈리게 하는 것은 const입니다. const는 이름을 고정할 뿐, 내용을 고정하지 않습니다. 바인딩 자체는 새로운 객체로 재할당할 수 없지만, 그 바인딩이 가리키는 객체는 여전히 완전히 수정 가능합니다:

js
const cart = { total: 0, items: 0 };
cart.total = 25; // 허용됩니다, 객체를 변경하는 것뿐이니까요
cart.items += 1; // 허용됩니다
// cart = {}; // TypeError, const 바인딩을 재할당하려는 것이니까요

이런 이유로, 나중에 바꿀 계획이 있는 객체라도 const를 기본값으로 쓰는 것이 합리적입니다. 변수 자체를 다른 곳으로 다시 가리키게 할 일은 거의 없으니, const는 그런 실수를 막아주면서도 객체 자체는 자유롭게 열어둡니다.

객체는 변경 가능합니다. 키에 값을 대입하면 upsert가 일어나고(생성하거나 덮어쓰고), delete가 그 키를 제거합니다:

js
const user = { name: "지민", age: 30 };

user.role = "admin"; // 추가
delete user.age; // 삭제

console.log(user); // { name: "지민", role: "admin" }

const에 대한 구분은 꼭 몸에 익혀둘 만한데, 사실 이는 const가 실제로 무엇을 지켜주는지에 관한 문제이기 때문입니다. const는 바인딩을 고정할 뿐, 값을 고정하지 않습니다. 이름은 재할당할 수 없지만, 그 이름이 참조하는 객체는 여전히 열려 있습니다:

js
const cart = { total: 0 };
cart.total = 25; // 괜찮습니다, 참조된 객체를 변경하는 것뿐이니까요
// cart = { total: 25 }; // TypeError: Assignment to constant variable

객체 자체를 정말로 고정하고 싶다면, Object.freeze(obj)가 키의 추가, 변경, 삭제를 막아줍니다. 다만 이는 얕은 동결이라 (중첩된 객체는 여전히 변경 가능합니다) 비엄격 모드 코드에서는 오류 없이 조용히 아무 일도 하지 않습니다. 실무에서는 이 함수가 필요한 경우가 거의 없습니다. 지켜둘 만한 습관은 const를 기본값으로 삼는 것입니다: 이는 바인딩이 안정적이라는 신호를 주고 실제로 자주 일어나는 실수인 실수로 인한 재할당을 막아주면서도, 평범한 변경은 여전히 가능하게 열어둡니다.

Juno객체 바꾸기 키에 값을 대입하면 추가되거나 수정되고, delete로 지울 수 있습니다. 놀라운 부분은 const입니다: user가 다른 객체를 가리키게 하는 것은 막아주지만, 그 객체 안의 내용을 바꾸는 것은 여전히 가능합니다. 저도 const가 모든 것을 잠가준다고 생각하는 버릇을 버리는 데 한참 걸렸습니다.
Juno객체 바꾸기 대입은 키를 추가하거나 덮어쓰고, delete는 키를 지웁니다. const는 이름을 고정할 뿐 내용을 고정하지 않으므로, const 객체는 여전히 완전히 수정 가능하면서 변수 자체는 다시 가리킬 수 없습니다. 바로 이 점 때문에 바꿀 계획이 있는 객체라도 const가 좋은 기본값이 됩니다.
Juno객체 바꾸기 대입은 키를 upsert하고, delete는 키를 지우며, const는 값이 아니라 바인딩을 지켜주므로 참조된 객체는 계속 변경 가능한 상태로 남습니다. 객체 자체를 꼭 잠가야 한다면, Object.freeze가 얕게 그 일을 해주고 엄격 모드 밖에서는 조용히 실패합니다. 이건 드물게만 쓰세요. const를 기본값으로 쓰는 것만으로도 실제로 자주 발생하는 재할당 실수는 이미 막을 수 있습니다.

메서드와 this

객체 안의 값은 함수일 수도 있습니다. 그럴 때는 이를 **메서드**라고 부르는데, 객체가 저장하는 사실이 아니라 객체가 할 수 있는 동작을 뜻합니다. 점을 찍고 괄호를 붙여서 호출합니다:

js
const user = {
  name: "지민",
  greet() {
    return `안녕하세요, 저는 ${this.name}입니다`;
  },
};

console.log(user.greet()); // 안녕하세요, 저는 지민입니다

여기서 새로 나오는 단어가 this입니다. user.greet()를 호출하면, this는 "이 메서드가 호출된 대상 객체"를 뜻하고, 여기서는 user입니다. 그래서 this.name은 같은 객체에서 name 키를 읽어옵니다. 이렇게 메서드는 값을 따로 넘겨받지 않고도 자기 객체의 값에 접근할 수 있습니다.

함수가 객체 안에 값으로 저장되면 그것을 **메서드**라고 부릅니다. 메서드는 객체가 데이터와 함께 동작을 가질 수 있게 해주며, this를 통해 그 데이터에 접근합니다:

js
const cart = {
  items: ["램프", "책상"],
  total: 65,
  summary() {
    return `${this.items.length} items, ${this.total} total`;
  },
};

console.log(cart.summary()); // 2 items, 65 total

여러분이 마주칠 상황 대부분을 커버하는 this의 규칙은 이렇습니다: this는 호출 시점에 점 왼쪽에 있는 객체입니다. cart.summary()를 호출하면 thiscart이므로, this.itemsthis.total은 같은 객체에서 값을 읽습니다.

"호출 시점"이라는 표현이 중요합니다. this는 메서드를 작성할 때 정해지는 것이 아니라, 그 메서드가 어떻게 호출되는지에 따라 결정됩니다. cart.summary()처럼 호출하면 thiscart이지만, 그 함수를 꺼내서 단독으로 호출하면 this는 더 이상 cart를 가리키지 않습니다. 이 부분은 나중에 다시 다루겠지만, 점으로 호출하는 메서드라면 this는 여러분이 기대하는 대로 정확히 동작합니다.

객체에 저장된 함수가 **메서드**이고, 이는 this를 통해 그 객체 자신의 상태에 접근합니다. 실무에서 중요한 바인딩 규칙은 이것입니다: this는 함수가 어디서 정의됐는지가 아니라 어떻게 호출됐는지로 결정됩니다. 일반적인 메서드 호출에서는 this가 점 왼쪽의 객체입니다:

js
const cart = {
  total: 65,
  discount: 0.1,
  finalPrice() {
    return this.total * (1 - this.discount);
  },
};

console.log(cart.finalPrice()); // 58.5

바인딩이 동적으로 결정되기 때문에, 메서드를 분리해내면 깨집니다: const fn = cart.finalPrice; fn();cart 컨텍스트를 잃어버리므로 this가 더 이상 원하는 곳을 가리키지 않습니다.

실무에서 흔히 걸려드는 부분은 메서드로 쓰인 화살표 함수입니다. 화살표 함수는 자기만의 this를 갖지 않고, 정의된 위치의 바깥 스코프에 있는 this를 그대로 사용합니다. 그래서 메서드로 쓰인 화살표 함수는 그것이 속한 객체가 아니라 바깥의 this(최상위 레벨의 this)를 읽습니다:

js
const user = {
  name: "지민",
  greet: () => `안녕하세요, 저는 ${this.name}입니다`, // 잘못된 방식: `this`가 `user`가 아닙니다
  greetOk() {
    return `안녕하세요, 저는 ${this.name}입니다`; // 올바른 방식: 메서드 축약 표기법
  },
};

기억해둘 규칙은 이렇습니다: 함수 안의 this가 그 객체를 뜻해야 한다면 메서드 축약 표기법(greet() {})을 쓰고, 화살표 함수는 실제로 바깥의 this를 그대로 쓰고 싶은 콜백에만 남겨두세요. 대부분의 다른 상황이 바로 그런 경우입니다.

Juno메서드와 this 객체에 저장된 함수가 메서드이며, user.greet()처럼 점과 괄호로 호출합니다. 메서드 안에서 this는 그 메서드가 호출된 객체를 뜻하므로, this.name은 그 객체 자신의 name을 읽습니다. 이렇게 메서드는 자기 객체의 값에 접근합니다.
Juno메서드와 this 메서드는 객체 안에 있는 함수이고, 호출할 때 this는 점 왼쪽의 객체가 됩니다. 그래서 cart.summary()를 호출하면 thiscart를 뜻합니다. 함정은 this가 메서드를 작성할 때가 아니라 호출할 때 결정된다는 점인데, 메서드를 객체에서 떼어내는 순간 이 점이 중요해집니다.
Juno메서드와 thisthis는 함수가 어디에 있는지가 아니라 어떻게 호출되는지에 따라 바인딩되므로, 메서드를 분리하면 그 객체를 잃어버립니다. this가 객체를 뜻해야 할 때는 메서드 축약 표기법을 쓰고, 바깥의 this를 그대로 쓰고 싶은 콜백에는 화살표 함수를 남겨두세요. 화살표 함수는 대신 바깥의 this를 그대로 캡처하기 때문입니다. 메서드로 화살표 함수를 잘못 쓰는 실수는 흔한 사례이고, 조용히 실패합니다.

객체와 배열을 함께 쓰기

실제 데이터 대부분은 객체의 목록입니다: 여러 사용자, 여러 상품처럼, 각 객체가 같은 키를 가진 하나의 레코드입니다. 이런 객체들을 배열 안에 넣습니다:

js
const products = [
  { title: "책상 램프", price: 40 },
  { title: "노트", price: 6 },
  { title: "펜", price: 3 },
];

console.log(products[0].title); // 책상 램프

먼저 위치로 하나의 객체에 접근하고(products[0]), 그다음 점으로 그 객체의 필드 중 하나에 접근합니다(.title). 이어 붙이면, products[0].title은 첫 번째 상품의 제목을 읽습니다.

모든 객체에서 어떤 필드를 읽으려면 배열을 순회하세요:

js
for (const product of products) {
  console.log(product.title);
}
// 책상 램프
// 노트
// 펜

반복문이 돌 때마다 product는 목록에서 하나의 객체가 되고, product.title은 그 객체의 제목을 읽습니다. 이 모양, 즉 순회하는 객체 배열이, 여러분이 하게 될 데이터 작업의 대부분입니다.

객체 배열은 실제 데이터 대부분이 가진 모양입니다: 각 객체가 레코드이고, 모두 같은 키를 공유합니다. 배열 접근과 점 접근을 결합하면 어떤 필드에도 접근할 수 있습니다:

js
const products = [
  { title: "책상 램프", price: 40 },
  { title: "노트", price: 6 },
  { title: "펜", price: 3 },
];

console.log(products[1].price); // 6

배열이기 때문에 배열 메서드를 사용할 수 있고, 바로 여기서 그 진짜 가치가 드러납니다. map으로 각 객체에서 필드 하나를 뽑아내고, filter로 조건에 맞는 레코드만 남기고, reduce로 하나의 값으로 합칠 수 있습니다:

js
const titles = products.map((product) => product.title);
console.log(titles); // ["책상 램프", "노트", "펜"]

const total = products.reduce((sum, product) => sum + product.price, 0);
console.log(total); // 49

콜백은 반복마다 객체 하나씩 전달받고, 여러분은 키로 그 필드를 읽습니다. 이 원리가 익숙해지면 "각 레코드마다 이 필드를 뽑아라"와 "모든 레코드에서 이 필드를 합산하라"가 한 줄짜리 코드가 됩니다.

객체 배열은 구조화된 데이터의 전형적인 모양이고, 배열 메서드가 그것을 다루는 방법입니다. 각 콜백은 레코드 하나를 전달받고, 여러분은 키로 그 필드를 읽어서 변환, 필터링, 집계합니다:

js
const orders = [
  { id: 1, total: 40, paid: true },
  { id: 2, total: 6, paid: false },
  { id: 3, total: 3, paid: true },
];

const paidTotal = orders
  .filter((order) => order.paid)
  .reduce((sum, order) => sum + order.total, 0);

console.log(paidTotal); // 43

메서드 체이닝은 위에서 아래로 파이프라인처럼 읽힙니다: 결제된 주문만 남기고, 그 총액을 합산합니다. 각 단계가 객체 배열을 받아서 다시 배열을(또는 reduce의 경우 단일 값을) 반환하므로 매끄럽게 조합됩니다.

데이터가 커질 때 기억해둘 점이 하나 있습니다: 이 객체들은 참조로 전달되는데, 이는 아래 advanced 섹션에서 정확히 다룹니다. map이나 forEach 콜백 안에서 객체를 변경하면 사본이 아니라 배열 안의 원본 레코드가 바뀌는데, 이는 때로는 의도한 동작이지만 종종 예상치 못한 결과를 낳습니다. 원본을 건드리지 않고 변환하려는 의도라면, 전달받은 객체를 수정하는 대신 새로운 객체를 만드세요.

Juno객체와 배열을 함께 쓰기 실제 데이터 대부분은 객체 배열입니다: 각 항목이 같은 키를 가진 하나의 레코드인 목록입니다. products[0]으로 하나에 접근하고, products[0].title로 필드에 접근합니다. 배열을 순회하면 모든 레코드를 처리할 수 있고, 이것이 여러분이 하게 될 데이터 작업 대부분의 패턴입니다.
Juno객체와 배열을 함께 쓰기 객체 배열은 실제 데이터가 도착하는 가장 흔한 모양이고, 배열 메서드는 바로 이를 위해 만들어졌습니다. map은 각 레코드에서 필드 하나를 뽑고, filter는 조건에 맞는 것만 남기고, reduce는 그것들을 합산하는데, 콜백은 매번 객체 하나씩 전달받습니다. "각 레코드마다 이 필드를 뽑아라"가 반복문이 아니라 한 줄짜리 코드가 됩니다.
Juno객체와 배열을 함께 쓰기 배열 메서드는 레코드에 대한 파이프라인으로 이어집니다: 걸러내고, 값으로 합산하며, 각 단계에서 키로 필드를 읽습니다. 이 객체들이 참조로 전달된다는 점을 주의하세요, 그래서 map이나 forEach 콜백 안에서 객체를 변경하면 원본이 그 자리에서 바뀝니다. 원본을 건드리지 않고 변환하려는 의도라면, 전달받은 객체를 수정하는 대신 새로운 객체를 만드세요.

다음으로 이어지는 객체

객체와 배열은 앞으로 계속 손이 가게 될 두 가지 구조이며, 여러분이 다루는 데이터 대부분은 이 둘이 어떤 형태로든 중첩된 모습입니다: 배열 필드를 가진 객체, 객체로 이루어진 배열, 그리고 이 둘의 조합입니다. 배열 메서드와 함께 쓰면 거의 어떤 데이터든 필요한 형태로 다듬을 수 있습니다.

다음 단계는 객체를 실제 페이지에서 활용하는 것입니다. DOM을 다루게 되면, 여러분이 읽고 바꾸는 요소들도 결국 객체이며, 키를 설정하고 메서드를 호출할 수 있으므로 여기서 배운 내용이 그대로 이어집니다.