주요 포인트 한눈에 보기
배열(Array)은 장바구니, 검색 결과, 태그 목록처럼 “화면에 반복 렌더링되는 목록”의 출발점입니다. 이 문서는 배열을 만들고(생성), 원본을 지키고(복사), 필요한 것만 골라 쓰는(메서드) 흐름을 예제로 빠르게 정리합니다. reduce()는 활용 패턴이 많아 별도 글로 분리했으니, 아래 링크 섹션을 참고해 주세요.
- 배열 개념과 특징
- 배열 생성·복사·구조 분해
- 추가·삭제·수정: 원본 변경 메서드
- 순회·검색·검증 메서드
- 변환·가공 메서드
- 정렬·역순·부분 변경: 비파괴 메서드
- 배열 메서드 정리표
- reduce 알아보기
- 결론
- FAQ (면접 질문)
배열 개념과 특징
배열은 “순서가 있는 목록”입니다. 인덱스와 length만 익히면, 대부분의 배열 코드를 읽기 시작할 수 있습니다.
const cartItems = ['apple', 'banana', 'peach'];
console.log(cartItems[0]); // 'apple' (첫 번째)
console.log(cartItems[cartItems.length - 1]); // 'peach' (마지막)
console.log(cartItems.length); // 3 (총 개수)
동작 설명
배열은 “번호가 붙은 목록”입니다. cartItems[0]처럼 번호(인덱스)로 바로 꺼낼 수 있고, length로는 “몇 개가 들어있는지”를 확인합니다.
초보자를 위한 포인트!
배열 생성·복사·구조 분해
이 파트는 어렵게 외우지 않으셔도 됩니다. “만들기 → 복사하기 → 꺼내기”만 예시로 바로 확인하시면 됩니다.
배열 만들기
const arr = [10, 20, 30];
console.log(arr);
[]로 만들면 끝입니다. 값이 순서대로 들어가고, 그 순서대로 꺼내 쓰는 “목록”이 됩니다.
배열 복사 (원본 보호)
const origin = [1, 2, 3];
const alias = origin; // 복사 아님(같은 배열 공유)
const copy = [...origin]; // 새 배열(복사)
alias.push(4);
console.log(origin); // [1, 2, 3, 4]
console.log(copy); // [1, 2, 3]
alias = origin은 “복사”가 아니라 “같은 배열을 같이 보는 것”입니다. 원본을 지키려면 [...origin]처럼 새 배열을 만들어 작업하시면 됩니다.
객체가 들어있을 때 주의
const users = [{ id: 1 }, { id: 2 }];
const copied = [...users];
copied[0].id = 999;
console.log(users[0].id); // 999
배열은 새로 만들었지만, 안의 객체는 공유될 수 있습니다. 그래서 복사본에서 객체 속성을 바꾸면 원본 쪽에도 영향이 갈 수 있습니다.
Array.from으로 배열로 변환
function demo() {
const realArray = Array.from(arguments);
return realArray.map((v) => String(v));
}
console.log(demo(1, 2, 3)); // ['1','2','3']
arguments처럼 배열처럼 보이지만 배열 메서드 사용이 애매한 값이 있습니다. 이럴 땐 Array.from()으로 배열로 바꾼 다음 쓰시면 됩니다.
구조 분해로 값 꺼내기
const point = [10, 20];
const [x, y] = point;
console.log(x); // 10
console.log(y); // 20
인덱스를 여러 번 쓰지 않고도, 필요한 값만 변수로 바로 꺼낼 수 있습니다.
추가·삭제·수정: 원본 변경 메서드
이 파트의 메서드는 배열 “원본”을 직접 바꿉니다. 빠르고 편하지만, 같은 배열을 여러 곳에서 공유하면 예상치 못한 결과가 생길 수 있어 주의가 필요합니다.
push / pop (끝에 추가·삭제)
const cart = ['apple'];
cart.push('banana');
cart.pop();
console.log(cart); // ['apple']
이 메서드들은 “원본 배열을 직접 바꿉니다”. 로컬 임시 배열을 만들 때는 편하지만, 공유 데이터에는 조심하셔야 합니다.
splice (중간 삭제/삽입)
const list = ['a', 'b', 'c', 'd'];
list.splice(1, 2, 'x');
console.log(list); // ['a', 'x', 'd']
splice는 중간을 잘라내거나 바꾸는 데 강력하지만, 원본이 바뀌기 때문에 “원본 유지가 필요한 곳”에서는 주의가 필요합니다.
sort (원본 변경) + 안전한 정렬
const scores = [3, 1, 2];
const sorted1 = [...scores].sort((a, b) => a - b);
console.log(scores); // [3, 1, 2]
console.log(sorted1); // [1, 2, 3]
sort()는 원본을 바꿉니다. 그래서 안전하게 정렬하려면 [...scores]로 복사 후 정렬하는 방식이 가장 흔합니다.
순회·검색·검증 메서드
배열에서 “찾기/확인하기”를 담당하는 메서드들입니다. 무엇이 있는지 확인하거나, 조건을 만족하는 값을 찾고, 필요하면 중간에 멈추는 패턴까지 함께 익힙니다.
includes / indexOf (있나? 어디 있나?)
const tags = ['js', 'ts', 'react'];
console.log(tags.includes('ts')); // true
console.log(tags.indexOf('react')); // 2
console.log(tags.indexOf('next')); // -1
includes는 “있는지”만 필요할 때 가장 깔끔합니다. indexOf는 “몇 번째인지”가 필요할 때 씁니다. 없으면 -1이 나옵니다.
find / findIndex (첫 번째로 찾기)
const users = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
];
const u = users.find((x) => x.id === 2);
const idx = users.findIndex((x) => x.id === 2);
console.log(u); // { id: 2, name: 'B' }
console.log(idx); // 1
find는 “요소 자체”를, findIndex는 “위치(인덱스)”를 반환합니다. 찾자마자 멈추기 때문에 “첫 번째만 필요할 때” 좋습니다.
forEach에는 break가 없습니다
const nums = [1, 2, 3, 4];
nums.forEach((n) => {
if (n === 3) return; // 반복 중단이 아님 (그냥 이번 콜백만 종료)
console.log(n);
});
forEach에는 반복을 종료하는 break 문법이 없습니다. “찾으면 멈추기”가 필요하면 아래처럼 some 또는 find를 쓰는 편이 안전합니다.
some으로 “찾으면 멈추기”
const nums = [1, 2, 3, 4];
nums.some((n) => {
if (n === 3) return true; // true면 즉시 중단
console.log(n);
return false;
});
some은 콜백이 true를 반환하는 순간 즉시 멈춥니다. “조건을 만족하는 게 하나라도 있나요?” 같은 질문에 특히 잘 맞습니다.
변환·가공 메서드
원본은 그대로 두고, 새 배열을 만들어 “모양을 바꾸거나(map)” “필요한 것만 남기는(filter)” 작업을 합니다. 화면에 뿌릴 데이터를 만들 때 가장 자주 쓰입니다.
map / filter (변환 vs 추출)
const nums = [1, 2, 3, 4];
const doubled = nums.map((n) => n * 2);
const evens = nums.filter((n) => n % 2 === 0);
console.log(doubled); // [2, 4, 6, 8]
console.log(evens); // [2, 4]
map은 “모양 바꾸기(변환)”, filter는 “조건에 맞는 것만 남기기(추출)”입니다. 둘 다 새 배열을 반환하며 메서드 자체는 원본 배열을 변경하지 않습니다. 다만 콜백에서 원본이나 요소 객체를 직접 바꾸면 변경이 남습니다. filter에 남은 객체는 원본과 같은 객체입니다.
flat / flatMap (중첩을 펴기)
const nested = [[1, 2], [3, 4]];
console.log(nested.flat()); // [1, 2, 3, 4]
const words = ['hi', 'ok'];
console.log(words.flatMap((w) => w.split(''))); // ['h','i','o','k']
flat은 중첩 배열을 펴고, flatMap은 “map 후 깊이 1의 flat”을 한 번에 처리합니다. 더 깊은 중첩까지 모두 펴지는 것은 아닙니다.
오류 예제: map에서 push 쓰기
const nums = [1, 2, 3];
const out = [];
nums.map((n) => {
out.push(n * 2);
});
console.log(out); // [2, 4, 6]
map은 “반환값으로 새 배열을 만드는 도구”인데, 위처럼 외부 배열에 push하면 의도가 흐려집니다.
개선 예제: map은 반환으로 끝내기
const nums = [1, 2, 3];
const out = nums.map((n) => n * 2);
console.log(out); // [2, 4, 6]
같은 결과라도 “의도가 보이게” 쓰면 코드가 훨씬 읽기 쉬워집니다.
정렬·역순·부분 변경: 비파괴 메서드
정렬·역순·부분 교체가 필요해도, 원본을 지키고 새 결과를 얻는 방법입니다. 상태 관리나 렌더링 데이터처럼 “원본 보호”가 중요한 곳에서 특히 유용합니다.
toSorted / toReversed (원본 유지)
const a = [3, 1, 2];
const sorted = a.toSorted((x, y) => x - y);
const reversed = a.toReversed();
console.log(a); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]
console.log(reversed); // [2, 1, 3]
이 메서드들은 원본을 바꾸지 않고, 결과를 새 배열로 반환합니다. “원본 유지가 중요한 코드”에서 특히 안전합니다. 구형 환경을 고려해야 한다면 [...a].sort((x, y) => x - y) 같은 방식으로 대체하는 편이 무난합니다.
toSpliced / with (부분 수정도 안전하게)
const items = ['a', 'b', 'c', 'd'];
const removed = items.toSpliced(1, 2); // 1번부터 2개 제거한 새 배열
const replaced = items.with(1, 'x'); // 1번 값을 바꾼 새 배열
console.log(items); // ['a', 'b', 'c', 'd']
console.log(removed); // ['a', 'd']
console.log(replaced); // ['a', 'x', 'c', 'd']
중간 삭제/교체가 필요해도 원본을 보존할 수 있습니다. splice와 달리 “원본을 지키는” 점이 핵심입니다.
배열 메서드 정리표
원본을 바꾸는 메서드와, 원본을 유지하는 메서드를 빠르게 구분하는 용도입니다.
| 구분 | 대표 메서드 | 한 줄 요약 |
|---|---|---|
| 원본 변경(주의) | push, pop, splice, sort, reverse | 배열 자체가 바뀜 (공유 데이터면 버그 원인) |
| 비변경/비파괴 | map, filter, slice, concat, toSorted, toReversed, toSpliced, with | 새 배열 반환 (내부 객체 참조와 콜백 변경은 별도 확인) |
reduce 알아보기

