함수형 프로그래밍은 함수의 출력 값이 오직 함수로 전달된 인자에만 의존하는 프로그래밍 패러다임으로, 동일한 인자를 사용해 함수를 호출하면 항상 같은 결과를 반환한다.
이를 참조 투명성이라고 하며, 함수형 프로그래밍의 핵심 개념 중 하나이다.
배열 처리 함수 map()
, filter()
, reduce()
는 배열을 변형하거나 처리할 때, 상태를 변경하지 않고 입력된 배열의 복사본을 기반으로 동작하며, 원본 배열을 변경하지 않기 때문에 순수 함수
의 개념에 부합하다.
이를 통해 함수 실행마다 예측 가능한 결과를 얻을 수 있다.
전통적인 명령형 프로그래밍에서는 전역 변수나 로컬 변수를 사용해 상태를 변경하는데, 이는 side-effect를 발생시킬 가능성을 높인다.
함수 실행 결과가 매번 다르면 코드의 동작을 이해하고 디버깅하기 어려워진다.
배열 처리 함수들은 이러한 부수 효과를 제거하면서, 복잡한 로직을 간결하게 구현할 수 있도록 도와준다.
map()
클라이언트는 서버로부터 받은 데이터를 변환해 사용하는 경우가 많다.
map
은 배열의 각 요소를 다른 값으로 변환하여 새로운 배열을 생성하며, 주로 배열의 요소를 다른 형태로 변환할 때 사용된다.
map
은 인자로 콜백을 받는다. map이 호출될 때, 이 콜백에 현재 값의 iteration, iteration의 index 그리고 원본 배열이 주어진다.
map을 위한 optional한 두 번째 인자도 있다. 이는 콜백 내부에서 this를 이용하기 위한 값이다.
const nums = [1, 2, 3];
const doubled = nums.map((num, index, array) => {
return num * 2;
});
console.log(doubled); // [2, 4, 6]
아래 자바스크립트 오브젝트 배열이 있다고 하자.
const Moles = [
{ id: 1, name: "jomboo", position: "first" },
{ id: 2, name: "yuna", position: "second" },
{ id: 3, name: "zhoo", position: "third" },
{ id: 4, name: "yubin", position: "na" },
];
name 값만 모아보자.
const allMoleNames = Moles.map((Mole) => {
return Mole.name;
});
console.log(allMoleNames); // ["jomboo", "yuna", "zhoo", "yubin"];
유틸 함수도 껴볼 수 있다.
const upperCaseMoles = Moles.map(myMoleFunc);
const myMoleFunc = (Mole) => {
return Mole.name.toUpperCase();
};
console.log(upperCaseMoles); // ["JOMBOO","YUNA","ZHOO","YUBIN"];
프로퍼티를 삭제하거나 추가해보자.
const mapped = Moles.map((Mole) => {
const { position, ...rest } = Mole;
return {
...rest,
ThisMoleIs: "CoolAndWarm",
};
});
console.log(mapped);
// [
// { id: 1, name: "jomboo", ThisMoleIs: "CoolAndWarm" },
// { id: 2, name: "yuna", ThisMoleIs: "CoolAndWarm" },
// { id: 3, name: "zhoo", ThisMoleIs: "CoolAndWarm" },
// { id: 4, name: "yubin", ThisMoleIs: "CoolAndWarm" },
// ];
filter()
배열의 각 요소에 대해 조건을 평가한 후, 조건에 맞는 요소들만을 포함하는 새로운 배열을 생성한다.
filter
는 map과 동일한 인자를 받으며, 비슷하게 동작한다.
유일한 차이점은 콜백 함수가 true
또는 false
를 반환하는 것이다.
true
를 반환하면 해당 원소가 배열에 남고, false
이면 필터링되어 제거된다.
const nums = [1, 2, 3, 4];
const even = nums.filter((num, index, array) => {
return num % 2 === 0;
});
console.log(even); // [2, 4]
간단한 문자열 검색허기
const strings = ["hello", "Matt", "Mastodon", "Cat", "Dog"];
const filtered = strings.filter((str) => {
return str.includes("at");
});
console.log(filtered); // ["Matt", "Cat"];
Moles
에서 나를 찾아보자.
const naMole = Moles.filter((Mole) => {
return Mole.position.toUpperCase() === "NA";
});
console.log(naMole);
reduce()
배열 하나를 받아서 하나의 값으로 바꿔준다.
map
, filter
와 비슷하게 동작한다. 콜백 인자로 accumulator를 받는다는 점에서 차이가 있다.
모든 반환 값을 누적하는 역할으로, reduce
함수의 두 번째 인자 값이 accumulator 초기값이다.
const nums = [1, 2, 3, 4];
const sum = nums.reduce((acc, curr) => {
return acc + curr;
}, 0);
const avg = sum / nums.length;
console.log(sum); // 10
console.log(avg); // 2.5
새로운 Moles
자바스크립트 오브젝트 배열을 가져왔다.
const Moles = [
{ id: 1, name: "jomboo", team: "fa" },
{ id: 2, name: "yuna", team: "bi" },
{ id: 3, name: "zhoo", team: "tb" },
{ id: 4, name: "yubin", team: "fa" },
];
..어디 들어들있는지 알아보자.
const obj = Moles.reduce((acc, curr) => {
const team = curr.team;
const teamCount = acc[team] ? acc[team] + 1 : 1;
return {
...acc,
[team]: teamCount,
};
}, {});
console.log(obj); // { "fa": 2, "bi": 1, "tb": 1};
새로운 두더지를 영입했다.
const newMoles = [Moles, [{ id: 5, name: "hanbin", team: "banggoo" }]];
const flatnewMoles = newMoles.reduce((acc, curr) => {
return acc.concat(curr);
}, []);
console.log(flatnewMoles);
// [
// { id: 1, name: "jomboo", team: "fa" },
// { id: 2, name: "yuna", team: "bi" },
// { id: 3, name: "zhoo", team: "tb" },
// { id: 4, name: "yubin", team: "fa" },
// { id: 5, name: "hanbin", team: "banggoo" },
// ]
map(), filter(), reduce()
위의 세 함수를 연계하여 사용할 수 있다.
2의 배수만 필터링하고, 각 요소를 제곱하여, 총합을 구해보자.
const numbers = [1, 2, 3, 4, 5];
const result = numbers
.filter((num) => num % 2 === 0) // 2의 배수 필터링
.map((num) => num ** 2) // 각 요소 제곱
.reduce((acc, curr) => acc + curr, 0); // 총합 계산
console.log(result); // 20
조금 더 복잡한 배열을 가져왔다. 각각의 노래는 초로 표기된 길이를 갖는다.
const day6Songs = [
{ id: 1, name: "nothing but", artist: "Young K", duration: 208 },
{ id: 2, name: "Unpainted Canvas", artist: "WONPIL", duration: 229 },
{ id: 3, name: "How to love", artist: "Day6", duration: 204 },
{
id: 4,
name: "Right Through Me",
artist: "Day6 (Even of Day)",
duration: 218,
},
];
const btobSongs = [
{ id: 1, name: "SWIMMING", artist: "임현식", duration: 261 },
{ id: 2, name: "AT THE END", artist: "이창섭", duration: 230 },
{ id: 3, name: "Dear My Dear", artist: "서은광", duration: 269 },
{ id: 4, name: "BE SOMEBODY", artist: "육성재", duration: 183 },
];
200초가 넘는 모든 노래의 제목을 컴마로 구분하는 배열을 가져와야 한다면?
const allSongs = [day6Songs, btobSongs];
const songNames = allSongs
// 배열 평탄화. const allSongs = [...day6Songs, ...btobSongs];와 같다.
.reduce((acc, curr) => {
return acc.concat(curr);
}, [])
// 200초 이상 노래 필터링
.filter((song) => song.duration > 200)
// 노래 이름만 추출
.map((song) => song.name)
// 컴마로 구분된 문자열 생성
.join(", ");
console.log(songNames); // nothing but, Unpainted Canvas, How to love, Right Through Me, SWIMMING, AT THE END, Dear My Dear
map
, filter
, reduce
를 함께 사용하면 array[i]
와 같은 형식보다 현재 값에 직접 접근하기 쉬워진다.
기존 배열의 변화를 방지하여 side-effect를 최소화할 수 있으며, for 문
을 관리할 필요가 없어지고 빈 배열을 생성해 push할 필요도 없어진다.
배열 처리 함수는 함수형 프로그래밍의 원칙을 잘 반영하여 side-effect를 최소화하고 코드의 가독성을 향상시키는 데 강점을 가지고 있다. map
은 현재 값의 iteration, iteration의 index, 그리고 원본 배열을 인자로 받는다. filter
는 map과 동일한 인자를 받지만, 반환값이 boolean이라는 차이가 있다. reduce
는 콜백 인자로 모든 반환 값을 누적하는 accumulator와 accumulator의 초기값을 인자로 받는다.