1. 배열로 유연한 컬렉션을 생성하라
이번에는 배열의 유연성을 극대화하는 방법에 대해 살펴보려고 한다.
원래 JavaScript에서는 데이터 컬렉션을 다루는 구조는 배열과 객체 두 가지밖에 없었다.
하지만 지금은 Map, Set, WeakMap, WeakSet 등이 추가로 들어왔다.
컬렉션을 선택할 때는 어떤 정보로 어떤 작업을 할지 생각해봐야 한다. 추가, 제거, 정렬, 필터, 교체 등을 해야 한다.
당신의 선택은 당연히 배열일 것이고, 배열을 사용하지 않더라도 반드시 배열에 적용되는 개념을 빌려야 한다.
배열은 놀라운 수준의 유연성을 갖추고 있다.
순서를 갖기 때문에 이를 기준으로 추가 또는 제거를 할 수 있고, 모든 위치에 값이 있는지 확인도 가능하다.
배열 메서드의 강력함: map, filter, reduce
map, filter, reduce 등의 배열 메서드를 이용하면 코드 한 줄로 정보를 쉽게 변경하거나 갱신이 가능하다. 아래에 몇 가지 예시를 살펴보자:
const numbers = [1, 2, 3, 4, 5];
// map: 모든 요소를 변환
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter: 조건에 맞는 요소만 선택
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
// reduce: 배열을 단일 값으로 축소
const sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // 15
// 메서드 체이닝: 여러 동작을 연결
const sumOfDoubledEvenNumbers = numbers
.filter(num => num % 2 === 0) // 짝수 선택: [2, 4]
.map(num => num * 2) // 각 값 2배: [4, 8]
.reduce((total, num) => total + num, 0); // 합계: 12
console.log(sumOfDoubledEvenNumbers); // 12
이처럼 배열 메서드를 활용하면 명령형 코드(반복문 사용)를 선언적 코드로 바꿀 수 있어 가독성과 유지보수성이 크게 향상된다.
이터러블과 배열의 관계
배열이 어디서나 등장하는 이유는 배열에 이터러블이 내장되어 있기 때문이다. 이터러블이란 현재 위치를 알고 있는 상태에서 항목을 하나하나씩 처리하는 방법이다.
문자열처럼 자체적으로 이터러블이 존재하거나 Object.keys처럼 이터러블로 변환할 수 있는 데이터 형식이라면 배열에 수행하는 모든 동작을 동일하게 실행할 수 있다.
배열을 사용해야 하는 이유
배열은 다른 컬렉션 타입들과 비교해서도 여러 장점이 있다:
- 풍부한 내장 메서드: JavaScript 배열은 다양한 작업을 위한 많은 메서드를 제공한다.
- 순서 보장: 배열은 삽입 순서를 유지하므로 순서가 중요한 데이터에 적합하다.
- 광범위한 지원: 배열은 JavaScript의 모든 환경에서 지원되며, 다른 API와의 호환성이 뛰어나다.
- 병렬 처리 가능: map, filter 등은 함수형 접근 방식을 제공하여 코드를 더 명확하게 만든다.
- 유연한 확장성: 배열은 크기가 동적으로 조정되며, 모든 타입의 데이터를 담을 수 있다.
물론 Map이나 Set 같은 특화된 컬렉션이 특정 상황에서 더 효율적일 수 있지만, 배열은 그 유연성과 다양한 메서드 덕분에 여전히 가장 기본적이고 강력한 데이터 구조로 남아있다.
2. includes로 존재 여부를 확인해라
배열을 다룰 때는 흔히 존재 여부 확인이 필요한데 기존에는 문자열의 위치를 찾아야 하는데 특정 문자열이 존재하면 해당 문자열의 색인으로 위치를 확인하는 등 정말 번거로운 짓을 해야 했다면 지금은 간단하게 includes를 이용해서 찾아볼 수 있다.
기존 방식 vs includes
과거에는 배열에서 특정 요소가 있는지 확인하기 위해 indexOf() 메서드를 사용했다:
const fruits = ['사과', '바나나', '오렌지', '딸기'];
// 기존 방식: indexOf 사용
if (fruits.indexOf('바나나') !== -1) {
console.log('바나나가 있습니다!');
}
// indexOf는 찾지 못하면 -1을 반환하므로 이런 체크가 필요했다
if (fruits.indexOf('포도') === -1) {
console.log('포도는 없습니다.');
}
이 방식은 직관적이지 않고, -1을 확인해야 하는 추가 작업이 필요했다. ES6에서는 더 명확한 includes() 메서드가 도입되었다:
const fruits = ['사과', '바나나', '오렌지', '딸기'];
// 현대적 방식: includes 사용
if (fruits.includes('바나나')) {
console.log('바나나가 있습니다!');
}
if (!fruits.includes('포도')) {
console.log('포도는 없습니다.');
}
결론
includes 메서드는 배열이나 문자열에서 특정 요소나 부분 문자열의 존재 여부를 확인하는 간결하고 직관적인 방법을 제공한다. 기존의 indexOf 메서드와 달리 불필요한 -1 비교를 없애고, 코드의 가독성을 크게 향상시킨다. 또한 NaN 같은 특수한 값을 처리할 때도 더 일관된 결과를 제공한다.
단순히 요소의 존재 여부를 확인하는 경우에는 includes를 사용하고, 요소의 위치까지 알아야 하는 경우에만 indexOf를 사용하는 것이 좋다.
3. 펼침 연산자로 배열을 본떠라
이번에는 펼침 연산자를 이용해서 배열에 대한 작업을 단순하게 만드는 방법을 살펴보려고 한다.
배열에는 수많은 메서드가 있어서 혼란스럽거나 조작 부수효과로 인한 문제를 맞닥뜨릴 수 있다.
하지만 ... 이것으로 최소한의 코드로 배열을 빠르게 생성 및 조작할 수 있다.
펼침 연산자에 대한 기능은 단순하다. 배열에 포함된 항목을 목록으로 바꿔준다. 목록은 매개 변수 또는 새로운 배열을 생성할 때 사용할 수 있는 일련의 항목이다.
펼침 연산자 기본 사용법
// 기본 배열
const fruits = ['사과', '바나나', '오렌지'];
// 펼침 연산자로 배열 복사하기
const fruitsCopy = [...fruits];
console.log(fruitsCopy); // ['사과', '바나나', '오렌지']
// 두 배열 합치기
const moreFruits = ['딸기', '키위'];
const allFruits = [...fruits, ...moreFruits];
console.log(allFruits); // ['사과', '바나나', '오렌지', '딸기', '키위']
// 배열 중간에 요소 추가하기
const fruitsWithMango = [...fruits.slice(0, 2), '망고', ...fruits.slice(2)];
console.log(fruitsWithMango); // ['사과', '바나나', '망고', '오렌지']
다양한 배열 조작 방법 비교
펼침 연산자가 얼마나 좋은 기능인지에 대해 알아보기 위해 3가지 코드를 살펴보자.
1. for 루프를 사용하여 배열에서 항목 제거하기
이 방식은 나쁘지 않지만 코드가 은근히 길다.
function removeItem(items, removable) {
const updated = [];
for (let i = 0; i < items.length; i++) {
if (items[i] !== removable) {
updated.push(items[i]);
}
}
return updated;
}
const numbers = [1, 2, 3, 4, 5];
const result = removeItem(numbers, 3);
console.log(result); // [1, 2, 4, 5]
2. splice 메서드를 사용하여 배열에서 항목 제거하기
이 방식은 일반적으로 사용되지만, splice 메서드가 원본 배열을 직접 수정한다는 점에서 부수 효과를 발생시킬 수 있어 혼란을 가져올 수 있다. 그래서 먼저 배열을 복사한 후 작업해야 한다.
function removeItem(items, removable) {
const index = items.indexOf(removable);
if (index !== -1) {
const copy = [...items]; // 먼저 복사본 생성
copy.splice(index, 1);
return copy;
}
return items;
}
const numbers = [1, 2, 3, 4, 5];
const result = removeItem(numbers, 3);
console.log(result); // [1, 2, 4, 5]
3. 펼침 연산자와 filter를 사용하여 배열에서 항목 제거하기
이 방식은 완벽하게 원본 조작도 없고 읽기 편하다. 재사용 가능하고 예측이 가능하니 우리가 좋아하는 특징을 다 갖추고 있다.
function removeItem(items, removable) {
return items.filter(item => item !== removable);
}
// 또는 펼침 연산자로 더 명시적인 복사본 생성
function removeItemWithSpread(items, removable) {
return [...items.filter(item => item !== removable)];
}
const numbers = [1, 2, 3, 4, 5];
const result = removeItem(numbers, 3);
console.log(result); // [1, 2, 4, 5]
펼침 연산자의 다양한 활용
// 배열 요소 추가하기
const start = [1, 2, 3];
const end = [7, 8, 9];
// 배열 끝에 요소 추가
const withEnd = [...start, 4, 5, 6];
console.log(withEnd); // [1, 2, 3, 4, 5, 6]
// 배열 시작에 요소 추가
const withStart = [0, ...start];
console.log(withStart); // [0, 1, 2, 3]
// 여러 배열 결합
const combined = [...start, 4, 5, 6, ...end];
console.log(combined); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 배열 복사 및 수정
const original = [10, 20, 30];
// 배열 복사 (얕은 복사)
const copy = [...original];
copy[0] = 100;
console.log(original); // [10, 20, 30] - 원본 배열은 변경되지 않음
console.log(copy); // [100, 20, 30]
// 특정 인덱스의 값만 변경한 새 배열 생성
function updateAt(array, index, value) {
return [
...array.slice(0, index),
value,
...array.slice(index + 1)
];
}
const updated = updateAt(original, 1, 200);
console.log(updated); // [10, 200, 30]
console.log(original); // [10, 20, 30] - 원본 배열은 변경되지 않음
// 객체 배열 다루기
const users = [
{ id: 1, name: '김철수' },
{ id: 2, name: '이영희' },
{ id: 3, name: '박지민' }
];
// 특정 사용자 정보 업데이트
function updateUser(users, id, newProps) {
return users.map(user =>
user.id === id ? { ...user, ...newProps } : user
);
}
const updatedUsers = updateUser(users, 2, { name: '새이름', active: true });
펼침 연산자의 장점 정리
- 불변성 유지: 원본 배열을 변경하지 않고 새로운 배열을 생성한다.
- 코드 가독성: 반복문보다 간결하고 의도가 명확하게 드러난다.
- 예측 가능성: 부수 효과가 없어 결과를 쉽게 예측할 수 있다.
- 유연한 조작: 배열의 시작, 중간, 끝 어디든 요소를 추가하거나 제거할 수 있다.
- 함수형 프로그래밍: 다른 함수형 메서드(map, filter 등)와 함께 사용하기 좋다.
결론
펼침 연산자(...)는 JavaScript에서 배열을 다룰 때 매우 강력하고 유용한 도구다. 불변성을 유지하면서 코드를 간결하게 작성할 수 있으며, 부수 효과를 줄여 예측 가능한 코드를 작성하는 데 도움이 된다. 배열 조작이 필요할 때 for 루프나 변형 메서드(splice, push 등) 대신 펼침 연산자와 불변성을 유지하는 메서드(map, filter, slice 등)의 조합을 사용하는 것이 좋다.
4. push 대신 펼침 연산자로 원본 변경을 피해라
배열에 새로운 항목을 추가할 때 push() 메서드는 가장 흔히 사용되는 방법 중 하나다.
하지만 push()는 원본 배열을 직접 수정하는 부수 효과(side effect)를 발생시키며, 이는 예상치 못한 버그의 원인이 될 수 있다.
push와 펼침 연산자 비교
// push 메서드 사용
const fruits = ['사과', '바나나', '오렌지'];
fruits.push('딸기');
console.log(fruits); // ['사과', '바나나', '오렌지', '딸기']
// 펼침 연산자 사용
const fruits = ['사과', '바나나', '오렌지'];
const newFruits = [...fruits, '딸기'];
console.log(fruits); // ['사과', '바나나', '오렌지'] - 원본 유지
console.log(newFruits); // ['사과', '바나나', '오렌지', '딸기'] - 새 배열 생성
결론
push()는 편리하지만 원본 배열을 변경하는 부수 효과를 일으키며, 이는 코드의 복잡성을 증가시키고 예기치 않은 버그를 발생시킬 수 있다. 반면 펼침 연산자는 원본 데이터를 보존하며 새로운 배열을 생성함으로써 코드의 안정성과 예측 가능성을 높여준다.
'개발독서' 카테고리의 다른 글
조건문을 깔끔하게 (0) | 2025.04.27 |
---|---|
특수한 컬렉션을 이용해 코드 명료화을 극대화해라. (0) | 2025.04.06 |
변수 할당으로 의도를 표현해라 (0) | 2025.04.06 |