데이터가 많아지는 순간, 코드의 모양이 달라진다
지금까지 우리는 값을 하나씩 다루었습니다. 이름 하나, 나이 하나, 로그인 여부 하나, 가격 하나. 그런데 실제 웹페이지는 그렇게 얌전하지 않습니다.
쇼핑몰에는 상품이 여러 개 있습니다. 블로그에는 글 목록이 있습니다. 할 일 앱에는 해야 할 일이 계속 추가됩니다. 검색 화면에는 수십 개의 결과가 나타납니다. 날씨 앱은 오늘 기온만 보여 주는 것이 아니라 시간대별 예보, 습도, 바람, 지역 정보까지 함께 다룹니다.
이런 데이터를 변수 하나하나로 만들면 금방 한계가 옵니다.
const productName1 = "무선 키보드";
const productName2 = "게이밍 마우스";
const productName3 = "USB-C 허브";
const productPrice1 = 45000;
const productPrice2 = 32000;
const productPrice3 = 28000;상품이 세 개일 때는 어떻게든 버틸 수 있습니다. 하지만 상품이 100개라면 어떨까요? 상품 이름뿐 아니라 가격, 재고, 할인 여부, 이미지 주소까지 필요하다면요?
이때 필요한 것이 배열과 객체입니다.
배열은 여러 값을 순서대로 담는 상자입니다. 객체는 하나의 대상을 여러 정보로 설명하는 상자입니다. 그리고 실제 웹앱에서는 이 둘을 함께 사용합니다. 예를 들어 "상품 여러 개"는 배열로 담고, "상품 하나의 정보"는 객체로 표현합니다.
const products = [
{
name: "무선 키보드",
price: 45000,
inStock: true
},
{
name: "게이밍 마우스",
price: 32000,
inStock: false
},
{
name: "USB-C 허브",
price: 28000,
inStock: true
}
];처음 보면 괄호가 많아 조금 복잡해 보일 수 있습니다. 하지만 겁먹을 필요는 없습니다. 이 장에서 하나씩 뜯어보면, 이 구조는 오히려 데이터를 훨씬 편하게 다루게 해 주는 강력한 도구라는 것을 알게 됩니다.

