arr.reduce(callback(accumulator, currentValue, index, array), initialValue);
// 배열.reduce(callback(누적값, 현재값, 인덱스, 요소), 초기값);
reduce는 빈 요소를 제외하고 배열 내 에 존재하는 각 요소에 대해 callback 함수를 한 번씩 실행하며, 네 가지 인수를 받는다.
accumulator
currentValue
currentIndex
array
callback : 배열의 각 요소에 대해 실행할 함수, 다음 네 가지 인수를 받는다.
initialVlue (optional): callback의 최초 호출에서 첫 번째 인수에 제공하는 값, 초기값을 제공하지 않으면 배열의 첫 번째 요소를 사용
const arr = [1, 2, 3, 4, 5]
arr.reduce((acc, cur, idx)=>{
console.log(acc, cur, idx);
return acc + cur;
}, 0)
// acc cur idx
// 0 1 0
// 1 2 1
// 3 3 2
// 6 4 3
// 10 5 4
코드와 출력 결과를 살펴보자.
초기값(0
)에 누적값(acc
)에 배열의 요소(cur
)를 더한 값을 return
하도록 하는 코드다.
초기값을 0으로 주었으므로 첫 번째 순서에 0 + 1을 return 한다. 그리고 출력값을 보면 return 된 값은 누적값(acc
)로 할당되는 걸 확인할 수 있다. 즉, 더해진 return 값은 순차적으로 누적값(acc)로 전달된다.
만약 초기값을 정해주지 않았다면 배열의 0번 index부터 시작한다. 아래의 코드와 위의 코드는 의미가 같다.
const arr = [1, 2, 3, 4, 5]
arr.reduce((acc, cur, idx)=>{
console.log(acc, cur, idx);
return acc + cur;
})
// acc cur idx
// 0 1 0
// 1 2 1
// 3 3 2
// 6 4 3
// 10 5 4
const arr = [{ value: 1 }, { value: 2 }, { value: 3 }];
const result = arr.reduce((acc, cur, idx) => {
console.log(idx + `번째 idx`);
console.log('acc: ' + acc +', cur: ' + cur.value + "\n");
return acc + cur.value;
}, 0);
console.log('result : ', result);
//0번째 idx
//acc: 0, cur: 1
//1번째 idx
//acc: 1, cur: 2
//2번째 idx
//acc: 3, cur: 3
//result : 6
reduce 함수는 객체 배열에서도 사용이 가능하다.
다만 초기값(acc)을 설정하지 않으면, 초기값이 원시값이 아닌 객체이므로 정상적인 계산이 되지 않는다. 아래 코드를 보자
const arr = [{ value: 1 }, { value: 2 }, { value: 3 }];
const result = arr.reduce((acc, cur, idx) => {
console.log(idx + `번째 idx`);
console.log('acc: ' + acc +', cur: ' + cur.value + "\n");
return acc + cur.value;
}, 0);
console.log('result : ', result);
//1번째 idx
//acc: [object Object], cur: 2
//2번째 idx
//acc: [object Object]2, cur: 3
//result : [object Object]23
const arr = [1, 2, 3, 4, 5]
const result = arr.reduce((acc, cur) => {
acc.push(cur % 2 ? "홀수" : "짝수");
return acc;
}, []);
console.log(result);
// [ '홀수', '짝수', '홀수', '짝수', '홀수' ]
map함수는 기존 배열과 다른 새 배열(다른 객체)을 리턴한다. reduce도 초기값을 빈 배열로 제공해주고, push와 같은 배열 메서드를 사용하면 map처럼 새로운 배열을 반환할 수 있다.
const users = {
id: ["whalstn9", "dkLim", "bh123"],
psWord: ["11111", "22222", "33333"],
name: ["Minsu", "Deagun", "Byungho"],
};
const id = users.id; //id 배열
const inputID = id[Math.floor(Math.random() * id.length)]; //임의의 id
const idx = users.id.indexOf(inputID); // 임의 id의 인덱스
const usersKeys = Object.keys(users); // users 객체의 key배열
const userInfo = usersKeys.reduce((newUser, info) => {
newUser[info] = users[info][idx];
return newUser;
}, {});
console.log(userInfo);
먼저 users라는 객체안에 id, psWord, name 값이 저장되어있다. 실제 프로젝트일 경우 당연히 DB를 통해 불러올 것으로 가정한다.
우리는 해당 코드를 통해 일정 id값에 따른 같은 인덱스에 위치하는 psWord와 name을 포함하는 object를 받고자한다. 그에 따라 그 index를 결정하기 위해 indexOf()을 이용해서 랜덤하게 받아온 id값의 index를 idx 변수에 넣어준다.
이제 reduce( )구문을 해석할 건데 앞서 reduce는 array를 순회한다. 즉 array 가 필요할 것이다. 그러므로 users 객체의 key값인 id, psWord, name을 array로 만들어준다. ( Object.keys()이용 )
usersKeys = ["id","psWord","name"]
accumulator(누적값)로 newUser
를 받고, currentValue(현재값)로 info
를 받게한다.
첫번째 순환에서는 acc인 newUser
는 초깃값이 된다. 즉 빈 object인 "{ }"이다. 그리고 현재값 info
는 "id"일 것이므로
newUser[info] = users[info][idx];
이 코드를 통해 newUser["id"] = users["id"][idx]와 같이 처리될 것이다.
즉, 만약 랜덤하게 받아온 id값이 "Nemoo"라면 첫 번째 순환에선 리턴값으로
{id : "whalstn9"}
를 가지게 될 것이다. id라는 key를 한번 순회했고 이것이 리턴값인 newUser인 것이다. 즉 리턴값 newUser은 다시 acc부분 newUser 파라미터로가 userKeys
배열을 다 순회할까지 반복
한다.
그럼 두 번째 순환에서 리턴 값 newUser
은
{id : "whalstn9", psWord : "11111"}
{id : "whalstn9", psWord : "11111", name: "Minsu"}
우린 이처럼 reduce()를 이용해 array를 object형태로 필요한 요소만을 grouping 할 수 있게 된 것이다.
- 요약
- 특정 키의 구분값 지정(id key의 특정 값-> inputID)
- 특정 구분자의 index추출(inputID의 index)
- key값 배열 추출
- 해당 key 배열을 reduce로 순회
- 순회 내부의 리턴값을 원본 객체 내 각각 배열의 인덱스에서 추출하여 새로운 key값에 대입
- 결국 원본 객체와 같은 키속성을 가진 매핑 데이터를 추출할 수 있다.
const users = {
id: ["minsu", "minsu", "minsu", "deagun", "huysu", "huysu"],
};
const id = users.id;
let countedNames = id.reduce((allNames, name) => {
if (name in allNames) {
allNames[name]++;
} else {
allNames[name] = 1;
}
return allNames;
}, {});
console.log(countedNames);
중복된 이름의 아이디가 존재하는 id의 값에 해당하는 array가 있다.
코드를 해석해보자.
누적값 allNames
에 현재값 name
이 포함되있다면 value
값을 1씩 증가
시키고 그렇지않다면 1
을 value값으로 가지는 object
를 만들게 된다.
이 코드는 if 내부보다는 else 내부를 먼저 고려해야하는데 처음에는 초기값인 빈 object "{ }"에 첫 번째 요소인 "minsu"가 key값으로 들어가게 되고 그때 value값은 1이 될 것이다.
else {
allNames[name] = 1;
}
처음 순환에는 allNames(빈 오브젝트)안에 들어가게 될 "minsu"요소가 하나밖에 없고 즉, 현재값 name
이 allNames
에 포함되지 않은 상태이므로
{minsu : 1}
다음과 같은 오브젝트를 얻게 되고 return allNames
를 통해 다시 reduce의 누적값
으로 들어가 순환을 시작한다.
if (name in allNames) {
allNames[name]++;
}
두 번째 순환에는 "minsu" 요소가 이미 오브젝트에 포함되어 있으므로 기존 minsu의 value값 1에서 1증가한 2가 되는 것이다.
{minsu : 2}
이런식으로 reduce()를 전부 순환하면서 다음과 같은 object
를 얻게 된다.
{ minsu : 3, deagun: 1, huysu: 2 }
- 요약
- 객체 배열 따로 추출(id 변수)
- id 배열을 reduce로 순회
- 조건문으로 allNames라는 누적 값에 현재값 name이 포함돼있다면 해당 키의 값에서 +1
- 포함돼있지 않으면 신규 키값 추가 및 1로 초기화
- 순회 반복 후 countedNames에 리턴
앞서 Counting
의 연장선 상에서 생각할 수 있는데 만약 유저가 회원가입을 하는 데 있어 이미 다른 유저가 등록한 중복된 아이디를 입력할 경우 해당 id값을 가질 수 없도록 취해 주어야 할 것이다.
간단한 reduce()
를 이용한 로직을 통해 중복 항목 제거를 구현해보자.
const users = {
id: ["minsu", "minsu", "minsu", "deagun", "huysu", "huysu"],
};
const id = users.id;
let removeDuplicated = id.reduce((allNames, name, index) => {
if (allNames.includes(name)) {
delete allNames[index];
} else {
allNames[index] = name;
}
return allNames;
}, []);
const newArr = removeDuplicated.filter((e) => {
return e !== "";
});
console.log(newArr); // ['minsu', 'deagun', 'huysu']
첫 번째 로직은 작성자 본인이 가장 먼저 작성해보았던 중복 항목 제거 로직이다. reduce()
부분과 fillter()
부분으로 나누어져있다.
reduce 부분을 해석하자면 카운팅 부분과 마찬가지로 allNames가 name를 포함한다면 중복항목을 제거하고 그렇지 않다면 name을 allNames배열에 포함
시켜주는 식으로 진행하였다.
delete allNames[index];
다음과 같이 delete를 이용해 현재 index에 대한 배열의 요소값을 제거하도록 하였다.
이렇게 reduce()
로직만 구현한 뒤 console창에서 출력한 결과 원하는 결과인 중복 항목 제거에 성공한 줄 알았지만 다음과 같이
배열에 중복항목들이 index를 가지고 있는 empty
값으로 출력되게 되었다.
empty가 되면서 중복항목들이 제거가 된 것은 맞지만 인덱스를 가지는 배열 요소로써 포함되므로 완전히 중복항목들이 제거되었다고 할 수는 없다.
그렇다면 왜 다음과 같이 empty가 나오게 된 것일까?
delete를 어원 그대로 해석해서 배열요소를 그냥 삭재해준다고 생각할 수도 있지만 delete로 배열의 요소를 제거할 시 배열의 길이는 그대로이게 된다.
즉, delete연산자는 배열 요소의 삭제가 아닌 빈 값으로 변경
하기 때문에 삭제보다는 변경에 가까운 개념이다.
결국 filter 구문을 추가
함으로써 empty부분을 제거해 완전한 중복항목 제거된 배열을 생성할 수 있었다.
const users = {
id: ["minsu", "minsu", "minsu", "deagun", "huysu", "huysu"],
};
const id = users.id;
let removeDuplicated = id.reduce((allNames, name, index) => {
if (allNames.indexOf(name) === -1) {
allNames.push(name);
}
return allNames;
}, []);
console.log(removeDuplicated); // ['minsu', 'deagun', 'huysu']
이번에는 reduce
만으로 중복요소 제거를 성공한 코드이다. indexOf
를 사용해 allNames
안에 name
이 없을 때 (index ==== -1일 때)
push
를 사용해 allNames
안에 name
을 넣어주도록 코드를 작성하였다.
reduce()를 사용해서 데이터를 단계별로 변환하는 함수 파이프라인을 생성할 수 있다.
const add5 = (x: number): number => x + 5;
const multiply3 = (x: number): number => x * 3;
const subtract2 = (x: number): number => x - 2;
const composedFunctions: ((x: number) => number)[] = [add5, multiply3, subtract2];
const result: number = composedFunctions.reduce((acc, curr) => curr(acc), 10);
console.log(result); // Output: 43
이 예시에서, 초기값 10에 차례로 적용하길 원하는 함수 배열을 가지고 있다. (composedFunctions) . reduce()
메서드를 사용해서 각 함수의 결과를 다음 함수의 입력으로 전달하고, 모든 함수를 적용한 결과값을 갖는다.
reduce() 메서드를 간단한 Redux 같은 상태관리 시스템을 구현하는데에 사용할 수 있다.
interface State {
count: number;
todos: string[];
}
interface Action {
type: string;
payload?: any;
}
const initialState: State = {
count: 0,
todos: [],
};
const actions: Action[] = [
{ type: 'INCREMENT_COUNT' },
{ type: 'ADD_TODO', payload: 'Learn Array.reduce()' },
{ type: 'INCREMENT_COUNT' },
{ type: 'ADD_TODO', payload: 'Master TypeScript' },
];
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'INCREMENT_COUNT':
return { ...state, count: state.count + 1 };
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
default:
return state;
}
};
const finalState: State = actions.reduce(reducer, initialState);
console.log(finalState);
/*
Output:
{
count: 2,
todos: ['Learn Array.reduce()', 'Master TypeScript']
}
*/
현재 state
와 action
을 파라미터로 받아서, action type
에 따라 새로운 state를 리턴하는 reducer
함수를 정의했다.
reduce()
메서드를 사용해서, 각 action
을 state
에 적용해서 최종 state
에 도달합니다. 이 방식은 mini-Redux를 사용하는 것과 같다.
const grades: number[] = [85, 90, 92, 88, 95];
const average: number = grades.reduce((acc, curr, index, array) => {
acc += curr;
if (index === array.length - 1) {
return acc / array.length;
}
return acc;
}, 0);
console.log(average); // Output: 90
acc
초기값을 0으로 설정했다. 각 요소(성적)을 반복해서 acc에 추가한다. 마지막 요소(array.length
를 이용해서 체크)에서 누산기 결과값을 총 개수를 나누어 평균을 계산한다.