티스토리 뷰

INTRO

 

개발을 하다보면 Array와 관련된 많은 메서드들을 사용하는데, 저는 그 중에서도 map()과 forEach()를 주로 씁니다.

많이 쓰는 만큼 한 번 정리해보고 싶기도 하고, 좀 다른 관점에서 이 두 메서드에 대해 접근해보면 재미있지 않을까 싶어 글로 써보려고 합니다!

재미로 읽어주세요 😉

 

 

어떠한 특징을 가진 메서드인가?

 

map()과 forEach() 모두 Array 관련 메서드들로써, ES5 부터 등장하였습니다.

forEach()가 배열 요소마다 한 번씩 주어진 함수(콜백)를 실행하는 것과 달리, map()은 배열 내의 모든 요소 각각에 대하여 주어진 함수(콜백)를 호출한 결과를 모아 새로운 배열을 반환한다는 특징을 가지고 있습니다.

그리고, 그 함수는

1. currentValue (배열 원소의 값)

2. index (현재 요소의 인덱스)

3. array (현재 배열)

이 세 개의 인자를 가지고 호출됩니다.

 

배열의 각 원소에 3을 곱하는 코드를 두 메서드의 특징에 맞게 짜본다면, 다음과 같이 구성할 수 있습니다.

 

 

 

분석 시작!

 

일단 일반적인 for문과 해당 두 메서드를 비교했을 때,

 

1. They have a callback to execute so that act as a overhead. (콜백을 실행하는데 그것은 오버헤드처럼 동작한다.)

2. There are lot of corner cases that javascript function consider like getters, sparse array and checking arguments that are passed is array or not which adds up to overhead. (Javascript 함수가 getters나 배열의 요소가 연속적이지 않은 희소 배열, 통과된 인수가 배열인지 아닌지 등을 고려하는 수많은 코너케이스들이 있는데 이들은 오버헤드를 증가시킨다.)

 

와 같은 이유들로 map()과 forEach()가 더 느릴 수 밖에 없습니다.

 

그렇다면 map()과 forEach() 각각을 비교하면 어떠한 차이가 있을까요?

좀 더 자세히 알아보기 위해 두 메서드의 Polyfill을 한 번 살펴봤습니다.

 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

 

두 코드를 비교해본 결과, 구조가 상당히 비슷하다는 것을 알 수 있었고, 가장 두드러지는 차이라고 말 할 수 있는 부분이 바로 A라는 배열과 return에 대한 유무였습니다.

 

 

 

음...코드의 분석을 조금 해보자면 일단 A[k]의 구문을 봤을 때, A라는 변수(Array)는 하나만 존재하고, 그것의 인덱스만 수정한다는 것을 알 수 있습니다. 

또한, 해당 배열을 undefined가 담긴, this(현재 배열)와 같은 길이의 배열로 만든다는 것을 확인할 수 있죠.

 

그렇다면 왜 index를 나타내는 i가 아닌 k를 사용하였을까요?

바로, 현재 배열을 Object로 만들어서 인덱스가 아닌,  값 처럼 사용하기 위함입니다. 즉, k는 key였던 것이죠.

For 문이 아닌, while 문을 사용하여 For-In 구문과 비슷하게 구현한 이유, i가 아닌 k를 사용한 이유, 배열에 undefined를 채워 미리 초기화한 의도....이것들은 모두 아래 코드로 설명할 수 있을 것 같습니다.

 

 

자바스크립트의 Array는 객체를 기반으로 만들었기 때문에 이를 길이로 카피할 경우, 어떤 인덱스에 아이템을 추가해도 문제가 되질 않습니다. 다만, 배열의 아이템들이 순차적으로 모두 차 있지 않은 상태에서 For 문을 사용할 경우, 배열의 길이만큼 루프를 다 돌 수 밖에 없어 불필요한 연산을 할 수 있습니다.

 

따라서, map() 구현 코드 중 해당 부분은 불필요한 연산을 막고 해당 인덱스가 객체의 키(Key)로 존재하는지 미리 확인하는 작업을 거쳐 존재할 경우에만 카피를 한다는 의미죠.

 