reduce()는 활용 패턴이 많아서 따로 자세히 정리해두었습니다. 필요하실 때 아래 링크를 참고해 주세요.
JavaScript reduce: 초기값과 누적 과정 자세히 보기
결론
배열은 결국 “목록을 화면에 쓰기 좋은 형태로 만들기” 위한 도구입니다. 그래서 실무에서는 문법을 외우기보다, 어떤 메서드를 쓰면 의도가 잘 보이는지를 기준으로 선택하시는 편이 훨씬 빠르게 늘어납니다.
FAQ (면접 질문)
Q. map과 forEach의 차이와 선택 기준을 말해보세요.
map은 “새 배열을 만들 때” 쓰고, 콜백의 반환값이 새 배열의 요소가 됩니다. forEach는 “그냥 실행” 용도라 반환값이 의미 없고, 중간에 멈추기도 어렵습니다.
Q. find와 filter는 결과가 어떻게 다르며, 언제 각각 쓰나요?
find는 조건을 만족하는 “첫 번째 요소 1개”를 반환하고 못 찾으면 undefined입니다. filter는 조건을 만족하는 “전부”를 모아 “새 배열”로 반환합니다.
Q. sort나 splice가 실무에서 위험하다고 하는 이유는 무엇인가요?
둘 다 원본 배열을 바꿉니다. 같은 배열을 다른 화면/로직이 공유하고 있으면, 한 번의 정렬·삭제가 다른 곳 결과까지 바꾸면서 버그가 나기 쉽습니다. 그래서 보통 “복사 후 작업”을 합니다.
Q. includes와 indexOf는 어떤 차이가 있나요?
includes는 “있는지(불리언)”만 필요할 때 가장 읽기 쉽습니다. indexOf는 “몇 번째인지(숫자)”가 필요할 때 쓰고, 없으면 -1이 나옵니다.
Q. […arr]로 복사했는데도 원본이 바뀌는 것처럼 보일 때가 있습니다. 왜 그런가요?
배열 안에 객체가 들어있으면, 배열은 새로 만들었더라도 “안의 객체”는 공유될 수 있습니다. 그래서 복사본에서 객체 속성을 바꾸면 원본 쪽에도 영향이 갈 수 있습니다.
직접 실습: 예산에 맞는 상품 목록 만들기
이 글의 목표는 배열 메서드의 선택입니다. 합계·그룹 누적은 reduce 글에서 다루고, 여기서는 목록·요소 하나·참거짓이라는 반환 모양을 먼저 정합니다. ZIP의 starter/catalog.mjs에서 아래 요구를 완성하세요.
- 예산 이하 상품을 filter로 남기고 숫자 가격 오름차순으로 정렬합니다.
- map으로 이름 목록을 만듭니다.
- find로 요청 ID의 상품 하나를 찾습니다.
- some으로 예산에 맞는 상품이 있는지, every로 모든 가격이 양수인지 검사합니다.
실습 파일
파일 경로를 확인하고 같은 프로젝트 안에 저장하세요. 이미지 등 소스 목록에 없는 파일과 실행 안내는 실습 ZIP에 포함되어 있습니다.
원본 배열을 보존하면서 필요한 상품을 고르고 새 결과 배열을 만듭니다. 수정 전은 시작 상태, 수정 후는 비교할 완성 상태입니다.
수정 전
app.mjs
import { buildCatalog } from './catalog.mjs';
const products = [
{ id: 1, name: '노트', price: 3000 },
{ id: 2, name: '연필', price: 1000 },
{ id: 3, name: '펜', price: 2000 },
];
console.log(buildCatalog(products, 2000, 3));
catalog.mjs
export function buildCatalog(products, budget, wantedId) {
return { names: [], selected: undefined, hasAffordable: false, allPriced: false };
}
verify.mjs
import assert from 'node:assert/strict';
import { buildCatalog } from './catalog.mjs';
const products = Object.freeze([
Object.freeze({ id: 1, name: '노트', price: 3000 }),
Object.freeze({ id: 2, name: '연필', price: 1000 }),
Object.freeze({ id: 3, name: '펜', price: 2000 }),
]);
assert.deepEqual(buildCatalog(products, 2000, 3), {
names: ['연필', '펜'],
selected: products[2],
hasAffordable: true,
allPriced: true,
});
assert.equal(buildCatalog(products, 2000, 3).selected, products[2]);
assert.deepEqual(buildCatalog(products, 999, 99), {
names: [],
selected: undefined,
hasAffordable: false,
allPriced: true,
});
assert.deepEqual(buildCatalog([], 2000, 1), {
names: [],
selected: undefined,
hasAffordable: false,
allPriced: true,
});
assert.equal(
buildCatalog([{ id: 4, name: '증정품', price: 0 }], 0, 4).allPriced,
false,
);
assert.deepEqual(
products.map((product) => product.id),
[1, 2, 3],
);
assert.deepEqual(
buildCatalog(
[
{ id: 1, name: '10', price: 10 },
{ id: 2, name: '2', price: 2 },
],
10,
1,
).names,
['2', '10'],
);
console.log('메서드 선택, 가격 경계, 빈 배열, 미검색, 숫자 정렬, 원본 보존 검증 통과');
수정 방법
app.mjs
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
catalog.mjs
catalog.mjs에서 아래 차이가 있는 위치를 수정하세요. -는 삭제할 줄, +는 추가할 줄이며 표시는 실제 코드에 넣지 않습니다.
원본 배열을 보존하면서 필요한 상품을 고르고 새 결과 배열을 만듭니다.
--- 수정 전/catalog.mjs
+++ 수정 후/catalog.mjs
@@ -1,3 +1,10 @@
export function buildCatalog(products, budget, wantedId) {
- return { names: [], selected: undefined, hasAffordable: false, allPriced: false };
+ const affordable = products.filter((product) => product.price <= budget);
+ const sorted = [...affordable].sort((a, b) => a.price - b.price);
+ return {
+ names: sorted.map((product) => product.name),
+ selected: products.find((product) => product.id === wantedId),
+ hasAffordable: products.some((product) => product.price <= budget),
+ allPriced: products.every((product) => product.price > 0),
+ };
}
수정 후 탭의 전체 파일과 비교한 뒤 본문의 확인 절차를 실행하세요.
verify.mjs
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
수정 후
app.mjs
import { buildCatalog } from './catalog.mjs';
const products = [
{ id: 1, name: '노트', price: 3000 },
{ id: 2, name: '연필', price: 1000 },
{ id: 3, name: '펜', price: 2000 },
];
console.log(buildCatalog(products, 2000, 3));
catalog.mjs
export function buildCatalog(products, budget, wantedId) {
const affordable = products.filter((product) => product.price <= budget);
const sorted = [...affordable].sort((a, b) => a.price - b.price);
return {
names: sorted.map((product) => product.name),
selected: products.find((product) => product.id === wantedId),
hasAffordable: products.some((product) => product.price <= budget),
allPriced: products.every((product) => product.price > 0),
};
}
verify.mjs
import assert from 'node:assert/strict';
import { buildCatalog } from './catalog.mjs';
const products = Object.freeze([
Object.freeze({ id: 1, name: '노트', price: 3000 }),
Object.freeze({ id: 2, name: '연필', price: 1000 }),
Object.freeze({ id: 3, name: '펜', price: 2000 }),
]);
assert.deepEqual(buildCatalog(products, 2000, 3), {
names: ['연필', '펜'],
selected: products[2],
hasAffordable: true,
allPriced: true,
});
assert.equal(buildCatalog(products, 2000, 3).selected, products[2]);
assert.deepEqual(buildCatalog(products, 999, 99), {
names: [],
selected: undefined,
hasAffordable: false,
allPriced: true,
});
assert.deepEqual(buildCatalog([], 2000, 1), {
names: [],
selected: undefined,
hasAffordable: false,
allPriced: true,
});
assert.equal(
buildCatalog([{ id: 4, name: '증정품', price: 0 }], 0, 4).allPriced,
false,
);
assert.deepEqual(
products.map((product) => product.id),
[1, 2, 3],
);
assert.deepEqual(
buildCatalog(
[
{ id: 1, name: '10', price: 10 },
{ id: 2, name: '2', price: 2 },
],
10,
1,
).names,
['2', '10'],
);
console.log('메서드 선택, 가격 경계, 빈 배열, 미검색, 숫자 정렬, 원본 보존 검증 통과');
기본 상품 가격은 노트 3000원, 연필 1000원, 펜 2000원입니다. 예산 2000원에서는 이름이 [“연필”, “펜”] 순서여야 합니다. node starter/verify.mjs로 직접 구현을 확인하고 node complete/app.mjs, node complete/verify.mjs로 완성 결과를 비교하세요.
빈 배열의 some은 false, every는 true입니다. 검사에서 어기는 요소가 없다는 뜻이며, 상품이 하나 이상 있다는 뜻은 아닙니다. 상품 존재도 필요하면 products.length > 0 && products.every(...)로 두 조건을 구분하세요. findIndex의 실패값 -1을 그대로 splice에 넘기면 마지막 요소를 지우므로 먼저 -1인지 확인합니다.
sort()의 기본 비교는 문자열 순서입니다. 숫자 [2, 10]은 비교 함수가 없으면 [10, 2]가 될 수 있으므로 (a, b) => a - b를 전달합니다. map에서 return을 빠뜨리면 생성된 배열 요소는 undefined가 됩니다. forEach의 반환값은 undefined이며 콜백의 return으로 반복 전체를 끝낼 수 없습니다.
최신 비변경 메서드는 실행 환경 지원을 확인하세요. 예를 들어 typeof [].toSorted === "function"으로 사용할 수 있는지 검사할 수 있습니다. 이 실습의 완성본은 복사 후 정렬을 사용합니다. MDN sort와 MDN flatMap에서 비교 함수와 평탄화 깊이를 확인할 수 있습니다.
같이 읽으면 좋은 글
이 글이 도움이 되었나요?
JavaScript 학습 순서
필수 13개 · 전체 14개
읽음 기록 관리
전체 과정 목차 (14개)
- 필수 학습 · JavaScript 조건문과 반복문: 변수 값의 흐름부터 추적하기
- 필수 학습 · JavaScript 함수와 객체, import export로 모듈 나누기
- 필수 학습 · JavaScript 배열 메서드: map filter forEach reduce 차이 현재 글
- 필수 학습 · JavaScript 객체 참조와 불변 갱신: 중첩 객체를 안전하게 바꾸기
- 필수 학습 · JavaScript reduce 사용법: 배열 누적 계산을 이해하는 기준
- 필수 학습 · JavaScript DOM 폼 만들기: 입력 검증과 접근성 처리
- 필수 학습 · JavaScript 투두리스트 만들기: 상태와 이벤트 위임으로 완성하기
- 필수 학습 · JavaScript URLSearchParams 사용법: URL 파라미터 읽고 수정하기
- 필수 학습 · JavaScript Date UTC KST 차이: 시간대 변환 기준 잡기
- 필수 학습 · JavaScript 실행 컨텍스트 기준: 스코프 호이스팅 클로저 연결하기
- 필수 학습 · JavaScript fetch 오류 처리: Promise부터 404까지
- 필수 학습 · JavaScript 이벤트 루프: Promise와 await 실행 순서 추적하기
- 필수 학습 · JavaScript 고급 비동기: AbortController와 최신 요청 경쟁 제어
- 선택 참고 · GSAP이 처음일 때 기초 사용법: 설치부터 기본 애니메이션까지
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.
BlogFlow AI
이 글과 함께 공부하기
무엇을 도와드릴까요?
읽다가 막힌 부분을 물어보세요. 이 글을 바탕으로 설명하고, 이어서 읽을 본문을 연결합니다.