JavaScript 배열로 함수형 프로그래밍의 힘을 활용하세요. 내장 메서드를 사용하여 데이터를 효율적으로 변환, 필터링, 축약하는 방법을 알아보세요.
JavaScript 배열로 함수형 프로그래밍 마스터하기
끊임없이 진화하는 웹 개발 환경에서 JavaScript는 계속해서 초석이 되고 있습니다. 객체 지향 및 명령형 프로그래밍 패러다임이 오랫동안 지배적이었지만, 함수형 프로그래밍(FP)이 상당한 주목을 받고 있습니다. FP는 불변성, 순수 함수, 선언형 코드를 강조하여 더욱 강력하고 유지 관리하기 쉬우며 예측 가능한 애플리케이션을 만들 수 있습니다. JavaScript에서 함수형 프로그래밍을 가장 강력하게 받아들이는 방법 중 하나는 네이티브 배열 메서드를 활용하는 것입니다.
이 포괄적인 가이드에서는 JavaScript 배열을 사용하여 함수형 프로그래밍 원칙의 힘을 어떻게 활용할 수 있는지 자세히 살펴보겠습니다. 주요 개념을 살펴보고 map
, filter
, reduce
와 같은 메서드를 사용하여 적용하는 방법을 시연하여 데이터 조작 방식을 변화시킬 것입니다.
함수형 프로그래밍이란 무엇인가요?
JavaScript 배열에 들어가기 전에 함수형 프로그래밍에 대해 간략하게 정의해 보겠습니다. 핵심은 FP를 계산을 수학적 함수의 평가로 취급하고 상태 변경 및 가변 데이터를 피하는 프로그래밍 패러다임입니다. 주요 원칙은 다음과 같습니다.
- 순수 함수: 순수 함수는 동일한 입력에 대해 항상 동일한 출력을 생성하며 부작용이 없습니다(외부 상태를 수정하지 않음).
- 불변성: 데이터는 한 번 생성되면 변경할 수 없습니다. 기존 데이터를 수정하는 대신 원하는 변경 사항으로 새 데이터를 생성합니다.
- 일급 함수: 함수는 다른 변수와 마찬가지로 취급될 수 있습니다. 변수에 할당하거나 다른 함수의 인수로 전달하거나 함수에서 반환할 수 있습니다.
- 선언형 vs. 명령형: 함수형 프로그래밍은 단계별로 달성 방법을 자세히 설명하는 명령형 스타일이 아닌, 달성하려는 내용을 설명하는 선언형 스타일에 중점을 둡니다.
이러한 원칙을 채택하면 특히 복잡한 애플리케이션에서 추론, 테스트 및 디버깅하기 쉬운 코드를 얻을 수 있습니다. JavaScript의 배열 메서드는 이러한 개념을 구현하는 데 완벽하게 적합합니다.
JavaScript 배열 메서드의 힘
JavaScript 배열에는 전통적인 루프(for
또는 while
과 같은)에 의존하지 않고 복잡한 데이터 조작을 허용하는 풍부한 내장 메서드 세트가 장착되어 있습니다. 이러한 메서드는 종종 새 배열을 반환하여 불변성을 촉진하고 콜백 함수를 허용하여 함수형 접근 방식을 가능하게 합니다.
가장 기본적인 함수형 배열 메서드를 살펴보겠습니다.
1. Array.prototype.map()
map()
메서드는 호출 배열의 각 요소에 제공된 함수를 호출한 결과로 채워진 새 배열을 만듭니다. 배열의 각 요소를 새로운 것으로 변환하는 데 이상적입니다.
구문:
array.map(callback(currentValue[, index[, array]])[, thisArg])
callback
: 각 요소에 대해 실행할 함수입니다.currentValue
: 배열에서 처리 중인 현재 요소입니다.index
(선택 사항): 처리 중인 현재 요소의 인덱스입니다.array
(선택 사항):map
이 호출된 배열입니다.thisArg
(선택 사항):callback
을 실행할 때this
로 사용할 값입니다.
주요 특징:
- 새 배열을 반환합니다.
- 원본 배열은 변경되지 않은 상태로 유지됩니다(불변성).
- 새 배열은 원본 배열과 길이가 같습니다.
- 콜백 함수는 각 요소에 대한 변환된 값을 반환해야 합니다.
예제: 각 숫자 두 배로 만들기
숫자 배열이 있고 각 숫자가 두 배가 된 새 배열을 만들고 싶다고 상상해 보세요.
const numbers = [1, 2, 3, 4, 5];
// 변환을 위한 map 사용
const doubledNumbers = numbers.map(number => number * 2);
console.log(numbers); // 출력: [1, 2, 3, 4, 5] (원본 배열은 변경되지 않음)
console.log(doubledNumbers); // 출력: [2, 4, 6, 8, 10]
예제: 객체에서 속성 추출하기
일반적인 사용 사례는 객체 배열에서 특정 속성을 추출하는 것입니다. 사용자 목록이 있고 이름만 가져오고 싶다고 가정해 봅시다.
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
const userNames = users.map(user => user.name);
console.log(userNames); // 출력: ['Alice', 'Bob', 'Charlie']
2. Array.prototype.filter()
filter()
메서드는 제공된 함수에 의해 구현된 테스트를 통과하는 모든 요소를 포함하는 새 배열을 만듭니다. 조건에 따라 요소를 선택하는 데 사용됩니다.
구문:
array.filter(callback(element[, index[, array]])[, thisArg])
callback
: 각 요소에 대해 실행할 함수입니다. 요소를 유지하려면true
를 반환하거나, 버리려면false
를 반환해야 합니다.element
: 배열에서 처리 중인 현재 요소입니다.index
(선택 사항): 현재 요소의 인덱스입니다.array
(선택 사항):filter
이 호출된 배열입니다.thisArg
(선택 사항):callback
을 실행할 때this
로 사용할 값입니다.
주요 특징:
- 새 배열을 반환합니다.
- 원본 배열은 변경되지 않은 상태로 유지됩니다(불변성).
- 새 배열은 원본 배열보다 요소가 적을 수 있습니다.
- 콜백 함수는 부울 값을 반환해야 합니다.
예제: 짝수 필터링
짝수만 유지하도록 숫자 배열을 필터링해 봅시다.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 짝수 선택을 위한 filter 사용
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(numbers); // 출력: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(evenNumbers); // 출력: [2, 4, 6, 8, 10]
예제: 활성 사용자 필터링
사용자 배열에서 활성으로 표시된 사용자를 필터링해 봅시다.
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true },
{ id: 4, name: 'David', isActive: false }
];
const activeUsers = users.filter(user => user.isActive);
console.log(activeUsers);
/* 출력:
[
{ id: 1, name: 'Alice', isActive: true },
{ id: 3, name: 'Charlie', isActive: true }
]
*/
3. Array.prototype.reduce()
reduce()
메서드는 이전 요소에 대한 계산 반환 값을 전달하여 배열의 각 요소에 대해 사용자 제공 “리듀서” 콜백 함수를 순서대로 실행합니다. 배열의 모든 요소를 통해 리듀서를 실행한 최종 결과는 단일 값입니다.
이것은 아마도 배열 메서드 중에서 가장 다재다능하며 많은 함수형 프로그래밍 패턴의 초석으로, 배열을 단일 값(예: 합계, 곱, 개수 또는 새 객체 또는 배열)으로 “줄이는” 것을 허용합니다.
구문:
array.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
callback
: 각 요소에 대해 실행할 함수입니다.accumulator
: 콜백 함수의 이전 호출 결과 값입니다. 첫 번째 호출에서는 제공된 경우initialValue
이고, 그렇지 않으면 배열의 첫 번째 요소입니다.currentValue
: 처리 중인 현재 요소입니다.index
(선택 사항): 현재 요소의 인덱스입니다.array
(선택 사항):reduce
가 호출된 배열입니다.initialValue
(선택 사항):callback
의 첫 번째 호출에 대한 첫 번째 인수로 사용할 값입니다.initialValue
가 제공되지 않으면 배열의 첫 번째 요소가 초기accumulator
값으로 사용되며 반복은 두 번째 요소부터 시작됩니다.
주요 특징:
- 단일 값 (배열 또는 객체도 될 수 있음)을 반환합니다.
- 원본 배열은 변경되지 않은 상태로 유지됩니다(불변성).
initialValue
는 특히 빈 배열이나 누적기 유형이 배열 요소 유형과 다른 경우 명확성과 오류 방지에 중요합니다.
예제: 숫자 합산
배열의 모든 숫자를 합산해 봅시다.
const numbers = [1, 2, 3, 4, 5];
// 숫자를 합산하기 위한 reduce 사용
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // 0은 initialValue입니다.
console.log(sum); // 출력: 15
설명:
- 호출 1:
accumulator
는 0이고currentValue
는 1입니다. 0 + 1 = 1을 반환합니다. - 호출 2:
accumulator
는 1이고currentValue
는 2입니다. 1 + 2 = 3을 반환합니다. - 호출 3:
accumulator
는 3이고currentValue
는 3입니다. 3 + 3 = 6을 반환합니다. - 그리고 최종 합계가 계산될 때까지 계속됩니다.
예제: 속성별로 객체 그룹화
reduce
를 사용하여 객체 배열을 특정 속성별로 그룹화된 객체로 변환할 수 있습니다. 사용자들을 `isActive` 상태별로 그룹화해 봅시다.
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true },
{ id: 4, name: 'David', isActive: false }
];
const groupedUsers = users.reduce((acc, user) => {
const status = user.isActive ? 'active' : 'inactive';
if (!acc[status]) {
acc[status] = [];
}
acc[status].push(user);
return acc;
}, {}); // 빈 객체 {}는 initialValue입니다.
console.log(groupedUsers);
/* 출력:
{
active: [
{ id: 1, name: 'Alice', isActive: true },
{ id: 3, name: 'Charlie', isActive: true }
],
inactive: [
{ id: 2, name: 'Bob', isActive: false },
{ id: 4, name: 'David', isActive: false }
]
}
*/
예제: 발생 횟수 계산
과일 목록에서 각 과일의 빈도를 계산해 봅시다.
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const fruitCounts = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(fruitCounts); // 출력: { apple: 3, banana: 2, orange: 1 }
4. Array.prototype.forEach()
forEach()
는 새 배열을 반환하지 않고 종종 더 명령형으로 간주되지만(주 목적은 각 배열 요소에 대해 함수를 실행하는 것이므로), 부작용이 필요하거나 변환된 출력을 필요로 하지 않고 반복할 때 함수형 패턴에 역할을 하는 기본적인 메서드입니다.
구문:
array.forEach(callback(element[, index[, array]])[, thisArg])
주요 특징:
undefined
를 반환합니다.- 배열의 각 요소에 대해 제공된 함수를 한 번 실행합니다.
- 콘솔에 로깅하거나 DOM 요소를 업데이트하는 등 부작용에 자주 사용됩니다.
예제: 각 요소 로깅
const messages = ['Hello', 'Functional', 'World'];
messages.forEach(message => console.log(message));
// 출력:
// Hello
// Functional
// World
참고: 변환 및 필터링의 경우 불변성 및 선언적 특성으로 인해 map
및 filter
가 선호됩니다. 새 구조로 결과를 수집하지 않고 각 항목에 대해 작업을 수행해야 하는 경우 forEach
가 더 적합한 선택입니다.
5. Array.prototype.find()
및 Array.prototype.findIndex()
이 메서드는 배열에서 특정 요소를 찾는 데 유용합니다.
find()
: 제공된 테스트 함수를 만족하는 제공된 배열의 첫 번째 요소의 값을 반환합니다. 어떤 값도 테스트 함수를 만족하지 않으면undefined
가 반환됩니다.findIndex()
: 제공된 테스트 함수를 만족하는 제공된 배열의 첫 번째 요소의 인덱스를 반환합니다. 그렇지 않으면 테스트를 통과한 요소가 없음을 나타내는 -1을 반환합니다.
예제: 사용자 찾기
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
const bob = users.find(user => user.name === 'Bob');
const bobIndex = users.findIndex(user => user.name === 'Bob');
const nonExistentUser = users.find(user => user.name === 'David');
const nonExistentIndex = users.findIndex(user => user.name === 'David');
console.log(bob); // 출력: { id: 2, name: 'Bob' }
console.log(bobIndex); // 출력: 1
console.log(nonExistentUser); // 출력: undefined
console.log(nonExistentIndex); // 출력: -1
6. Array.prototype.some()
및 Array.prototype.every()
이 메서드는 배열의 모든 요소가 제공된 함수에 의해 구현된 테스트를 통과하는지 테스트합니다.
some()
: 배열의 적어도 하나의 요소가 제공된 함수에 의해 구현된 테스트를 통과하는지 테스트합니다. 부울 값을 반환합니다.every()
: 배열의 모든 요소가 제공된 함수에 의해 구현된 테스트를 통과하는지 테스트합니다. 부울 값을 반환합니다.
예제: 사용자 상태 확인
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true }
];
const hasInactiveUser = users.some(user => !user.isActive);
const allAreActive = users.every(user => user.isActive);
console.log(hasInactiveUser); // 출력: true (Bob이 비활성이므로)
console.log(allAreActive); // 출력: false (Bob이 비활성이므로)
const allUsersActive = users.filter(user => user.isActive).length === users.length;
console.log(allUsersActive); // 출력: false
// every를 직접 사용하는 대안
const allUsersActiveDirect = users.every(user => user.isActive);
console.log(allUsersActiveDirect); // 출력: false
복잡한 작업을 위한 배열 메서드 체이닝
JavaScript 배열과 함수형 프로그래밍의 진정한 힘은 이러한 메서드를 함께 체이닝할 때 빛을 발합니다. 이러한 메서드의 대부분은 새 배열을 반환하므로(forEach
제외), 한 메서드의 출력을 다른 메서드의 입력으로 원활하게 파이프하여 우아하고 읽기 쉬운 데이터 파이프라인을 만들 수 있습니다.
예제: 활성 사용자 이름 찾기 및 ID 두 배로 만들기
활성 사용자를 모두 찾고 이름을 추출한 다음, 각 이름 앞에 필터링된 목록에서의 인덱스를 나타내는 숫자와 ID가 두 배가 된 새 배열을 만들어 보겠습니다.
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true },
{ id: 4, name: 'David', isActive: true },
{ id: 5, name: 'Eve', isActive: false }
];
const processedActiveUsers = users
.filter(user => user.isActive) // 활성 사용자만 가져오기
.map((user, index) => ({ // 각 활성 사용자 변환
name: `${index + 1}. ${user.name}`,
doubledId: user.id * 2
}));
console.log(processedActiveUsers);
/* 출력:
[
{ name: '1. Alice', doubledId: 2 },
{ name: '2. Charlie', doubledId: 6 },
{ name: '3. David', doubledId: 8 }
]
*/
이 체인 방식은 선언적입니다. 명시적인 루프 관리 없이 단계(필터링 후 매핑)를 지정합니다. 또한 각 단계가 새 배열 또는 객체를 생성하여 원본 users
배열을 그대로 두기 때문에 불변합니다.
불변성 실천
함수형 프로그래밍은 불변성에 크게 의존합니다. 즉, 기존 데이터 구조를 수정하는 대신 원하는 변경 사항으로 새 구조를 만듭니다. map
, filter
, slice
와 같은 JavaScript 배열 메서드는 새 배열을 반환함으로써 본질적으로 이를 지원합니다.
불변성이 왜 중요한가요?
- 예측 가능성: 공유된 가변 상태의 변경 사항을 추적할 필요가 없으므로 코드를 더 쉽게 추론할 수 있습니다.
- 디버깅: 버그가 발생할 때 데이터가 예상치 않게 수정되지 않는 경우 문제의 원인을 쉽게 파악할 수 있습니다.
- 성능: 특정 컨텍스트(Redux와 같은 상태 관리 라이브러리 또는 React)에서는 불변성이 효율적인 변경 감지를 허용합니다.
- 동시성: 불변 데이터 구조는 본질적으로 스레드 안전하여 동시 프로그래밍을 단순화합니다.
전통적으로 배열을 수정하는 작업(요소 추가 또는 제거와 같은)을 수행해야 할 때 slice
, 스프레드 구문(...
)과 같은 메서드를 사용하거나 다른 함수형 메서드를 결합하여 불변성을 달성할 수 있습니다.
예제: 불변 방식으로 요소 추가
const originalArray = [1, 2, 3];
// 명령형 방식 (originalArray를 수정함)
// originalArray.push(4);
// 스프레드 구문을 사용한 함수형 방식
const newArrayWithPush = [...originalArray, 4];
console.log(originalArray); // 출력: [1, 2, 3]
console.log(newArrayWithPush); // 출력: [1, 2, 3, 4]
// slice 및 연결을 사용한 함수형 방식 (이제는 덜 일반적임)
const newArrayWithSlice = originalArray.slice(0, originalArray.length).concat(4);
console.log(newArrayWithSlice); // 출력: [1, 2, 3, 4]
예제: 불변 방식으로 요소 제거
const originalArray = [1, 2, 3, 4, 5];
// 인덱스 2 (값 3)의 요소 제거
// slice 및 스프레드 구문을 사용한 함수형 방식
const newArrayAfterSplice = [
...originalArray.slice(0, 2),
...originalArray.slice(3)
];
console.log(originalArray); // 출력: [1, 2, 3, 4, 5]
console.log(newArrayAfterSplice); // 출력: [1, 2, 4, 5]
// filter를 사용하여 특정 값 제거
const newValueToRemove = 3;
const arrayWithoutValue = originalArray.filter(item => item !== newValueToRemove);
console.log(arrayWithoutValue); // 출력: [1, 2, 4, 5]
모범 사례 및 고급 기법
함수형 배열 메서드에 더 익숙해지면 이러한 모범 사례를 고려해 보세요.
- 가독성 우선: 체이닝은 강력하지만 지나치게 긴 체인은 읽기 어려울 수 있습니다. 복잡한 작업을 더 작고 이름이 지정된 함수로 분리하거나 중간 변수를 사용하는 것을 고려하십시오.
- `reduce`의 유연성 이해:
reduce
는 단일 값뿐만 아니라 배열이나 객체를 구축할 수 있음을 기억하십시오. 이것은 복잡한 변환에 대해 매우 다재다능합니다. - 콜백에서 부작용 피하기:
map
,filter
,reduce
콜백을 순수하게 유지하도록 노력하십시오. 부작용이 있는 작업을 수행해야 하는 경우forEach
가 종종 더 적합한 선택입니다. - 화살표 함수 사용: 화살표 함수(
=>
)는 콜백 함수에 대한 간결한 구문을 제공하고this
바인딩을 다르게 처리하므로 종종 함수형 배열 메서드에 이상적입니다. - 라이브러리 고려: 더 고급 함수형 프로그래밍 패턴을 사용하거나 불변성을 광범위하게 사용하는 경우 Lodash/fp, Ramda 또는 Immutable.js와 같은 라이브러리가 유용할 수 있지만 최신 JavaScript에서 함수형 배열 작업을 시작하는 데는 엄격하게 필요하지는 않습니다.
예제: 데이터 집계에 대한 함수형 접근 방식
다른 지역의 판매 데이터가 있고 각 지역별 총 판매액을 계산한 다음 판매액이 가장 많은 지역을 찾고 싶다고 가정해 봅시다.
const salesData = [
{ region: 'North', amount: 100 },
{ region: 'South', amount: 150 },
{ region: 'North', amount: 120 },
{ region: 'East', amount: 200 },
{ region: 'South', amount: 180 },
{ region: 'North', amount: 90 }
];
// 1. reduce를 사용하여 지역별 총 판매액 계산
const salesByRegion = salesData.reduce((acc, sale) => {
acc[sale.region] = (acc[sale.region] || 0) + sale.amount;
return acc;
}, {});
// salesByRegion은 다음과 같습니다: { North: 310, South: 330, East: 200 }
// 2. 집계된 객체를 추가 처리를 위해 객체 배열로 변환
const salesArray = Object.keys(salesByRegion).map(region => ({
region: region,
totalAmount: salesByRegion[region]
}));
// salesArray는 다음과 같습니다: [
// { region: 'North', totalAmount: 310 },
// { region: 'South', totalAmount: 330 },
// { region: 'East', totalAmount: 200 }
// ]
// 3. reduce를 사용하여 판매액이 가장 많은 지역 찾기
const highestSalesRegion = salesArray.reduce((max, current) => {
return current.totalAmount > max.totalAmount ? current : max;
}, { region: '', totalAmount: -Infinity }); // 매우 작은 숫자로 초기화
console.log('Sales by Region:', salesByRegion);
console.log('Sales Array:', salesArray);
console.log('Region with Highest Sales:', highestSalesRegion);
/*
출력:
Sales by Region: { North: 310, South: 330, East: 200 }
Sales Array: [
{ region: 'North', totalAmount: 310 },
{ region: 'South', totalAmount: 330 },
{ region: 'East', totalAmount: 200 }
]
Region with Highest Sales: { region: 'South', totalAmount: 330 }
*/
결론
JavaScript 배열을 사용한 함수형 프로그래밍은 단순한 스타일 선택이 아니라 더 깨끗하고 예측 가능하며 강력한 코드를 작성하는 강력한 방법입니다. map
, filter
, reduce
와 같은 메서드를 활용함으로써 불변성 및 순수 함수와 같은 함수형 프로그래밍의 핵심 원칙을 준수하면서 데이터를 효과적으로 변환, 쿼리 및 집계할 수 있습니다.
JavaScript 개발 여정을 계속하면서 이러한 함수형 패턴을 일상적인 워크플로에 통합하면 의심할 여지 없이 더 유지 관리 가능하고 확장 가능한 애플리케이션을 얻을 수 있습니다. 프로젝트에서 이러한 배열 메서드를 실험하는 것부터 시작하면 곧 그 엄청난 가치를 발견하게 될 것입니다.