Empty인 인덱스 영역은 그냥 Empty인 채로 두면 되기 때문에 불필요한 메모리 할당을 막고, 카피를 하기 전의 배열과 동일한 모습을 만드는 것을 의도한 것으로 보입니다!

 

뭐 어쨌든, forEach와 다르게 map의 구현 코드 내부에 있는 A라는 배열의 선언과 값의 할당

즉, 변수의 선언 및 대입은 메모리의 생명주기 중 할당(Allocation) 부분에 속하며, 이 또한 연산으로 볼 수 있습니다.

 

변수가 메모리에 할당될 때는, 단순히 4byte나 8byte가 아닌 현재의 타입, 데이터가 저장된 주소 값, 생성된 위치 등의 메타 데이터 형태로 할당되기 때문에 변수의 할당 시 더 많은 메모리가 사용될 수 밖에 없습니다.

 

다시 말해, 코드를 그대로 실행하는 것이 아닌, 결과 값을 새로운 변수(A)에 담고 반환하는 작업을 하는 map()이 forEach() 보다 느릴 수 밖에 없다는 말입니다.

 

실제, 브라우저 내에서 console.time() 및 timeEnd()를 활용하여 10,000,000개의 요소를 도는 코드를 작성하였을 때, 실행시간은 각각 아래와 같이 161ms-203ms1301ms-1445ms로 최대 9배 가량 차이가 났습니다.

물론 상황에 따라서 결과는 달라질 수 있겠지만, 한 가지 확실한 건 두 메서드의 실행시간에 차이가 나며 기본적으로 forEach()가 map() 보다 더욱 빠르게 동작한다는 것입니다.

 

 

그럼, 시간 복잡도를 고려해봤을 때 두 메서드는 어떠한 차이가 있을까요?

 

 

시간 복잡도

 

시간복잡도란 알고리즘을 구성한 명령어가 실행된 횟수를 뜻합니다.

시간 복잡도를 표기하는 방법에는 크게 3가지로 나눌 수 있는데,

1. Big O 실행간 상한 표현 (최악의 시간 계산)

2. Ω 실행 시간 하한 표현 (최고의 시간 계산 - 운이 좋았을 때)

3. Θ 실행 시간 평균 표현

가 이에 속합니다.

 

여기서 Big O가 시간복잡도에 가장 큰 영향을 미치는 차항이기 때문에 대부분 시간복잡도를 Big O를 이용하여 나타내며, 상수를 제외한 최고차항만 계산하여 표기하는 것이 원칙입니다.

이 정도로 설명을 마치고, 두 메서드의 시간복잡도를 구하면 모두 O(N)으로 동일한 것을 알 수 있습니다. (가장 크게 연산을 할 때가 while문 내부이기 때문에 최고차항은 N이 됩니다!)

 

하지만, 시간복잡도를 계산할 때 변수를 생성, 할당 및 반환하는 행동들 모두 실행횟수에 포함시키므로 둘의 시간복잡도는 동일하게 O(N)이지만, map()이 forEach()보다 좀 더 크다는 사실을 알 수 있습니다.

 

 

결론

 

지금까지 두 메서드에 대해 알아보는 시간을 가졌습니다.

두 메서드에 대해 비교해봤지만, 애초에 수행하는 역할과 목적이 다르기도 하고 실제로 실행 시간에 영향을 미치는 다양한 사이드 이펙트들이 있기 때문에, 솔직히 비교하는 것이 의미가 있을까 싶기도 합니다. 위의 실행 시간 측정도 좀 억지같네요 ㅋㅋㅋ

처음에 언급했던 것 처럼 그냥 재미로 읽어주셨으면 하고 '이 메서드가 다른 것 보다 빠르니 이것만 써야겠다'라는 식의 생각은 지양해주셨으면 합니다.

그냥 상황에 알맞는 메서드를 사용하는 것이 베스트😉

 

글 읽어주셔서 감사하고 피드백은 언제나 환영입니다.

댓글