03. 배열과 객체 다루기 - map, filter, reduce
Redux의 리듀서와 selector는 대부분 배열을 “조회하고 변환"하는 작업입니다. map, filter, reduce 세 가지 고차 함수만 능숙하게 다뤄도 Redux 코드의 상당 부분을 읽고 쓸 수 있습니다.
학습 목표
map,filter,reduce의 동작과 반환값을 정확히 설명할 수 있다.- 원본을 바꾸는 배열 메서드와 바꾸지 않는 메서드를 구분해 리듀서에서 안전한 쪽만 쓸 수 있다.
- 세 함수를 조합해 상태에서 파생 데이터를 계산할 수 있다.
map: 각 요소를 변환한 새 배열을 만든다
map()은 배열의 각 요소에 함수를 적용해 같은 길이의 새 배열을 반환합니다.
| |
핵심은 조건에 해당하지 않는 항목도 새 객체로 만들 필요는 없다는 것입니다. 위 예제에서 id가 1이 아닌 항목은 원래 객체를 그대로 반환합니다(불필요한 새 객체 생성을 피해 리렌더 최적화에도 도움이 됩니다. 13편에서 다룹니다).
filter: 조건을 만족하는 요소만 남긴다
filter()는 조건 함수가 true를 반환하는 요소만 모아 새 배열을 반환합니다.
| |
filter()로 특정 항목을 “삭제"하는 것도 흔한 패턴입니다. 원본 배열에서 해당 id를 제외한 새 배열을 만드는 방식이, 배열 메서드 splice()로 원본을 직접 자르는 것보다 Redux에서 안전합니다.
| |
reduce: 배열을 하나의 값으로 접는다
reduce()는 배열의 모든 요소를 순회하며 **누적값(accumulator)**을 만들어 갑니다. 이름 그대로 Redux의 “Reducer"라는 개념과 정확히 같은 발상입니다.
| |
reduce()의 두 번째 인자(0)는 초기 누적값입니다. 이 초기값이 바로 Redux 리듀서의 initialState와 같은 역할을 합니다. 실제로 Redux의 combineReducers는 여러 리듀서를 실행하며 전체 상태 객체를 “누적"해 나가는데, 이는 reduce()의 발상을 상태 관리에 그대로 적용한 것입니다.
| |
이 “배열 → id 기반 객체” 변환은 27편(정규화)에서 대규모 데이터를 다루는 핵심 기법으로 다시 등장합니다.
세 함수 조합하기
실무에서는 map/filter/reduce를 체이닝해 복잡한 파생 데이터를 계산합니다.
| |
이런 체이닝은 14편(Selector 패턴)에서 useSelector와 함께 쓰이며, 컴포넌트가 필요로 하는 파생 데이터를 상태로부터 계산하는 표준적인 방식이 됩니다.
객체 순회와 변환: Object.keys/values/entries
Redux 상태에서 배열 못지않게 자주 마주치는 형태가, 방금 reduce()로 만든 todosById처럼 id를 키로 쓰는 객체입니다. 이런 객체는 배열이 아니므로 map/filter를 바로 쓸 수 없습니다. JavaScript는 객체를 배열로 변환하는 세 가지 메서드로 이 문제를 해결합니다.
| |
Object.values()로 객체를 배열로 바꾸고 나면, 앞서 배운 map/filter/reduce를 그대로 이어 쓸 수 있습니다. 예를 들어 완료된 todo 개수를 세려면 Object.values(todosById).filter((todo) => todo.done).length처럼 씁니다. 27편(정규화)에서 state.entities(id 기반 객체)로부터 목록을 뽑아낼 때 바로 이 패턴을 다시 사용합니다.
객체를 불변하게 업데이트하기: Object.assign과 스프레드
배열에 push/splice(변경)와 concat/filter(새 배열 반환)가 있듯, 객체에도 원본을 직접 바꾸는 방식과 새 객체를 반환하는 방식이 있습니다. 리듀서 안에서는 항상 새 객체를 반환하는 쪽을 씁니다.
| |
Object.assign({}, todo, { done: true })은 빈 객체 {}에 todo의 속성을 먼저 복사하고, 그 위에 { done: true }를 덮어씁니다. 첫 인자를 빈 객체가 아니라 todo 자체로 쓰면(Object.assign(todo, { done: true })) 원본을 직접 변경해버리는 흔한 실수가 됩니다. 02편에서 배운 스프레드 연산자가 같은 일을 더 짧고 안전하게 하므로, 실무에서는 스프레드를 더 많이 씁니다.
원본을 바꾸는 메서드는 리듀서에서 쓰지 않는다
JavaScript 배열 메서드 중 일부는 **원본을 직접 변경(mutate)**합니다. Redux 리듀서 안에서는 이런 메서드를 쓰면 안 됩니다.
| 원본을 바꾸는 메서드(리듀서에서 금지) | 새 배열/값을 반환하는 메서드(리듀서에서 사용) |
|---|---|
push(), pop() | concat(), [...arr, item] |
splice() | slice(), filter() |
sort(), reverse() (원본 정렬) | [...arr].sort() (복사본 정렬) |
인덱스 직접 대입(arr[0] = x) | map()으로 새 배열 생성 |
객체 속성 직접 대입(obj.x = y) | { ...obj, x: y } 또는 Object.assign({}, obj, { x: y }) |
sort()와 reverse()는 특히 자주 놓치는 함정입니다. 정렬 자체는 새 배열을 반환하는 것처럼 보이지만, 실제로는 원본 배열을 제자리에서 정렬한 뒤 그 원본을 반환합니다.
| |
실무 체크리스트
map/filter로 새 배열을 만들 때, 조건에 해당하지 않는 항목까지 불필요하게 새 객체로 만들고 있지 않은가?reduce의 초기값 타입(배열/객체/숫자)이 최종적으로 원하는 결과 타입과 일치하는가?- 리듀서 코드에
push,splice,sort()(원본 정렬), 객체 속성 직접 대입(obj.x = y) 같은 변경이 섞여 있지 않은가?
연습 과제
기초(★☆☆)
orders배열에서status가"cancelled"인 주문만 골라내는filter코드를 작성해보세요.
중급(★★☆)
todos배열을reduce로{ done: [...], active: [...] }형태의 객체로 분류해보세요.todosById객체에서Object.values()와filter()를 조합해 완료되지 않은 todo 개수를 구해보세요.
고급(★★★)
map,filter,reduce를 체이닝해 “완료되지 않은 할 일 중 텍스트 길이가 5자 이상인 항목의 개수"를 계산하는 코드를 작성해보세요.
요약
map은 변환,filter는 선별,reduce는 누적이라는 각자의 역할이 있고 조합해서 복잡한 파생 데이터를 계산한다.reduce의 발상(초기값 + 누적 함수)은 Redux 리듀서 개념과 본질적으로 같다.Object.keys/values/entries로 id 기반 객체를 배열로 바꾸면 map/filter/reduce를 그대로 이어 쓸 수 있고, 객체 업데이트는 스프레드나Object.assign({}, obj, patch)로 새 객체를 반환해야 한다.- 리듀서 안에서는 원본을 바꾸는 배열 메서드(
push,splice,sort())를 절대 쓰지 않는다.
참고 문헌 및 출처(추천)
- MDN Web Docs, “Array.prototype.reduce()” — reduce의 정확한 동작과 초기값 규칙
- MDN Web Docs, “Array.prototype.sort()” — sort가 원본을 변경한다는 명세
- Redux 공식 문서, “Immutable Update Patterns” — 배열/객체 불변 업데이트 패턴 목록
![Featured image of post [Redux] 03. 배열과 객체 다루기 - map, filter, reduce](/post/redux/array-object-manipulation/wordcloud_hu_9b3f69962e364cc9.webp)
![[Redux] 01. JavaScript 핵심 개념 - 변수, 함수, 객체](/post/redux/javascript-fundamentals/wordcloud_hu_6f9ecedd71e970c4.webp)
![[Redux] 02. ES6+ 필수 문법 - 구조 분해, 스프레드, 템플릿 리터럴](/post/redux/es6-essential-syntax/wordcloud_hu_18823cbc025345cb.webp)
![[Redux] 03. 배열과 객체 다루기 - map, filter, reduce](/post/redux/array-object-manipulation/wordcloud_hu_27392b9afcb1090c.webp)
![[Redux] 04. 비동기 JavaScript - Promise와 async/await](/post/redux/asynchronous-javascript/wordcloud_hu_ed18ead24b277c5.webp)
![[Redux] 05. TypeScript 기초 - 타입 시스템 이해하기](/post/redux/typescript-basics/wordcloud_hu_a10feb7b41a22039.webp)
![[Redux] 00. 커리큘럼 - 학습 로드맵](/post/redux/getting-started-redux/wordcloud_hu_2ed9b2e29c50fe99.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Tutorial] Learn Prompting - 프롬프트 엔지니어링 무료 가이드 정리](/post/2022-12-30-learn-prompting/wordcloud_hu_6a9d105de4834753.webp)