배열: 여러 값을 순서대로 담는 방법
배열은 여러 값을 하나의 변수에 담을 때 사용합니다. 대괄호 [] 안에 값을 넣고, 값과 값 사이는 쉼표로 구분합니다.
const fruits = ["사과", "바나나", "딸기"];이제 fruits라는 변수 하나가 세 개의 문자열을 가지고 있습니다. 배열 안에 들어 있는 각각의 값을 요소라고 부릅니다.
배열은 순서를 기억합니다. 첫 번째 값은 첫 번째 자리에, 두 번째 값은 두 번째 자리에 있습니다. 그런데 JavaScript에서 배열의 순서를 셀 때는 1부터가 아니라 0부터 시작합니다. 이 순서 번호를 인덱스라고 합니다.
const fruits = ["사과", "바나나", "딸기"];
console.log(fruits[0]); // 사과
console.log(fruits[1]); // 바나나
console.log(fruits[2]); // 딸기처음에는 fruits[0]이 첫 번째라는 점이 어색할 수 있습니다. 하지만 배열을 자주 쓰다 보면 금방 익숙해집니다. 웹 개발을 하다 보면 "첫 번째는 0번"이라는 생각이 자연스러워집니다.
배열의 길이는 length 속성으로 알 수 있습니다.
const fruits = ["사과", "바나나", "딸기"];
console.log(fruits.length); // 3여기서 주의할 점이 하나 있습니다. 배열의 마지막 요소 인덱스는 length보다 1 작습니다.
const fruits = ["사과", "바나나", "딸기"];
console.log(fruits.length); // 3
console.log(fruits[fruits.length - 1]); // 딸기배열 길이가 3이면 인덱스는 0, 1, 2입니다. 그래서 마지막 값에 접근하려면 fruits[3]이 아니라 fruits[2], 즉 fruits[fruits.length - 1]을 사용합니다.
배열에 값 추가하기와 꺼내기
배열은 한 번 만들고 끝나는 상자가 아닙니다. 필요에 따라 값을 추가하거나 제거할 수 있습니다. 쇼핑몰 장바구니를 떠올려 봅시다. 사용자가 상품을 담으면 배열에 상품이 추가되고, 상품을 삭제하면 배열에서 빠집니다.
가장 많이 사용하는 추가 메서드는 push입니다. 메서드는 어떤 값에 붙어서 동작하는 함수라고 생각하면 됩니다. push는 배열의 맨 뒤에 값을 추가합니다.
const cart = ["무선 키보드", "게이밍 마우스"];
cart.push("USB-C 허브");
console.log(cart); // ["무선 키보드", "게이밍 마우스", "USB-C 허브"]반대로 pop은 배열의 맨 뒤 값을 제거합니다. 제거한 값을 반환하기 때문에, 빠져나온 값을 따로 저장할 수도 있습니다.
const cart = ["무선 키보드", "게이밍 마우스", "USB-C 허브"];
const removedItem = cart.pop();
console.log(removedItem); // USB-C 허브
console.log(cart); // ["무선 키보드", "게이밍 마우스"]여기서 중요한 표현이 나옵니다. push와 pop은 원본 배열을 바꿉니다. 원본 배열을 바꾼다는 말은, 처음 만든 배열 자체의 내용이 달라진다는 뜻입니다.
const numbers = [1, 2, 3];
numbers.push(4);
console.log(numbers); // [1, 2, 3, 4]numbers라는 배열 자체가 [1, 2, 3, 4]로 바뀌었습니다. 이런 동작을 원본 변경 또는 mutation이라고 부릅니다. mutation은 "변형"이라는 뜻입니다.
원본이 바뀌는 것이 항상 나쁜 것은 아닙니다. 장바구니처럼 실제로 내용이 바뀌어야 하는 경우에는 자연스럽습니다. 다만 원본이 바뀌는지 아닌지를 모르면 예상하지 못한 버그가 생길 수 있습니다.
slice와 splice: 이름은 비슷하지만 성격은 다르다
배열 메서드 중에서 많은 초보자가 헷갈리는 것이 slice와 splice입니다. 이름이 거의 쌍둥이처럼 생겼지만, 성격은 꽤 다릅니다.
slice는 배열의 일부를 잘라서 새 배열로 반환합니다. 원본 배열은 바꾸지 않습니다.
const colors = ["빨강", "주황", "노랑", "초록", "파랑"];
const selectedColors = colors.slice(1, 4);
console.log(selectedColors); // ["주황", "노랑", "초록"]
console.log(colors); // ["빨강", "주황", "노랑", "초록", "파랑"]slice(1, 4)는 인덱스 1부터 시작해서 인덱스 4 앞까지 가져옵니다. 다시 말해 1, 2, 3번 요소를 가져오고 4번 요소는 포함하지 않습니다.
이번에는 splice를 보겠습니다. splice는 배열 중간의 요소를 삭제하거나, 그 자리에 새 요소를 추가할 수 있습니다. 그리고 원본 배열을 바꿉니다.
const colors = ["빨강", "주황", "노랑", "초록", "파랑"];
const removedColors = colors.splice(1, 2);
console.log(removedColors); // ["주황", "노랑"]
console.log(colors); // ["빨강", "초록", "파랑"]splice(1, 2)는 인덱스 1부터 시작해서 2개를 제거한다는 뜻입니다. 그래서 "주황"과 "노랑"이 제거됩니다.
splice는 삭제뿐 아니라 추가도 할 수 있습니다.
const colors = ["빨강", "초록", "파랑"];
colors.splice(1, 0, "노랑");
console.log(colors); // ["빨강", "노랑", "초록", "파랑"]splice(1, 0, "노랑")은 인덱스 1 위치에서 0개를 삭제하고, 그 자리에 "노랑"을 넣으라는 뜻입니다.
정리하면 이렇게 볼 수 있습니다.
| 메서드 | 하는 일 | 원본 배열 변경 여부 |
|---|---|---|
push | 맨 뒤에 요소 추가 | 변경함 |
pop | 맨 뒤 요소 제거 | 변경함 |
slice | 일부를 복사해 새 배열 반환 | 변경하지 않음 |
splice | 요소 삭제 또는 추가 | 변경함 |
실전에서는 이 차이가 매우 중요합니다. 특히 나중에 UI 화면을 다시 그리거나 상태를 관리할 때, 원본 데이터를 직접 바꿀지 새 데이터를 만들지 결정해야 하는 순간이 자주 옵니다.
배열을 반복해서 다루기
배열의 진짜 힘은 여러 값을 한 번에 처리할 수 있다는 데 있습니다. 앞 장에서 반복문을 배웠습니다. 배열과 반복문은 아주 잘 어울립니다.
const menus = ["홈", "상품", "문의", "로그인"];
for (let i = 0; i < menus.length; i++) {
console.log(menus[i]);
}이 코드는 배열의 처음부터 끝까지 돌면서 메뉴 이름을 하나씩 출력합니다. 그런데 JavaScript에는 배열을 다룰 때 자주 쓰는 더 편리한 메서드들이 있습니다.
대표적으로 map, filter, find, reduce가 있습니다. 처음에는 이름이 낯설 수 있지만, 각각의 역할은 꽤 직관적입니다.
| 메서드 | 핵심 역할 | 자주 쓰는 상황 |
|---|---|---|
map | 각 요소를 다른 형태로 바꾼다 | 데이터 목록을 화면용 문자열로 변환 |
filter | 조건에 맞는 요소만 남긴다 | 완료된 할 일만 보기, 품절 상품 제외 |
find | 조건에 맞는 첫 번째 요소를 찾는다 | 특정 id의 상품 찾기 |
reduce | 여러 값을 하나의 결과로 누적한다 | 총합, 평균, 통계 계산 |
이 네 가지 메서드는 실전 웹앱에서 정말 자주 등장합니다. 이름만 외우기보다 "어떤 문제를 해결할 때 쓰는가"를 중심으로 익히면 훨씬 쉽습니다.
map: 목록을 다른 모양으로 바꾸기
map은 배열의 각 요소를 하나씩 꺼내서, 새로운 값으로 바꾼 뒤 새 배열을 만듭니다. 원본 배열은 바꾸지 않습니다.
const prices = [10000, 20000, 30000];
const salePrices = prices.map(function (price) {
return price * 0.9;
});
console.log(salePrices); // [9000, 18000, 27000]
console.log(prices); // [10000, 20000, 30000]여기서 map 안에 들어간 함수는 배열의 요소마다 한 번씩 실행됩니다. 첫 번째 실행에서는 price가 10000, 두 번째 실행에서는 20000, 세 번째 실행에서는 30000이 됩니다.
앞 장에서 배운 화살표 함수를 사용하면 더 짧게 쓸 수 있습니다.
const prices = [10000, 20000, 30000];
const salePrices = prices.map((price) => price * 0.9);
console.log(salePrices); // [9000, 18000, 27000]map은 웹페이지에서 목록을 만들 때 특히 강력합니다. 예를 들어 메뉴 배열을 HTML 문자열 배열로 바꿀 수 있습니다.
const menus = ["홈", "상품", "문의", "로그인"];
const menuItems = menus.map((menu) => `<li>${menu}</li>`);
console.log(menuItems);
// ["<li>홈</li>", "<li>상품</li>", "<li>문의</li>", "<li>로그인</li>"]배열로 나뉜 HTML 문자열을 하나의 문자열로 합치고 싶다면 join을 사용할 수 있습니다.
const menus = ["홈", "상품", "문의", "로그인"];
const menuHTML = menus
.map((menu) => `<li>${menu}</li>`)
.join("");
console.log(menuHTML);
// <li>홈</li><li>상품</li><li>문의</li><li>로그인</li>아직 실제 화면에 넣지는 않았습니다. 다음 장에서 DOM을 배우면 이 문자열을 웹페이지 안에 넣어 화면으로 보여 줄 수 있습니다. 지금은 "배열 데이터를 화면에 넣기 좋은 형태로 바꿀 수 있다"는 감각을 잡으면 충분합니다.
filter: 필요한 데이터만 남기기
filter는 조건에 맞는 요소만 골라 새 배열을 만듭니다. 이름 그대로 거름망처럼 작동합니다.
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter((number) => number % 2 === 0);
console.log(evenNumbers); // [2, 4, 6]number % 2 === 0은 숫자를 2로 나눈 나머지가 0인지 확인하는 조건입니다. 즉 짝수만 남깁니다.
이번에는 상품 목록에서 재고가 있는 상품만 골라 보겠습니다.
const products = [
{ name: "무선 키보드", inStock: true },
{ name: "게이밍 마우스", inStock: false },
{ name: "USB-C 허브", inStock: true }
];
const availableProducts = products.filter((product) => product.inStock);
console.log(availableProducts);
// [
// { name: "무선 키보드", inStock: true },
// { name: "USB-C 허브", inStock: true }
// ]product.inStock이 true인 상품만 새 배열에 남았습니다. 원본 products는 그대로입니다.
검색 기능도 filter로 만들 수 있습니다.
const products = [
{ name: "무선 키보드" },
{ name: "게이밍 마우스" },
{ name: "USB-C 허브" },
{ name: "블루투스 스피커" }
];
const keyword = "무선";
const searchResults = products.filter((product) => {
return product.name.includes(keyword);
});
console.log(searchResults);
// [{ name: "무선 키보드" }]includes는 문자열 안에 특정 글자가 포함되어 있는지 확인합니다. 검색어가 상품명에 들어 있으면 true, 없으면 false를 반환합니다.
find: 딱 하나를 찾기
filter가 조건에 맞는 모든 요소를 배열로 반환한다면, find는 조건에 맞는 첫 번째 요소 하나를 반환합니다.
const users = [
{ id: 1, name: "민준" },
{ id: 2, name: "서연" },
{ id: 3, name: "도윤" }
];
const user = users.find((user) => user.id === 2);
console.log(user); // { id: 2, name: "서연" }find는 "id가 2인 사용자를 찾아줘"처럼 특정 대상을 찾을 때 자주 사용합니다. 웹앱에서는 데이터를 구분하기 위해 id를 많이 씁니다. 이름은 같을 수 있지만 id는 보통 고유하게 관리되기 때문입니다.
만약 조건에 맞는 요소가 없으면 undefined가 반환됩니다.
const users = [
{ id: 1, name: "민준" },
{ id: 2, name: "서연" }
];
const user = users.find((user) => user.id === 99);
console.log(user); // undefined따라서 find를 쓸 때는 결과가 없을 수도 있다는 점을 생각해야 합니다.
const user = users.find((user) => user.id === 99);
if (user) {
console.log(`${user.name}님을 찾았습니다.`);
} else {
console.log("사용자를 찾을 수 없습니다.");
}앞 장에서 배운 조건문이 여기서 자연스럽게 연결됩니다. 배열 메서드는 혼자 쓰이는 것이 아니라 조건문, 함수, 객체와 함께 움직입니다.
reduce: 여러 값을 하나로 모으기
reduce는 처음 보면 조금 어렵게 느껴지는 메서드입니다. 하지만 역할은 단순합니다. 배열의 여러 값을 하나의 결과로 누적합니다.
가장 쉬운 예는 숫자 합계입니다.
const numbers = [10, 20, 30];
const total = numbers.reduce((sum, number) => {
return sum + number;
}, 0);
console.log(total); // 60여기서 sum은 지금까지 누적된 값입니다. number는 배열에서 현재 꺼낸 값입니다. 마지막의 0은 시작값입니다.
흐름을 천천히 보면 이렇습니다.
| 순서 | sum | number | return 값 |
|---|---|---|---|
| 시작 | 0 | - | 0 |
| 1회 | 0 | 10 | 10 |
| 2회 | 10 | 20 | 30 |
| 3회 | 30 | 30 | 60 |
결국 최종 결과는 60이 됩니다.
상품 가격 총합도 같은 방식으로 구할 수 있습니다.
const cartItems = [
{ name: "무선 키보드", price: 45000 },
{ name: "게이밍 마우스", price: 32000 },
{ name: "USB-C 허브", price: 28000 }
];
const totalPrice = cartItems.reduce((total, item) => {
return total + item.price;
}, 0);
console.log(totalPrice); // 105000reduce는 합계뿐 아니라 평균, 개수 세기, 그룹 만들기 같은 작업에도 사용할 수 있습니다. 다만 처음부터 너무 복잡한 reduce를 만들려고 하면 코드가 읽기 어려워질 수 있습니다. 초반에는 "배열을 하나의 값으로 접는다" 정도로 이해하면 충분합니다.
객체: 하나의 대상을 여러 정보로 표현하기
배열이 여러 값을 순서대로 담는 상자라면, 객체는 하나의 대상을 여러 정보로 설명하는 상자입니다.
상품 하나를 생각해 봅시다. 상품에는 이름, 가격, 재고 여부, 카테고리 같은 정보가 있습니다. 이것을 각각 따로 변수로 만들 수도 있습니다.
const productName = "무선 키보드";
const productPrice = 45000;
const productInStock = true;
const productCategory = "전자기기";하지만 이 변수들이 모두 하나의 상품을 설명한다는 관계가 코드에서 잘 드러나지 않습니다. 객체를 사용하면 이 정보를 하나로 묶을 수 있습니다.
const product = {
name: "무선 키보드",
price: 45000,
inStock: true,
category: "전자기기"
};객체는 중괄호 {}로 만듭니다. 객체 안에는 key: value 형태로 값을 넣습니다. 여기서 name, price, inStock, category 같은 이름을 속성이라고 부릅니다. 영어로는 property라고 합니다.
객체의 속성에 접근할 때는 점 표기법을 주로 사용합니다.
console.log(product.name); // 무선 키보드
console.log(product.price); // 45000대괄호 표기법도 사용할 수 있습니다.
console.log(product["name"]); // 무선 키보드
console.log(product["price"]); // 45000대부분의 경우에는 점 표기법이 읽기 쉽습니다. 하지만 속성 이름을 변수로 다뤄야 할 때는 대괄호 표기법이 필요합니다.
const key = "price";
console.log(product[key]); // 45000객체의 속성 값은 바꿀 수 있습니다.
const product = {
name: "무선 키보드",
price: 45000,
inStock: true
};
product.price = 39000;
product.inStock = false;
console.log(product);
// { name: "무선 키보드", price: 39000, inStock: false }const로 선언했는데 값이 바뀌는 것이 이상하게 느껴질 수 있습니다. 여기서 중요한 점은 const가 막는 것은 변수에 완전히 다른 값을 다시 넣는 일입니다. 객체 내부의 속성을 바꾸는 것은 가능합니다.
const product = {
name: "무선 키보드",
price: 45000
};
product.price = 39000; // 가능
// product = { name: "게이밍 마우스", price: 32000 }; // 불가능객체와 배열은 내부 값을 바꿀 수 있기 때문에, 원본 데이터가 바뀌는 순간을 잘 의식해야 합니다.
객체 안의 함수, 메서드
객체의 속성 값에는 문자열, 숫자, 불리언뿐 아니라 함수도 들어갈 수 있습니다. 객체 안에 들어 있는 함수를 메서드라고 부릅니다.
const user = {
name: "윤서",
age: 28,
sayHello: function () {
console.log("안녕하세요!");
}
};
user.sayHello(); // 안녕하세요!메서드는 객체가 할 수 있는 동작을 표현할 때 사용합니다. 예를 들어 상품 객체가 자신의 가격을 보기 좋게 출력하게 만들 수 있습니다.
const product = {
name: "무선 키보드",
price: 45000,
getPriceText: function () {
return `${this.price.toLocaleString()}원`;
}
};
console.log(product.getPriceText()); // 45,000원여기서 this는 현재 객체를 가리킵니다. 즉 this.price는 product.price와 비슷하게 동작합니다. 다만 this는 상황에 따라 의미가 달라질 수 있기 때문에, 처음에는 "객체 메서드 안에서 자기 자신의 속성을 가리킬 때 사용할 수 있다" 정도로 이해하면 됩니다.
웹앱 데이터를 만들 때 모든 객체에 메서드를 넣는 경우가 많지는 않습니다. 실제로는 데이터는 데이터대로 두고, 데이터를 처리하는 함수는 따로 만드는 방식도 많이 씁니다.
const product = {
name: "무선 키보드",
price: 45000
};
function getPriceText(product) {
return `${product.price.toLocaleString()}원`;
}
console.log(getPriceText(product)); // 45,000원앞 장에서 배운 함수가 다시 등장했습니다. 객체는 데이터를 담고, 함수는 그 데이터를 처리합니다. 이 둘을 적절히 나누면 코드가 훨씬 읽기 쉬워집니다.
배열 안의 객체: 웹앱 데이터의 기본 모양
실제 웹앱에서 가장 많이 만나는 구조는 배열 안에 객체가 들어 있는 형태입니다.
const todos = [
{
id: 1,
text: "JavaScript 문법 복습하기",
done: false
},
{
id: 2,
text: "배열 메서드 연습하기",
done: true
},
{
id: 3,
text: "DOM 예습하기",
done: false
}
];이 구조를 말로 풀면 이렇습니다. todos는 할 일 목록입니다. 목록이므로 배열입니다. 배열 안의 각 요소는 할 일 하나입니다. 할 일 하나에는 id, 내용, 완료 여부가 있습니다. 그래서 객체입니다.
이 구조를 사용하면 목록 데이터를 다루기 쉬워집니다.
완료되지 않은 할 일만 보고 싶다면 filter를 사용합니다.
const activeTodos = todos.filter((todo) => !todo.done);
console.log(activeTodos);할 일 문장만 뽑고 싶다면 map을 사용합니다.
const todoTexts = todos.map((todo) => todo.text);
console.log(todoTexts);
// ["JavaScript 문법 복습하기", "배열 메서드 연습하기", "DOM 예습하기"]id가 2인 할 일을 찾고 싶다면 find를 사용합니다.
const targetTodo = todos.find((todo) => todo.id === 2);
console.log(targetTodo);
// { id: 2, text: "배열 메서드 연습하기", done: true }완료된 할 일이 몇 개인지 세고 싶다면 reduce를 사용할 수 있습니다.
const doneCount = todos.reduce((count, todo) => {
if (todo.done) {
return count + 1;
}
return count;
}, 0);
console.log(doneCount); // 1물론 이 경우에는 filter와 length를 함께 쓰는 편이 더 간단할 수도 있습니다.
const doneCount = todos.filter((todo) => todo.done).length;
console.log(doneCount); // 1좋은 코드는 무조건 어려운 메서드를 쓰는 코드가 아닙니다. 읽는 사람이 쉽게 이해할 수 있는 코드가 좋은 코드입니다.
데이터에 id가 필요한 이유
배열 안의 객체를 만들 때 id를 자주 넣습니다. 처음에는 굳이 필요 없어 보일 수 있습니다. 배열에는 인덱스가 있으니까요.
const todos = [
{ text: "문법 복습하기", done: false },
{ text: "배열 연습하기", done: true }
];
console.log(todos[0]);하지만 인덱스는 배열의 순서가 바뀌면 달라집니다. 할 일을 삭제하거나 정렬하면 같은 할 일이 더 이상 같은 인덱스에 있지 않을 수 있습니다.
const todos = [
{ id: 1, text: "문법 복습하기", done: false },
{ id: 2, text: "배열 연습하기", done: true }
];id는 데이터의 이름표와 같습니다. 순서가 바뀌어도 id가 2인 할 일은 여전히 id가 2인 할 일입니다. 웹앱에서는 특정 데이터를 수정하거나 삭제할 때 id를 기준으로 찾는 경우가 많습니다.
const targetId = 2;
const updatedTodos = todos.map((todo) => {
if (todo.id === targetId) {
return {
...todo,
done: !todo.done
};
}
return todo;
});
console.log(updatedTodos);여기서 ...todo는 전개 구문입니다. 객체 안의 기존 속성을 펼쳐서 새 객체에 복사합니다. 그리고 done: !todo.done으로 완료 여부만 반대로 바꿉니다.
아직 낯설다면 이렇게 읽으면 됩니다.
return {
id: todo.id,
text: todo.text,
done: !todo.done
};전개 구문을 사용하면 기존 객체를 직접 바꾸지 않고 새 객체를 만들기 편합니다. 나중에 상태 관리와 렌더링 함수를 배울 때 이 방식이 다시 중요해집니다.
원본을 바꿀 것인가, 새 배열을 만들 것인가
배열과 객체를 다루다 보면 중요한 선택을 자주 하게 됩니다.
"기존 데이터를 직접 바꿀까?"
"아니면 새 데이터를 만들어 반환할까?"
예를 들어 할 일을 추가할 때는 push를 사용할 수 있습니다.
const todos = [
{ id: 1, text: "문법 복습하기", done: false }
];
todos.push({ id: 2, text: "배열 연습하기", done: false });
console.log(todos);이 코드는 원본 배열을 직접 바꿉니다. 단순한 상황에서는 충분히 괜찮습니다.
하지만 원본을 바꾸지 않고 새 배열을 만들 수도 있습니다.
const todos = [
{ id: 1, text: "문법 복습하기", done: false }
];
const newTodo = { id: 2, text: "배열 연습하기", done: false };
const nextTodos = [...todos, newTodo];
console.log(nextTodos);
console.log(todos);[...todos, newTodo]는 기존 배열 요소를 펼쳐 넣고, 마지막에 새 할 일을 추가한 새 배열을 만듭니다.
삭제할 때도 마찬가지입니다. splice로 원본 배열을 직접 바꿀 수 있습니다.
const todos = [
{ id: 1, text: "문법 복습하기", done: false },
{ id: 2, text: "배열 연습하기", done: false }
];
todos.splice(0, 1);
console.log(todos);또는 filter로 삭제할 대상을 제외한 새 배열을 만들 수 있습니다.
const todos = [
{ id: 1, text: "문법 복습하기", done: false },
{ id: 2, text: "배열 연습하기", done: false }
];
const targetId = 1;
const nextTodos = todos.filter((todo) => todo.id !== targetId);
console.log(nextTodos);어느 쪽이 정답이라고 단정할 수는 없습니다. 중요한 것은 내가 작성한 코드가 원본을 바꾸는지, 새 값을 만드는지 알고 쓰는 것입니다. 이 감각이 생기면 디버깅이 훨씬 쉬워집니다.
JSON: 데이터를 주고받기 위한 약속
웹앱은 혼자 모든 데이터를 가지고 있지 않습니다. 서버에서 상품 목록을 받아오거나, 외부 API에서 날씨 정보를 가져오기도 합니다. 이때 데이터를 주고받는 형식으로 자주 쓰이는 것이 JSON입니다.
JSON은 JavaScript Object Notation의 줄임말입니다. 이름에 JavaScript가 들어 있지만, JavaScript만의 전용 형식은 아닙니다. 여러 프로그래밍 언어에서 데이터를 주고받기 위한 공통 형식으로 널리 사용됩니다.
JSON은 JavaScript 객체와 닮았습니다.
const user = {
name: "민지",
age: 25,
isMember: true
};JSON 문자열은 이렇게 생겼습니다.
{
"name": "민지",
"age": 25,
"isMember": true
}비슷하지만 중요한 차이가 있습니다. JSON에서는 속성 이름을 반드시 큰따옴표로 감싸야 합니다. 문자열도 큰따옴표를 사용합니다. 함수나 undefined 같은 값은 JSON에 넣을 수 없습니다.
JavaScript 객체를 JSON 문자열로 바꿀 때는 JSON.stringify를 사용합니다.
const user = {
name: "민지",
age: 25,
isMember: true
};
const jsonText = JSON.stringify(user);
console.log(jsonText);
// {"name":"민지","age":25,"isMember":true}반대로 JSON 문자열을 JavaScript 객체로 바꿀 때는 JSON.parse를 사용합니다.
const jsonText = '{"name":"민지","age":25,"isMember":true}';
const user = JSON.parse(jsonText);
console.log(user.name); // 민지나중에 Fetch API로 서버에서 데이터를 가져오면, 응답 데이터를 JavaScript에서 다룰 수 있는 객체나 배열로 바꾸는 과정이 등장합니다. 그때 JSON을 다시 만나게 됩니다.
실전 데이터 설계: 상품 목록 만들기
이제 웹페이지에 표시할 상품 목록 데이터를 직접 설계해 봅시다. 목표는 단순합니다. 상품 카드 UI를 만들기 좋은 데이터 구조를 준비하는 것입니다.
상품 카드에 필요한 정보가 무엇인지 먼저 생각해 보겠습니다. 화면에 상품을 보여 주려면 이름과 가격은 필요합니다. 할인 중인지도 보여 줄 수 있습니다. 품절이면 버튼을 비활성화해야 할 수도 있습니다. 카테고리로 필터링할 수도 있습니다.
const products = [
{
id: 1,
name: "무선 키보드",
price: 45000,
category: "keyboard",
imageUrl: "keyboard.jpg",
inStock: true,
isOnSale: true
},
{
id: 2,
name: "게이밍 마우스",
price: 32000,
category: "mouse",
imageUrl: "mouse.jpg",
inStock: false,
isOnSale: false
},
{
id: 3,
name: "USB-C 허브",
price: 28000,
category: "accessory",
imageUrl: "hub.jpg",
inStock: true,
isOnSale: false
},
{
id: 4,
name: "노트북 거치대",
price: 36000,
category: "accessory",
imageUrl: "stand.jpg",
inStock: true,
isOnSale: true
}
];이 데이터는 배열 안에 상품 객체가 들어 있는 구조입니다. 각 상품은 같은 속성 모양을 가지고 있습니다. 이렇게 모양이 일정하면 코드를 작성하기 쉽습니다.
예를 들어 상품 이름만 뽑을 수 있습니다.
const productNames = products.map((product) => product.name);
console.log(productNames);
// ["무선 키보드", "게이밍 마우스", "USB-C 허브", "노트북 거치대"]재고가 있는 상품만 걸러낼 수도 있습니다.
const availableProducts = products.filter((product) => product.inStock);
console.log(availableProducts);할인 중인 상품만 찾을 수도 있습니다.
const saleProducts = products.filter((product) => product.isOnSale);
console.log(saleProducts);특정 카테고리의 상품만 보여 줄 수도 있습니다.
const accessories = products.filter((product) => {
return product.category === "accessory";
});
console.log(accessories);특정 id를 가진 상품 하나를 찾을 수도 있습니다.
const selectedProduct = products.find((product) => product.id === 3);
console.log(selectedProduct);
// { id: 3, name: "USB-C 허브", ... }장바구니에 담긴 상품의 총액을 계산하는 흐름도 자연스럽게 이어집니다.
const cartItems = [
{ id: 1, name: "무선 키보드", price: 45000, quantity: 1 },
{ id: 3, name: "USB-C 허브", price: 28000, quantity: 2 }
];
const totalPrice = cartItems.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
console.log(totalPrice); // 101000여기서 quantity는 수량입니다. price * quantity를 더하면 각 상품의 금액이 총합에 누적됩니다.
화면에 넣기 좋은 형태로 바꾸기
아직 DOM을 직접 조작하지는 않지만, 상품 데이터를 HTML 문자열로 변환해 볼 수 있습니다. 이것은 다음 장에서 배울 화면 렌더링의 예고편 같은 코드입니다.
function getPriceText(price) {
return `${price.toLocaleString()}원`;
}
function createProductCard(product) {
const stockText = product.inStock ? "구매 가능" : "품절";
const saleBadge = product.isOnSale ? "<span>SALE</span>" : "";
return `
<article class="product-card">
<img src="${product.imageUrl}" alt="${product.name}">
<h3>${product.name}</h3>
<p>${getPriceText(product.price)}</p>
<p>${stockText}</p>
${saleBadge}
</article>
`;
}
const productHTML = products
.map((product) => createProductCard(product))
.join("");
console.log(productHTML);이 코드에는 지금까지 배운 내용이 여럿 섞여 있습니다. 배열 products가 있고, 상품 하나는 객체입니다. map으로 상품 하나하나를 HTML 카드 문자열로 바꿉니다. createProductCard 함수는 상품 객체를 인수로 받아 문자열을 반환합니다. join("")은 여러 문자열을 하나로 합칩니다.
여기서 함수에 전달하는 실제 값이 바로 인수입니다. createProductCard(product)에서 product가 인수입니다. 함수 안에서는 이 값을 매개변수로 받아 사용합니다. 그리고 return을 통해 완성된 HTML 문자열을 밖으로 돌려줍니다.
이제 데이터가 화면으로 이어지는 길이 보이기 시작합니다. 데이터가 잘 정리되어 있으면 화면을 만드는 코드도 자연스럽게 정리됩니다.
데이터 구조를 설계할 때 생각할 것
배열과 객체를 배웠다고 해서 아무렇게나 중첩해서 쓰면 코드가 복잡해집니다. 좋은 데이터 구조는 나중에 코드를 읽는 사람에게 친절합니다. 물론 그 "나중의 사람"은 대개 미래의 나 자신입니다.
상품 목록 데이터를 설계할 때는 다음을 생각해 볼 수 있습니다.
이 데이터는 하나인가, 여러 개인가?
각 항목을 구분할 고유한 id가 필요한가?
화면에 바로 표시할 값과 계산해서 만들 값은 무엇인가?
검색, 필터, 정렬에 필요한 속성은 무엇인가?
나중에 서버에서 JSON으로 받아와도 자연스러운 모양인가?
예를 들어 상품 가격을 처음부터 "45,000원"이라는 문자열로 저장할 수도 있습니다.
const product = {
name: "무선 키보드",
price: "45,000원"
};하지만 이렇게 저장하면 계산이 어렵습니다. 장바구니 총액을 구하려면 문자열에서 쉼표와 원을 제거해야 합니다. 그래서 가격은 숫자로 저장하고, 화면에 보여 줄 때만 문자열로 바꾸는 편이 좋습니다.
const product = {
name: "무선 키보드",
price: 45000
};
function getPriceText(price) {
return `${price.toLocaleString()}원`;
}
console.log(getPriceText(product.price)); // 45,000원데이터는 가능한 한 계산하기 좋은 형태로 보관하고, 화면에 보여 줄 때 보기 좋은 형태로 바꾸는 것이 좋습니다.
작은 실습: 상품 목록 다듬기
다음 상품 목록이 있다고 해 봅시다.
const products = [
{ id: 1, name: "무선 키보드", price: 45000, category: "keyboard", inStock: true },
{ id: 2, name: "기계식 키보드", price: 89000, category: "keyboard", inStock: false },
{ id: 3, name: "게이밍 마우스", price: 32000, category: "mouse", inStock: true },
{ id: 4, name: "마우스 패드", price: 12000, category: "mouse", inStock: true },
{ id: 5, name: "USB-C 허브", price: 28000, category: "accessory", inStock: false }
];재고가 있는 상품만 골라 봅시다.
const availableProducts = products.filter((product) => product.inStock);
console.log(availableProducts);키보드 카테고리 상품만 골라 봅시다.
const keyboardProducts = products.filter((product) => {
return product.category === "keyboard";
});
console.log(keyboardProducts);각 상품 이름 앞에 카테고리를 붙인 새 배열을 만들어 봅시다.
const productLabels = products.map((product) => {
return `[${product.category}] ${product.name}`;
});
console.log(productLabels);재고가 있는 상품들의 총 가격을 구해 봅시다.
const availableTotalPrice = products
.filter((product) => product.inStock)
.reduce((total, product) => {
return total + product.price;
}, 0);
console.log(availableTotalPrice); // 89000이 코드는 filter와 reduce를 이어서 사용했습니다. 먼저 재고가 있는 상품만 남기고, 그 결과 배열에서 가격을 더합니다. 이렇게 메서드를 이어 붙이는 방식을 메서드 체이닝이라고 부릅니다. 체인은 사슬이라는 뜻입니다. 결과를 다음 메서드로 넘기며 사슬처럼 연결한다고 생각하면 됩니다.
코드가 너무 길어지면 중간 결과를 변수에 담아도 됩니다.
const availableProducts = products.filter((product) => product.inStock);
const availableTotalPrice = availableProducts.reduce((total, product) => {
return total + product.price;
}, 0);
console.log(availableTotalPrice); // 89000한 줄로 멋지게 쓰는 것보다 중요한 것은 읽기 쉬운 코드입니다.
흔한 실수와 디버깅 감각
배열과 객체를 다룰 때 자주 만나는 실수가 있습니다. 먼저 배열 인덱스를 착각하는 경우입니다.
const fruits = ["사과", "바나나", "딸기"];
console.log(fruits[3]); // undefined배열 길이는 3이지만 마지막 인덱스는 2입니다. 그래서 fruits[3]은 존재하지 않는 값을 가리키고, 결과는 undefined입니다.
객체 속성 이름을 잘못 쓰는 경우도 많습니다.
const product = {
name: "무선 키보드",
price: 45000
};
console.log(product.prcie); // undefinedprice를 prcie로 잘못 썼습니다. JavaScript는 "그런 속성이 없으니 undefined야"라고 조용히 알려 줄 뿐입니다. 오타를 찾기 어려울 때는 객체 전체를 console.log(product)로 찍어 보는 것이 도움이 됩니다.
map과 forEach를 헷갈리는 경우도 있습니다. map은 새 배열을 반환합니다. 반면 forEach는 각 요소에 대해 동작을 실행하지만 새 배열을 만들지는 않습니다.
const numbers = [1, 2, 3];
const doubled = numbers.map((number) => number * 2);
console.log(doubled); // [2, 4, 6]const numbers = [1, 2, 3];
const result = numbers.forEach((number) => {
return number * 2;
});
console.log(result); // undefined새 배열이 필요하면 map을 사용하세요. 단순히 반복 실행만 필요하면 forEach를 사용할 수 있습니다. 이 책에서는 목록 데이터를 바꾸는 흐름을 익히기 위해 map, filter, find, reduce를 중심으로 다룹니다.
배열과 객체가 연결하는 다음 세계
배열과 객체는 JavaScript의 중간 지점이 아닙니다. 사실상 웹앱으로 들어가는 문입니다.
버튼 클릭 이벤트가 발생하면 배열에 새 할 일이 추가됩니다. 입력창에 검색어를 입력하면 객체 배열에서 조건에 맞는 항목만 필터링합니다. 서버에서 JSON 데이터를 받아오면 배열과 객체로 변환해서 화면에 렌더링합니다. 장바구니 총액은 reduce로 계산하고, 특정 상품 상세 페이지는 find로 데이터를 찾습니다.
이제 우리는 단일 값만 다루는 단계에서 벗어났습니다. 여러 데이터를 모으고, 찾고, 걸러내고, 바꾸고, 합치는 방법을 배웠습니다.
다음 장에서는 이 데이터를 실제 웹페이지 위에 올려놓습니다. 지금까지 콘솔에 출력하던 상품 이름과 할 일 목록이 브라우저 화면 안에서 진짜 요소로 나타나기 시작합니다. JavaScript가 웹페이지를 움직이는 언어라는 말이 조금씩 현실이 됩니다.
정리
배열은 여러 값을 순서대로 저장하는 자료구조입니다. 인덱스는 0부터 시작하고, length로 길이를 확인할 수 있습니다. push와 pop은 배열의 맨 뒤에서 값을 추가하거나 제거하며 원본 배열을 바꿉니다. slice는 일부를 복사해 새 배열을 만들고, splice는 원본 배열에서 요소를 삭제하거나 추가합니다.
map, filter, find, reduce는 목록 데이터를 다룰 때 강력한 도구입니다. map은 요소를 다른 형태로 바꾸고, filter는 조건에 맞는 요소만 남기며, find는 조건에 맞는 첫 번째 요소를 찾습니다. reduce는 여러 값을 하나의 결과로 누적합니다.
객체는 하나의 대상을 여러 속성으로 표현합니다. 상품, 사용자, 글, 할 일처럼 실제 웹앱의 핵심 데이터는 대부분 객체로 표현할 수 있습니다. 배열 안에 객체를 담으면 상품 목록이나 할 일 목록처럼 여러 개의 구조화된 데이터를 다루기 좋아집니다.
JSON은 데이터를 주고받기 위한 문자열 형식입니다. JavaScript 객체와 비슷하지만 규칙이 더 엄격합니다. 나중에 서버나 외부 API에서 데이터를 가져올 때 JSON을 자주 만나게 됩니다.
배열과 객체를 잘 다룬다는 것은 웹앱의 재료를 잘 정리할 줄 안다는 뜻입니다. 재료가 잘 정리되어 있으면, 이제 화면을 만드는 일도 훨씬 쉬워집니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
