오늘 내가 쓰려는 글은 Javascript를 쓰는 개발자라면 최소한 한번 이상은 접해보았을 filter
, map
, reduce
에 대해 알게 된 내용을 적어보려고 한다.
최근에 실무에서 작업을 하면서 처음으로 알았던 내용이었는데 나만 처음 알았던 걸 수도 있다 ㅋㅋㅋㅋㅋ 이 글을 읽는 사람 중 누군가는 이걸 아직도 몰랐어? 할 수도 있지만 그런분들은 흐릿한 눈으로 봐주면 좋을 것 같다.
그렇기에 오늘 적을 블로그의 내용은 그렇게 길지 않을 것이다.
프론트엔드 개발자 입장에서 filter, map, reduce와 같은 고차함수들은 흔히 데이터를 가공할 때 자주 사용된다.
간단한 예시로는 백엔드에서 내려주는 데이터 형태가 ui를 그리는데 적합하지 않다던가, 특정 데이터들만 filtering해서 그대로 그려준다던가 활용법은 흔히 아는 것들이다.
코드로 보는 게 설명에 더 쉬울 것 같아서 바로 코드로 설명해보려 한다. 당연히 실무에서 쓰는 코드가 아닌 비슷한 형태의 예시로 변형해서 가져왔다.
// API response 형태
type GetCartListResponse = {
cart_list: Array<CartItem>
}
type CartItem = {
// 장바구니 물품 id
cart_id: string;
// 장바구니 물품 이름
name: string;
// 장바구니 물품 가격
price: number;
// 장바구니 물품 세일
sale: number;
// 장바구니 물품 카테고리
category: Category;
// 장바구니 물품 넣은 timestamp
added_timestamp: number;
}
// 장바구니 목록 리스트
const data = {
cart_list: [{
cart_id: 'category1_1',
name: '물건1',
price: 10000,
sale: 0,
category: '카테고리1',
added_tiemstamp: 1727936157084
},{
cart_id: 'category1_2',
name: '물건2',
price: 10000,
sale: 0,
category: '카테고리1',
added_tiemstamp: 1727936157184
},{
cart_id: 'category1_3',
name: '물건3',
price: 10000,
sale: 0,
category: '카테고리2',
added_tiemstamp: 1727936157284
},
// ...
]
}
이런 형태의 장바구니 목록이 데이터로 내려온다고 하자. 이렇게 보면 정말 흔하디 흔한 데이터 구조이다.
만약 여기서 ui형태의 요구사항을 충족시켜야 한다면 때에 따라 고민이 필요한 순간이 온다.
ex)
1. 카테고리별로 묶어서 표현해줬으면 해요.
2. 날짜별로 묶어서 표현해줬으면 해요.
등등...
이러한 요구사항이 주어진다면 개발자가 선택할 수 있는 방법은 여러가지이다.
😃 데이터를 category 또는 timestamp 별로 묶어서 가공한다.
가장 쉽게 생각할 수 있는 방법 중 하나이다. 특정 데이터의 기준으로 만들어두고 ui레벨에서는 해당 리스트에 맞춰서 렌더링하면 되는 것이다.
🤔 API 협의를 통해 데이터 구조를 요구사항에 맞춰서 달라고 한다.
이 선택지는 사실 어떤 API이냐에 따라 백엔드 입장에서 어려운 요구사항일 수 있다.
단순히 UI 요구사항을 맞추기 위해 API응답 형태를 바꾸는게 항상 올바른 정답은 아닐 수 있기 때문이다. 물론 API가 정말 UI에 맞춰서 내려주는게 더 효율적이고 맞는 방법이라면 이러한 선택지도 좋은 방법일 수 있다.
🤩 filter, map, reduce의 세번째 인자를 활용한다.
바로 이 선택지가 내가 새롭게 알게된 방법이었다. 고차함수를 사용하면서 진짜 거의 사용해보지 않았었고, 사용할 일이 많지 않았던 것 같다.
const CartList = () => {
// ...
return (
<li>
{data.cart_list.map((cartItem, index, array) => {
const prevCartTime = format(
array[index - 1]?.added_tiemstamp,
'yyyy-MM-dd'
);
const curCartTime = format(cartItem.added_tiemstamp, 'yyyy-MM-dd');
return (
<>
// 날짜가 서로 다를 때만 표기하여 요구사항을 맞춰주기
{!prevCartTime ||
(prevCartTime !== curCartTime && <span>{curCartTime}</span>)}
<CartItem key={cartItem.id} />
</>
);
})}
</li>
);
}
이런식으로 데이터를 가공하지 않고도 ui 요구사항에 맞춰서 그려줄 수 있게 된 것이다.
물론 이렇게 되야하는 전제에는 데이터가 timestamp순으로 정렬이 되어있어야 한다.
바로 직전의 데이터만 비교했을 때도 올바르게 동작하기 때문이다.
블로그에서 소개한 활용법들: The Little-Known Third Parameter in High-Level Array Functions in JavaScript: Array
Using the third argument of callbackFn: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#using_the_third_argument_of_callbackfn
솔직히 말하자면 크게 사용될 일이 많아 보이지는 않는다.😂
세번 째에 옵셔널 인자로 있는 것이 일단 사용할 일이 많지 않다고 암묵적으로 의미하는 것 같기도 하다.
그래도 위의 예시처럼 기존 배열 + index의 조합을 통해 로직을 구현해야 할 때 활용성은 있어 보인다.
예시 1) 배열에서 직전의 값과의 차이 구하기
const differences: Array<number> = [];
const numbers = [1, 2, 3, 5, 8, 10];
numbers.forEach((number, index, array) => {
if (index < array.length - 1) {
differences.push(array[index + 1] - number);
}
});
console.log('diffrences: ', diffrences)
// 결과 diffrences: [1, 1, 2, 3, 2]
예시 2) 끝말잇기 비교하기
const wrongIndex: Array<number> = [];
const wordRelay = ['사과', '과일', '일꾼', '몰라요'];
wordRelay.forEach((word, index, array) => {
if (index > 0) {
const prevWord = array[index - 1]
const prevLastWord = prevWord[prevWord.length - 1];
const curFirstWord = word[0];
const isSame = prevLastWord === curFirstWord;
if (!isSame) {
wrongIndex.push(index);
}
}
});
console.log('wrongIndex: ', wrongIndex)
// 결과 wrongIndex: [3]
위의 예시는 정말 간단하게 생각한 예시들이다 ㅋㅋㅋ
특히 끝말잇기는 저렇게만 하면 버그투성일 것이다. 그럼에도 그나마 활용할 곳을 찾아본다면 이런 식으로 직전의 값을 가져올 때 유용하게 활용할 수 있어 보인다.
사실 이렇게 길게 늘어서 썼지만, 새롭게 알게된 것을 코드에 녹여내진 않았다.
> 😃 데이터를 category 또는 timestamp 별로 묶어서 가공한다.
위에서 잠깐 선택지로 나왔던 것을 활용하게 되었다. 간단하게 그 이유를 말해보면 바로 Sementic한 태그를 놓치지 않기 위해서였다.
위의 방식으로 구현하게 된다면 태그의 형태는 아래와 같을 것이다.
<ul>
<li>
// 특정 li 요소에만 span이 들어가 있음.
<span><span/>
<div>...</div>
</li>
<li>
<div>...</div>
</li>
<li>
<div>...</div>
</li>
...
</ul>
즉 어떤 li
요소 내부에는 title
과 같은 ui가 들어있는데 특정 li
에는 없게 되는 시멘틱하지 않은 태그 형태가 되는 것이다. 그래서 결국 위의 형태로 구현하지는 않았다.
결국 데이터를 가공하는 형태로 구현하였는데, 슈도 코드로 보여주자면 다음과 같다.
const getCartListQuery = useQuery({
//...
select: data => {
// [date: string]: CartList 형태로 가공
const parsedData = data.reduce((acc, cur) => {
const cartTime = format(cur.added_tiemstamp, 'yyyy-MM-dd');
if (acc[cartTime]) {
acc[cartTime] = [cur];
} else {
acc[cartTime].push({...cur});
}
}, {});
return {
data: parsedData;
}
}
})
useQuery
의 select
에서 데이터를 미리 가공해서 내려주는 형태로 구현하였다.
이렇게 되면 날짜별로 map을 한번 돌리고 날짜별로 장바구니 리스트를 가져와 렌더링하면 시멘틱 태그도 지키고 요구사항도 맞출 수 있게 된다.
거의 3달 만에 회고가 아닌 내용을 쓰는 것 같은데, 쓰고 싶은 내용이 마땅히 떠오르지 않았던 것도 있는데 귀찮아서 쓰지 않았던 이유 있다.. ㅎㅎ
올해도 벌써 두 달도 채 남지 않았는데 시간이 너무 빨리 가는 것 같다 ㅠㅠ
남은 2024년 화이팅!!!!