배열 내 객체 중복 값 제거

myung hun kang·2023년 1월 6일
0

프로젝트를 하다보면 빈번하게 마주하게되는 경우이다.

[
  { id: 111, content: 'zzz', admin: false} ,
  { id: 222, content: '미ㅣasg', admin: true} ,
  { id: 333, content: 'i19jf24n', admin: true} ,
  { id: 444, content: 'abcdefg', admin: false} ,
  ...
  { id: 111, content: 'zzz', admin: false} ,
  { id: 333, content: 'i19jf24n', admin: true} ,
]

예를 들어 서버에서 어떤 객체 배열을 받아온다고 하자

값을 어쩔 수 없이 중첩되서 받아오게 되는 경우라면 위와 같이 앞에 사용된 값이 다시 배열에 concat 되는 경우가 있다.

본인의 경우는 프로젝트 내 사용자의 리뷰를 무한스크롤로 보여주는 기능을 구현할 때 마주했다.

쉽게 할 수 있어 보이지만 의외로 그렇지 않다...

하지만 해결하고 난 후 다양한 솔루션을 찾아보니 정말 다양한 방법으로 해결할 수 있다는 것을 알았다.

이번 글에서는 JS의 다양한 함수를 이용하여 해결한 후기를 작성해보겠다.


data 모습

내 프로젝트에서 다루게 된 data는 이런 형식을 갖추고 있었다.

{
reviewEntryNo:847
reviewContent:"모바일로 남깁니다. 좋네요"
starCount:4
createdDate:"2022-12-27T13:43:58.364523"
modifiedDate:"2022-12-27T13:43:58.364523"
userEmail:"abcabc@gmail.com"
storeId:"21544343"
keywords: ["제품이 다양해요","직원이 친절해요",  "매장이 넓어요"]
}

여기서 reviewEntryNo 로 리뷰를 구분하게 되는데 이 데이터를 가져오다보면 reviewEntryNo를 중첩되게 가져오는 경우가 생길 수 있다.

위 데이터 형식으로 서버로부터 받아온 data(action.payload값)는 redux의 slice 내에서 다음과 같이 이전 state값과 합쳐지게 된다.

const totalReviews = [...state.reviews, ...action.payload]

이제부터 작성하게 될 방법은 이 reviewEntryNo 값으로 중첩되는 배열안 object를 제거하는 방법이다.

1. for - of

    const newReviews: ReviewType[] = []
      for(const review of totalReviews){
       if (
         newReviews.findIndex(
          (newReview) => newReview.reviewEntryNo === review.reviewEntryNo
            ) === -1
          ) {
            newReviews.push(review)
          }
        })

제일 이해되기 쉬운 방법 중 하나이다.

forEach로 totalReviews 안의 review들 중에서 중첩값을 빼고 저장될 newReviews 안의 newReview와 같은 값을 가지고 있지 않다면

newReviews에 push를 하는 식이다.

2. filter

filter를 사용하는 방법은 두가지이다.

첫번째

 const newReviews = totalReviews.filter(
          (review, idx) => {
            return (
              totalReviews.findIndex((review1) => {
                return review.reviewEntryNo === review1.reviewEntryNo
              }) === idx
            )
          }
        )

totalReviews안 review들 중 reviewEntryNo를 가지는 review중 첫번째 값만 filter 하겠다는 코드이다.

filter에는 3번째 인자로 callback을 쓸 수 있기 때문에 위 코드는 다음처럼 쓸 수도 있다.

const newReviews = totalReviews.filter(
   (review, idx, callback) =>
     idx === callback.findIndex(
      (review1) => review1.reviewEntryNo === review.reviewEntryNo
            )
        )

callback을 써서 간결해졌다뿐 똑같다.

3. reduce

실제 프로젝트에 쓰려다 실패했는데 다 구현하고 찾아냈다.

const newReviews = totalReviews.reduce((acc: ReviewType[], curr) => {
   if (
     acc.findIndex(
     ({ reviewEntryNo }) => reviewEntryNo === curr.reviewEntryNo
       ) === -1
      ) {
        acc.push(curr)
      }
      return acc
}, [])

filter도 간결하고 가독성이 좋지만 reduce도 사용하기 좋다.

4. new Set()

const newReviews = [
          ...new Set(totalReviews.map((review) => JSON.stringify(review))),
        ].map((review) => JSON.parse(review)) as ReviewType[]

제일 짧고 좋다.

하지만 Set과 같은 경우에 reviewEntryNo 는 같지만 다른 값들이 다르다면 다른 값으로 인식해 처리가 안될수도 있다. 그래서 프로젝트에 사용하지는 않았다.

5. new Map()

 const map = new Map()
    for (const review of totalReviews) {
      map.set(review.reviewEntryNo,review)
     }
 const newReviews = Array.from(map.values()) as ReviewType[]

Set은 다른 프로젝트에서 사용해본 반면 Map은 사용해본 적이 없어서 전혀 생각하고 있지 못한 방법이었다.

map의 set method를 이용하면 앞의 key부분 (여기서는 review.reviewEntryNo) 값이 똑같은 값이 있다면 업데이트를 해서 저장시켜주는 method이다.

Set과 비슷하게 동일한 값을 알아서 제거해줘서 좋지만
for - of를 사용하고 다시 배열로 변환해주는 식이 필요해서 그렇게 좋은 방식같지는 않아보인다.(?)

결론

다양한 방식으로 중복 값을 제거해보았다. 위 방식들을 찾아보고 적용해보면서 한 블로그 글을 보게 되었다.

ForEach, Reduce, Filter, For - Of , Set에 대해서 성능을 테스트해본 글이였고
결과는 거의 다 빠르고 좋으나 배열이 매우 크고 성능을 많이 고려해야 한다면
Set이 제일 우수하다는 글이였다.

그치만 앞서 언급한 바와 같이 object내 속성이 여러개 있는 경우라면 다른 추가적인 절차가 필요할 것으로 여겨진다.

속성값이 하나라면 new Set() 을 쓰도록 하자


참고자료

https://www.technicalfeeder.com/2021/07/8-ways-to-remove-duplicates-from-an-array/

https://sylhare.github.io/2022/03/08/Reduce-in-typescript.html

https://triplexlab.tistory.com/125

profile
프론트엔드 개발자입니다.

0개의 댓글