avatar
Published on

reduce에 spread 를 쓰면 안되는 이유

Author
  • avatar
    Name
    yceffort

자바스크립트 프로젝트를 개발하다보면, 배열을 하나의 객체로 묶어야될 필요성이 생길 때가 있다. 아래 예제를 들어보자.

const items = [
  { name: 'obama', age: 10, country: 'USA' },
  { name: 'trump', age: 14, country: 'USA' },
  { name: 'moon', age: 37, country: 'KOREA' },
  { name: 'clinton', age: 64, country: 'USA' },
  { name: 'bush', age: 49, country: 'USA' },
]

이렇게 배열로 되어있는 것을 이름을 키로 하는 객체로 바꾼다고 한다면, 아마 다들 reduce를 생각할 것이다.

const result = items.reduce((prev, item) => {
  prev[item.name] = { age: item.age, country: item.country }
  return prev
}, {})

// {
//   obama: { age: 10, country: 'USA' },
//   trump: { age: 14, country: 'USA' },
//   moon: { age: 37, country: 'KOREA' },
//   clinton: { age: 64, country: 'USA' },
//   bush: { age: 49, country: 'USA' }
// }

혹은 for loop를 활용하여 똑같이 할 수 있다.

const result2 = {}
for (let item of items) {
  result2[item] = { age: item.age, country: item.country }
}

둘 중에 뭐가 더 읽기 좋은 코드냐고 묻는다면, 당연히 후자일 것이다. 내가 아무리 reduce를 좋아한다고 해도 그건 반박 불가능한 사실이다. 그러나 요즘 리액트 커뮤니티의 성장으로 인해 함수형 프로그래밍 스타일로 코드를 작성하는 일이 잦아짐에 따라서 reduce를 쓰는일이 많아지고 있다.

그러나 문제는 아래의 스타일이다.

const result3 = items.reduce(
  (prev, item) => ({
    ...prev,
    [item.name]: { age: item.age, country: item.country },
  }),
  {},
)

이제부터 이런 코드를 reduce...spread라고 부르겠다.

이 spread operator는 Object.assign과 유사하게 동작하는데, 소스 객체의 속성을 순환하면서 각 속성을 타켓의 객체에 하나씩 복사해 넣는다. 위 코드의 경우에는, 새로운 객체를 계속해서 복사하는 것이다. 위 코드의 문제는 무엇일까?

위코드의 동작을 살펴본다면, 대충 아래와 같을 것이다.

# 첫번째
result = {}

{...result, 'obama': {age: 10, country: 'USA'}}

# 두번째
result = {'obama': {age: 10, country: 'USA'}}

{...result, 'trump': {age: 14, country: 'USA'}}

# 세번째

result = {'obama': {age: 10, country: 'USA'}, 'trump': {age: 14, country: 'USA'}}

{...result, 'moon': {age: 37, country: 'KOREA'}}

##....

의사 코드이기 때문에 대충 보면 된다.

루프가 반복될때마다, 모든 루프에서 정확히 n회 실행되는 것은 아니지만, 이렇게 될 경우 O(n2)O(n^2)라 볼 수 있다.

바벨이나 타입스크립트 트랜스파일러를 사용하면 해당 코드를 어떻게 변환할까?

그전에 먼저, tc39에 나와있는 객체 전개 연산자의 스펙을 살펴보자.

let aClone = { ...a }
let aClone = Object.assign({}, a)

desuagred된 구문도 거의 동일한 작업을 수행하므로, 전개 연산자와 비슷한 시간 복잡성을 가질 것이다. 즉, 새 객체를 생성한다음, 나머지 객체의 키를 새객체에 반복해서 복사하는 것이다.

따라서 reduce에서 전개연산자로 합성해서 리턴하는 것은 굉장히 느린 코드라고 볼 수 있다.

여담으로, 이와 관련된 키보드 배틀(?) 이 트위터에서 열린 적이 있다.

https://twitter.com/fildon_dev/status/1396252890721918979

문제의 코드

const users = [
  { id: 1, name: 'Tony Stark', active: false },
  { id: 2, name: 'Bruce Banner', active: true },
  { id: 3, name: 'Natasha Romanoff', active: false },
  { id: 4, name: 'Chris Evans', active: true },
  { id: 5, name: 'Chris Hemsworth', active: false },
  { id: 6, name: 'Clark Gregg', active: false },
]

// good?
const result1 = users.reduce((acc, curr) => {
  if (curr.active) {
    return acc
  }
  return { ...acc, [curr.id]: curr.name }
})

// bad?
const result2 = users
  .filter((user) => !user.active)
  .map((user = (user) => ({ [user.id]: user.name })))
  .reduce(Object.assign, {})

그래서 뭐가 더 성능이 좋고 빠를까? 저 두개가 최선의 방법일까? 만약에 나라면,,,

🤔