`Array` 객체
이 글에서는 Array 객체에 대해 설명합니다.
배열의 실제 사용법을 쉽게 단계별로 설명하겠습니다.
YouTube Video
Array 객체
자바스크립트의 Array 객체는 다양한 데이터 처리를 위한 기반을 이루는 가장 중요한 구조 중 하나입니다. 기본적인 배열 조작부터 효율적인 데이터 변환에 유용한 고차 함수까지, 알아두어야 할 기능이 많이 있습니다.
배열 기본
자바스크립트에서 배열은 여러 값을 함께 다루기 위한 기본 데이터 구조입니다. 여기서는 배열을 생성하는 방법과 요소를 읽고 쓰는 방법을 간단한 예시와 함께 소개합니다.
1// Create arrays in different ways
2const arr1 = [1, 2, 3]; // array literal
3const arr2 = new Array(4, 5, 6); // Array constructor
4const arr3 = Array.of(7, 8, 9); // Array.of
5
6console.log("arr1 created with literal. :", arr1);
7console.log("arr2 created with constructor:", arr2);
8console.log("arr3 created with Array.of. :", arr3);
9// arr1 created with literal. : [ 1, 2, 3 ]
10// arr2 created with constructor: [ 4, 5, 6 ]
11// arr3 created with Array.of. : [ 7, 8, 9 ]
12
13// Access and modify elements
14let first = arr1[0]; // read element
15console.log("First element of arr1:", first);
16// First element of arr1: 1
17
18arr1[1] = 20; // modify element
19console.log("arr1 after modifying index 1:", arr1);
20// arr1 after modifying index 1: [ 1, 20, 3 ]
21
22const len = arr1.length; // get length
23console.log("Length of arr1:", len);
24// Length of arr1: 3
- 이 코드는 배열을 생성하는 세 가지 방법, 인덱스를 사용한 요소의 읽기 및 업데이트, 그리고
length속성을 이용한 길이 확인 방법을 보여줍니다. - 배열 리터럴은 가장 일반적이고 가독성이 뛰어나며, 일상적인 상황에서 가장 자주 사용됩니다.
요소 추가 및 제거 (끝 또는 시작)
배열에서는 끝이나 시작 부분에 요소를 쉽게 추가하거나 제거할 수 있습니다. 이러한 연산은 스택이나 큐 같은 구조를 구현할 때도 유용합니다.
1// Push and pop (stack-like)
2const stack = [];
3console.log("Initial stack:", stack);
4
5stack.push(1); // push 1
6console.log("After push(1):", stack);
7
8stack.push(2); // push 2
9console.log("After push(2):", stack);
10
11const last = stack.pop(); // pop -> 2
12console.log("Popped value:", last);
13console.log("Stack after pop():", stack);
14
15// Unshift and shift (queue-like)
16const queue = [];
17console.log("Initial queue:", queue);
18
19queue.push('a'); // add to end
20console.log("After push('a'):", queue);
21
22queue.unshift('start'); // add to front
23console.log("After unshift('start'):", queue);
24
25const firstItem = queue.shift(); // remove from front
26console.log("Shifted value:", firstItem);
27console.log("Queue after shift():", queue);
28
29// Initial stack: []
30// After push(1): [ 1 ]
31// After push(2): [ 1, 2 ]
32// Popped value: 2
33// Stack after pop(): [ 1 ]
34
35// Initial queue: []
36// After push('a'): [ 'a' ]
37// After unshift('start'): [ 'start', 'a' ]
38// Shifted value: start
39// Queue after shift(): [ 'a' ]
push와pop은 배열의 끝에서 동작합니다. 이들은 스택 구조를 구현할 때 이상적입니다.unshift와shift는 배열의 시작에서 동작합니다. 하지만 시작 부분에서 조작할 경우 내부적으로 모든 요소의 인덱스를 이동시키기 때문에 비용이 많이 드는 연산임을 유의하세요.
중간 요소 처리 (splice와 slice)
배열 중간 요소를 다룰 때 원본 배열을 수정할지 여부에 따라 splice 또는 slice를 선택하세요. 배열의 일부만 추출하고 싶다면 slice를 사용하세요. 요소를 삽입하거나 삭제하는 등 배열 자체를 수정하려면 splice를 사용하세요.
1// slice (non-destructive)
2const nums = [0, 1, 2, 3, 4];
3console.log("Original nums:", nums);
4
5const part = nums.slice(1, 4); // returns [1, 2, 3]
6console.log("Result of nums.slice(1, 4):", part);
7console.log("nums after slice (unchanged):", nums);
8
9// splice (destructive)
10const arr = [10, 20, 30, 40];
11console.log("\nOriginal arr:", arr);
12
13// remove 1 item at index 2, insert 25 and 27
14arr.splice(2, 1, 25, 27);
15console.log("After arr.splice(2, 1, 25, 27):", arr);
16
17// Original nums: [ 0, 1, 2, 3, 4 ]
18// Result of nums.slice(1, 4): [ 1, 2, 3 ]
19// nums after slice (unchanged): [ 0, 1, 2, 3, 4 ]
20
21// Original arr: [ 10, 20, 30, 40 ]
22// After arr.splice(2, 1, 25, 27): [ 10, 20, 25, 27, 40 ]
slice는 요소만 추출하고 원본 배열은 수정하지 않습니다.splice는 요소를 추가하거나 제거하며 배열 자체를 수정하므로, 그 영향에 특히 주의해야 합니다.
반복문 (for / for...of / forEach)
배열을 순차적으로 처리하는 방법은 여러 가지가 있으며, 목적과 코딩 스타일에 따라 선택할 수 있습니다. 여기 대표적인 세 가지 반복문을 소개합니다.
1const items = ['apple', 'banana', 'cherry'];
2console.log("Items:", items);
3
4// classic for
5console.log("\n--- Classic for loop ---");
6for (let i = 0; i < items.length; i++) {
7 console.log(`Index: ${i}, Value: ${items[i]}`);
8}
9
10// for...of (recommended for values)
11console.log("\n--- for...of loop ---");
12for (const item of items) {
13 console.log(`Value: ${item}`);
14}
15
16// forEach (functional style)
17console.log("\n--- forEach loop ---");
18items.forEach((item, index) => {
19 console.log(`Index: ${index}, Value: ${item}`);
20});for반복문은 가장 유연하며 인덱스 조작이나break등으로 반복을 세밀하게 제어할 수 있습니다.for...of는 요소 값을 간단하게 처리할 수 있으며 읽기 쉬운 코드 작성을 위한 균형잡힌 방법입니다.forEach는 함수형 스타일 코딩에 적합하며, 각 요소마다 로깅이나 데이터 갱신 등 부수 효과가 필요한 작업에 적합합니다. 하지만break나continue를 사용할 수 없으며,await를 활용한 비동기 처리에는 부적합합니다.
map / filter / reduce — 고차 함수
map, filter, reduce는 배열을 변환, 필터링 또는 집계할 때 자주 사용되는 고차 함수입니다. 반복적인 처리를 명확하게 표현할 수 있어서 코드가 간단하고 이해하기 쉬워집니다.
1const numbers = [1, 2, 3, 4, 5];
2console.log("Original numbers:", numbers);
3
4// map: transform each item
5const doubled = numbers.map(n => n * 2);
6console.log("\nResult of map (n * 2):", doubled);
7
8// filter: select items
9const evens = numbers.filter(n => n % 2 === 0);
10console.log("Result of filter (even numbers):", evens);
11
12// reduce: accumulate to single value
13const sum = numbers.reduce((acc, n) => acc + n, 0);
14console.log("Result of reduce (sum):", sum);
15
16// Original numbers: [ 1, 2, 3, 4, 5 ]
17// Result of map (n * 2): [ 2, 4, 6, 8, 10 ]
18// Result of filter (even numbers): [ 2, 4 ]
19// Result of reduce (sum): 15
- 이런 메서드를 이용하면 선언적 스타일로 '무엇을 할지'에 집중할 수 있어, 가독성이 향상되고 부작용을 피하는 데 도움이 됩니다.
find / findIndex / some / every
아래에서는 검색 및 조건 확인 관련 메서드를 개요로 소개합니다. 이들은 특정 조건을 만족하는 요소를 찾거나 집합에 대한 불린 체크를 수행하는 데 유용합니다.
1const users = [
2 { id: 1, name: 'Alice' },
3 { id: 2, name: 'Bob' },
4 { id: 3, name: 'Carol' }
5];
6
7console.log("Users:", users);
8
9// Find the first user whose name is 'Bob'
10const bob = users.find(user => user.name === 'Bob');
11console.log("\nResult of find (name === 'Bob'):", bob);
12
13// Find index of the user whose id is 3
14const indexOfId3 = users.findIndex(user => user.id === 3);
15console.log("Result of findIndex (id === 3):", indexOfId3);
16
17// Check if there exists a user with id = 2
18const hasId2 = users.some(user => user.id === 2);
19console.log("Result of some (id === 2):", hasId2);
20
21// Check if all users have a numeric id
22const allHaveNumericId = users.every(user => typeof user.id === 'number');
23console.log("Result of every (id is number):", allHaveNumericId);
24// Result of find (name === 'Bob'): { id: 2, name: 'Bob' }
25// Result of findIndex (id === 3): 2
26// Result of some (id === 2): true
27// Result of every (id is number): true
- **
find**는 조건에 맞는 첫 번째 요소를 반환합니다. - **
findIndex**는 조건에 맞는 요소의 인덱스를 반환합니다. - **
some**은 조건을 만족하는 요소가 하나라도 있으면true를 반환합니다. - **
every**는 모든 요소가 조건을 만족하면true를 반환합니다.
이들 모두 배열 처리에서 매우 유용하므로 상황에 맞게 적절히 사용하면 코드를 간결하고 명확하게 만들 수 있습니다.
정렬 및 비교 함수
sort를 사용하여 배열을 정렬할 때 기본적으로 요소를 문자열로 비교하므로, 숫자 정렬 시 예상치 못한 결과가 발생할 수 있습니다.
1const nums = [10, 2, 33, 4];
2console.log("Original nums:", nums);
3
4// Default sort: compares elements as strings (not suitable for numbers)
5nums.sort();
6console.log("\nAfter default sort (string comparison):", nums);
7
8// Numeric ascending sort using a compare function
9nums.sort((a, b) => a - b);
10console.log("After numeric sort (a - b):", nums);
11
12// Sort objects by a property
13const people = [{ age: 30 }, { age: 20 }, { age: 25 }];
14console.log("\nOriginal people:", people);
15
16people.sort((a, b) => a.age - b.age);
17console.log("After sorting people by age:", people);
18
19// After default sort (string comparison): [ 10, 2, 33, 4 ]
20// After numeric sort (a - b): [ 2, 4, 10, 33 ]
21// Original people: [ { age: 30 }, { age: 20 }, { age: 25 } ]
22// After sorting people by age: [ { age: 20 }, { age: 25 }, { age: 30 } ]
- 숫자나 객체를 정렬할 때는 항상 비교 함수를 지정하여 의도한 순서대로 정렬하세요.
- 비교 함수에서 음수를 반환하면
a가b앞에, 양수면b가 앞에, 0이면 순서가 변경되지 않습니다.
배열 복사와 불변성
배열을 복사할 때 '참조 복사'와 '얕은 복사'의 차이를 이해하는 것이 중요합니다. 특히 배열 내에 객체가 있을 경우, 얕은 복사는 내부 객체가 공유된다는 점에 유의해야 합니다.
참조 복사
배열을 다른 변수에 할당하면 배열 자체가 복제되는 것이 아니라, 동일한 배열을 가리키는 '참조'가 복사됩니다.
1const a = [1, 2, 3];
2console.log("Original a:", a);
3
4// Reference copy; modifying b also affects a
5const b = a;
6console.log("\nReference copy b = a:", b);
7// Reference copy b = a: [ 1, 2, 3 ]
8
9b[0] = 100;
10console.log("After modifying b[0] = 100:");
11console.log("a:", a); // a: [ 100, 2, 3 ] (affected)
12console.log("b:", b); // b: [ 100, 2, 3 ]
- 참조 복사의 경우, 복사된 변수를 사용하여 배열의 내용을 수정하면 동일한 배열을 참조하는 원본 변수에도 변경 사항이 반영됩니다.
얕은 복사
slice()나 전개(스프레드) 문법을 사용하면 값만 복제되기 때문에 '얕은 복사'가 만들어지고, 원본 배열과 복사된 배열은 별개의 객체로 취급됩니다.
1const a = [1, 2, 3];
2console.log("Original a:", a);
3
4// Shallow copy (new array)
5const b = a.slice();
6console.log("\nShallow copy using slice():", b);
7// Shallow copy using slice(): [ 100, 2, 3 ]
8
9const c = [...a];
10console.log("Shallow copy using spread [...a]:", c);
11// Shallow copy using spread [...a]: [ 100, 2, 3 ]
12
13// Modifying c or d does NOT affect a
14b[1] = 200;
15c[2] = 300;
16console.log("\nAfter modifying b[1] = 200 and c[2] = 300:");
17console.log("a:", a); // [ 100, 2, 3 ]
18console.log("b:", b); // [ 100, 200, 3 ]
19console.log("c:", c); // [ 100, 2, 300 ]
- 이 코드는
slice()나 전개 문법으로 만들어진 배열의 얕은 복사가 원본 배열에 영향을 주지 않음을 보여줍니다.
얕은 복사와 불변성
'얕은 복사'를 사용해 배열을 복제하더라도, 배열 내부에 객체가 포함되어 있으면 의도치 않은 공유가 발생할 수 있습니다.
1// Shallow copy doesn't clone inner objects
2const nested = [{ x: 1 }, { x: 2 }];
3console.log("\nOriginal nested:", nested);
4// Original nested: [ { x: 1 }, { x: 2 } ]
5
6const shallow = nested.slice();
7console.log("Shallow copy of nested:", shallow);
8// Shallow copy of nested: [ { x: 1 }, { x: 2 } ]
9
10// Changing inner object affects both arrays
11shallow[0].x = 99;
12console.log("\nAfter shallow[0].x = 99:");
13console.log("nested:", nested);
14console.log("shallow:", shallow);
15// nested: [ { x: 99 }, { x: 2 } ]
16// shallow: [ { x: 99 }, { x: 2 } ]
- 이 코드는 얕은 복사의 경우 내부 객체가 공유되므로, 내부 객체를 수정하면 원본 배열과 복사본 모두에 영향을 미친다는 것을 보여줍니다.
- 독립적인 데이터가 필요하다면
structuredClone()이나 JSON 변환 등을 통한 '깊은 복사'가 필요합니다.
유용한 유틸리티 메서드
아래는 배열을 다룰 때 자주 사용되는 유틸리티 메서드입니다. 목적에 맞게 적절히 사용하면 짧고 읽기 쉬운 코드를 작성할 수 있습니다.
includes
includes 메서드는 배열에 특정 값이 포함되어 있는지 확인합니다.
1// includes: check if array contains a value
2const letters = ['a', 'b', 'c'];
3const hasB = letters.includes('b');
4console.log("letters:", letters); // [ 'a', 'b', 'c' ]
5console.log("letters.includes('b'):", hasB); // true
- 이 코드에서는
includes메서드를 사용해 지정한 값이 배열 내에 존재하는지 간단하게 확인합니다.
concat
concat 메서드는 지정한 배열이나 값을 끝에 추가한 새로운 배열을 반환하며, 원본 배열은 변경되지 않습니다.
1// concat: merge arrays without mutation
2const a1 = [1, 2];
3const a2 = [3, 4];
4const combined = a1.concat(a2);
5console.log("a1.concat(a2):", combined); // [ 1, 2, 3, 4 ]
6console.log("a1(unchanged):", a1); // [ 1, 2 ]
- 이 코드는
concat이 파괴적이지 않아, 원본 배열을 유지하면서 새로운 배열을 생성할 수 있음을 보여줍니다.
flat
flat 메서드를 사용하면 중첩된 배열을 평탄화할 수 있습니다.
1// flat and flatMap: flatten arrays or map + flatten in one step
2const nested = [1, [2, [3]]];
3console.log("nested:", nested); // nested: [ 1, [ 2, [ 3 ] ] ]
4console.log("nested.flat():", nested.flat()); // default depth = 1
5// nested.flat(): [ 1, 2, [ 3 ] ]
- 이 코드는 배열을 한 단계 평탄화한 결과를 보여줍니다.
flat은 깊이를 지정할 수 있으므로, 필요에 따라 유연하게 중첩을 해소할 수 있습니다.
flatMap
flatMap 메서드는 각 요소에 변환을 적용한 뒤, 결과를 자동으로 1차원 배열로 평탄화합니다.
1// flat and flatMap: flatten arrays or map + flatten in one step
2const words = ['hello world', 'hi'];
3console.log("words:", words); // [ 'hello world', 'hi' ]
4
5const splitWords = words.flatMap(w => w.split(' '));
6console.log("words.flatMap(w => w.split(' ')):", splitWords);
7// words.flatMap(w => w.split(' ')): [ 'hello', 'world', 'hi' ]
- 이 코드는 배열 내의 각 문자열을 공백으로 분할한 뒤, 결과를 결합하여 하나의 배열로 평탄화하는 예시를 보여줍니다.
join
join 메서드는 배열의 요소들을 지정한 구분자로 연결해 문자열을 생성합니다.
1// join: combine elements into a string with a separator
2const csv = ['a', 'b', 'c'].join(',');
3console.log("['a', 'b', 'c'].join(','):", csv); // a,b,c
- 이 코드에서는
join메서드를 사용하여 배열을 쉼표로 구분된 문자열로 변환합니다.
자주 발생하는 실수
배열 연산은 겉보기에는 단순하지만, 쉽게 예상치 못한 동작을 초래하는 여러 가지 주의점이 있습니다. 일상적인 배열 조작 중 놓치기 쉬운 함정들이 많으니 아래 사항을 신경 쓰면 코드의 신뢰성을 크게 높일 수 있습니다.
Array의sort()는 기본적으로 문자열 비교로 정렬됩니다. 숫자를 올바르게 정렬하려면 반드시 비교 함수를 지정해야 합니다.slice나 스프레드 문법 등으로 배열을 복사하면 얕은 복사가 이루어집니다. 배열에 객체가 포함되어 있으면 원본 데이터가 의도치 않게 수정될 수 있으니 주의하세요.splice는 배열을 직접 변경하는 파괴적 메서드이고,slice는 원본 배열을 수정하지 않는 비파괴적 메서드입니다. 용도에 맞게 적절하게 사용하는 것이 중요합니다.forEach는await를 사용하는 비동기 반복 처리에는 적합하지 않습니다. 비동기 처리를 순차적으로 제대로 실행하고 싶다면for...of를 사용하는 것이 좋습니다.
실전 예시
아래는 배열 메서드를 조합하여 '사용자 데이터의 전체 나이 합산, 30세 이상 추출, 이름 리스트 생성'을 수행하는 예시입니다.
1const users = [
2 { name: 'Alice', age: 28 },
3 { name: 'Bob', age: 34 },
4 { name: 'Carol', age: 41 },
5 { name: 'Dave', age: 19 }
6];
7
8console.log("Users:", users);
9
10// sum ages
11const totalAge = users.reduce((acc, u) => acc + u.age, 0);
12console.log("\nTotal age of all users:", totalAge);
13// Total age of all users: 122
14
15// filter and map names of users 30 and older
16const namesOver30 = users
17 .filter(u => u.age >= 30)
18 .map(u => u.name);
19
20console.log("Users aged 30 or older (names):", namesOver30);
21// Users aged 30 or older (names): [ 'Bob', 'Carol' ]
reduce,filter,map을 연결하면 데이터 집계, 조건 추출, 변환을 간단하게 작성할 수 있습니다.- 이와 같은 '데이터 처리 파이프라인'은 가독성이 높으며, 부작용이 적은 스타일로 실제 현업에서도 자주 사용됩니다.
요약
자바스크립트 배열은 기본적인 조작만으로도 활용도가 넓으며, 고차 함수를 사용하면 코드를 더욱 간결하고 능동적으로 만들 수 있습니다. 이해해야 할 점이 많지만, 각각의 기능을 상황에 맞게 익히면 데이터 처리가 훨씬 원활해질 것입니다.
위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.