😎 reduce는 그냥 더하기 셔틀이 아니다.
https://www.zerocho.com/category/JavaScript/post/5acafb05f24445001b8d796d
이 글이 아주 감명 깊었다.
대부분의 배열 메서드는 약간의 수고를 들이면 reduce로 동일하게 구현해낼 수 있다.
즉 모든 배열 고차 함수들의 부모는 reduce나 다름없다..
사실 reduce는 대부분 배열합계 구하기 셔틀로 사용하곤한다.
나 역시도 reduce를 가장 많이 사용하는 곳은 더하기 셔틀이다..
하지만 reduce는 그 자유도가 높은 만큼 활용도 역시 아주 높다.
다만 이해하는데에 약간의 장벽이 있다.
따라서 그냥 더하기 셔틀로 써도 무방하지만..
알고 있으면 아주 간지난다.
😎reduce는 정확히 보면 이렇다.
MDN을 보면 이렇게 이해하기 난해한 말로 쭉 설명을 해준다.
읽기 싫기도하고 읽어도 이해가 잘 안된다. 일단 나는 그랬음
arr.reduce( function(acc,cur,idx,src){} , 초기값)
보통은 화살표 함수로 굉장히 축약해서 쓰기때문에
헷갈리는 것이지만 길게 쭉 풀어쓰면 쉽게 이해할 수 있습니다.
총 두개의 인자를 넣을 수 있다. 첫번째 인자에는 함수를 두번째 인자에는 초기값을 지정해줄수있다.
근데 다른 배열 메서드와 달리 초기값을 넣을 수 있는 이유가 있겠죠?
초기값을 넣을 수 있는 이유는.. reduce에서 초기값이 굉장히 중요하게 사용되기 때문입니다.
그 이유는 후략합니다.
😎만만한 idx,src 부터 보면
let a = [1, 2, 3, 4, 5];
a.reduce((acc, cur, idx, src) => {
console.log('idx값은 이렇습니다', idx);
console.log('src값은 이렇습니다', src);
}, 0);
/*
idx값은 이렇습니다 0
src값은 이렇습니다 [ 1, 2, 3, 4, 5 ]
idx값은 이렇습니다 1
src값은 이렇습니다 [ 1, 2, 3, 4, 5 ]
idx값은 이렇습니다 2
src값은 이렇습니다 [ 1, 2, 3, 4, 5 ]
idx값은 이렇습니다 3
src값은 이렇습니다 [ 1, 2, 3, 4, 5 ]
idx값은 이렇습니다 4
src값은 이렇습니다 [ 1, 2, 3, 4, 5 ]
*/
idx는 인덱스 src는 원본 배열을 뜻합니다.
감이 안오실 수 있으니 console.을 찍어보겠습니다
여러분도 어떻게 동작할지 감이 안올 땐 콘솔을 찍어보는 습관을 들여보세요
for문으로 치환해서 보면 이해가 쉽습니다
idx값은 for문의 i src값은 reduce를 돌리는 배열 전체를 의미하네요
reduce 메서드는 배열의 길이-1 만큼 인자로 받은 콜백함수를 호출합니다.
이걸 잘 기억해두세요
😎acc,cur값을 봅시다.
let a = [1, 2, 3, 4, 5];
a.reduce((acc, cur) => {
console.log('acc값입니다', acc);
console.log('cur값입니다', cur);
acc = acc + cur;
console.log('cur을 더해준 acc값입니다.', acc);
return acc;
}, 0);
/*
acc값입니다 0
cur값입니다 1
cur을 더해준 acc값입니다. 1
acc값입니다 1
cur값입니다 2
cur을 더해준 acc값입니다. 3
acc값입니다 3
cur값입니다 3
cur을 더해준 acc값입니다. 6
acc값입니다 6
cur값입니다 4
cur을 더해준 acc값입니다. 10
acc값입니다 10
cur값입니다 5
cur을 더해준 acc값입니다. 15
*/
우선 초기값을 넘겨준 상태의 예제입니다.
콘솔은 총 3번을 한번 순회에서 출력하며
처음엔 초기값으로 지정해준 0이 acc값으로 들어가있고
acc = acc + cur을 해준 다음
acc를 return하는 문장입니다.
자 저 콜백함수가 여러번 실행됩니다. 만약 초기값을 주지 않으면
배열의 맨 첫번째 요소가 초기값으로 지정되고 배열의 두번째 요소부터 순회를 시작하는 특징이 있습니다.
😎그냥 한줄로 더하기 셔틀 시킬땐 내가 밥처럼 보였지?
[1,2,3,4,5].reduce((acc,cur) => acc + cur)
ㅋㅋ;; 사실 저건 화살표 함수가 극한의 축약을 도와주기 때문에
한줄로 처리할 때는 중괄호도 생략할 수 있고 return문도 생략할 수 있기때문에
저렇게 처리하는게 가능하다는것 그러니까 사실 저 구문은 아래의 이것과 같은 의미입니다.
그리고 return문은 reduce에서 매우매우매우매우매우매우 중요한 요소입니다.
[1,2,3,4,5].reduce(function(acc,cur){return acc + cur})
😎acc에 대한 오해
acc는 누적값이다. 하지만 우리가 그냥 생각하는 누적값과는 좀 거리가 있습니다.
acc는 정확하게 말하면
이전 콜백함수가 리턴한 반환 값을 의미합니다.
이전 콜백함수가 리턴한 반환 값을 사용해서 그 값에 뭔가를 누적해주고..
또 그 다음 콜백함수는 이전 콜백함수와 이이전 콜백함수가 쌓아놓은 값에 또 값을 누적하고..
뭐 이런개념이라면 누적... 인것같기도..? 하지만
그냥 누적값이구나 하고 넘어가면 저 누적값이 어떤 값인지.
누적값이 어떻게 결정되는 건지 이해가 안되어 버리는 모순에 빠지게됩니다.
따라서 이전 콜백함수가 리턴한 반환값이라는 것을 기억을 하고
그런데 reduce의 첫 순회에선 "이전" 콜백함수가 존재할 수 없다는 모순이 있습니다.
그 모순을 해결하기 위해 초기값을 설정하거나 배열의 맨 첫번째 요소를 초기값으로 사용하는 것입니다.
[1, 2, 3, 4, 5].reduce((acc, cur) => {
console.log(acc);
console.log(cur)
return acc;
});
위 코드에서 첫번째 콜백에 acc, cur 값은 얼마일까요?
맞추면 칭찬스티커 드리겠습니다.
처음엔 acc가 1 cur이 2입니다.
초기값을 따로 설정해주지 않았기 때문에 배열의 맨 첫번째 요소가 초기값으로 사용되는 것
그런데 만약 초기값을 설정해준다면?
[1, 2, 3, 4, 5].reduce((acc, cur) => {
console.log(acc);
console.log(cur)
return acc;
},0);
이 경우 첫번째 콜백에서 acc 는 0이고 cur은 1입니다.
그래서 reduce를 사용할 때..그리고 실전 코딩테스트에서 매우 유의할 점이 있습니다.
빈 배열이 들어오는 경우 초기값 설정이 없는 reduce는
이니셜 밸류(초기값)으로 사용할 요소가 없기 때문에 그에 해당하는 오류를 반환합니다.
😎빈배열에 초기값 없는 reduce를 사용하면?
[].reduce((acc, cur) => {
return acc + cur;
});
이렇게 되면 출력결과는.... 다음과 같습니다.
TypeError: Reduce of empty array with no initial value
TypeError는 TypeError인데 처음 보는 유형의 에러메시지가 나옵니다.
Reduce of empty array with no initail value
리듀스를 하는 배열이 요소도 없는 빈 배열이면서 이니셜밸류(초기값)도 지정되어 있지 않다.
그래서 TypeError가 발생했다. 라는 문구입니다.
이제.. 왜 코테 문제를 간지나게 풀려고 reduce를 써봤는데
종종 오류가 터지면서 내 논리적 오류없는 답안이 틀렸다 나오는지 이해가 되지않나요?
😎실전에서 활용하는 reduce의 강력함
https://school.programmers.co.kr/learn/courses/30/lessons/120853
생각 난 김에 예전에 풀었던 0레벨 문제 중
reduce를 활용하면 깔끔하게 풀 수 있는 문제를 하나 골라서 풀어봤습니다.
문제 링크는 위와 같습니다.
컨트롤제트라는 문제이고 level0 입니다.
문제 설명을 하면
"-1 -3 Z 3" 이런식으로 공백을 기준으로 나눠진 문자열이 주어지고
안에 들어있는건 음의 정수, 양의 정수, Z입니다.
기본적으론 값을 서로 더하면서 가는데 Z를 만나면 이전 숫자를 누적값에서 빼주는 문제입니다.
아주 간단하지만 누적값이라는 점에서 reduce를 활용하기 아주 좋아보이는 문제네요
한번 reduce를 활용해서 풀어보려고 해보시면 재밌을 것 같습니다.
추천드려요
function solution(s) {
return s.split(' ').reduce((acc,cur,idx,src) => {
return cur === "Z" ? acc -= src[idx-1] : acc += parseInt(cur)},0)
}
초기값을 설정해주고 현재 인덱스의 값이 "Z"라면 이전값을 빼주고
아니라면 parseInt를 이용해 정수로 변환한 값을 누적값에 더해주면서
누적값을 쌓아나가는 코드입니다.
정직하게 acc를 계속 리턴해주는 방법도 사용할 수 있겠지만
어차피 우리는 현재 콜백함수의 return 값을 다음 콜백함수의 acc값으로 사용한다는 것을 알고 있습니다.
따라서 삼항연산자를 이용해 어떤 값을 반환할지를 return문 시점에서 결정해주어도
문제없이 동작한다는 것입니다.
😎좀 더 하드하게.. 비동기까지 곁들인
const promiseFactory = (time) => {
return new Promise((resolve, reject) => {
console.log(time);
setTimeout(resolve, time);
});
};
[1000, 2000, 3000, 4000].reduce((acc, cur) => {
return acc.then(() => promiseFactory(cur));
}, Promise.resolve());
promise를 반환하는 함수 promiseFactory와
reduce를 곁들인...
초깃값을 Promise.resolve()로 한 후에, return된 프로미스에 then을 붙여 다음 누적값으로 넘기면 됩니다.
프로미스가 순차적으로 실행됨을 보장할 수 있습니다.
반복되는 모든 것에는 reduce를 쓸 수 있다는 것을 기억하시면 됩니다.
map과 reduce 외에도, 배열의 메서드인 sort, filter, every, some, find, findIndex, includes 정도는 알아두시면 좋습니다. 오
늘 reduce만 있어도 다른 메서드들을 다 구현할 수 있다는 것을 배웠기 때문에, 다른 메서드를 까먹으면 reduce로 구현하시면 됩니다!
이라고 설명을 해주시는데
흠... Promise에 대한 이해가 없으면 이해가 쉽지 않을 듯 합니다.
const promiseFactory = (time) => {
return new Promise((resolve, reject) => {
//프로미스 생성자 함수는 비동기처리를 수행할 콜백함수를 전달받습니다.
console.log(time);
setTimeout(resolve, time);
//setTimeout이 실행되면 콜백으로 넣은 resolve 콜백이 실행하는 코드입니다.
});
};
[1000, 2000, 3000, 4000].reduce((acc, cur) => {
return acc.then(() => promiseFactory(cur));
}, Promise.resolve());
일단 알아두어야하는 점 reduce의 초기값으로 준
Promise.resolve()는 프로미스 생성자 함수의 내장 메서드 .resolve()를 뜻합니다.
Promise.resolve()는 뭐하는거냐?
이미 존재하는 값을 래핑해서 프로미스로 만드는데에 사용합니다.
인수로 전달받은 값을 resolve하게 만들어주는 녀석인데
let a = Promise.resolve();
console.dir(a);
//Promise { undefined }
이렇게 인자로 아무것도 안넘겨주면
프로미스인데 안에 undefined가 들어가있는... 녀석이 나옵니다.
let a = Promise.resolve([1, 2, 3]);
console.log(
a.then((value) => {
console.log(value); //[ 1, 2, 3 ]
}),
);
resolve하니까 then을 통해서 후속처리가 가능해집니다.
아하... .then() 메서드는 프로미스 객체에 사용할 수 있는데
reduce는 초기값을 지정해주지않으면 맨 첫번째 인자를 사용하니까
초기값을 Promise.resolve()의 반환결과인 프로미스객체를 이용함으로써
오류를 방지할 수 있는거군여
const promiseFactory = (time) => {
return new Promise((resolve, reject) => {
console.log(resolve);
setTimeout(resolve, time);
});
};
[1000, 2000, 3000, 4000].reduce((acc, cur) => {
return acc.then(() => promiseFactory(cur));
}, Promise.resolve());
/*
[Function (anonymous)]
[Function (anonymous)]
[Function (anonymous)]
[Function (anonymous)]
*/
'javascript' 카테고리의 다른 글
타이머 함수를 케이크처럼 쉽게 이해하는 법 (2) | 2023.03.12 |
---|---|
이벤트 루프와 블록 컨텍스트를 알아보자. (0) | 2023.03.10 |
이터러블과 이터레이터를 이해하는 방법 (0) | 2023.03.06 |
너무 재미있는 예제로 알아보는 실행컨텍스트 (0) | 2023.03.05 |
getter,setter까지 복사하는 deep copy를 구현하자 (0) | 2023.03.02 |