개탕 IT FACTORY

Array.prototype.map() 직접 구현하기 본문

Front-end

Array.prototype.map() 직접 구현하기

rendar02 2025. 3. 31. 22:58
반응형

개요

최근 네카라쿠배중에 운좋게 면접을 보게 되었는데 (떨어졌긴하지만…)
라이브 코딩테스트에서 자바스크립트 메소드를 직접 구현해보라는 내용이 나왔다.
사실 그동안 생각지도 못하고 그냥 단순히 구현된 메소드만 써봤지 내부 구현이나 어떻게 생겼는지 확인을 해본적은 없는 것 같았다.

이번참에 디테일하게 공부할겸 메소드를 직접 구현해보고 브라우저에서는 어떻게 구현되어 있는지 확인하는 시간을 가져볼까한다.

무엇보다 AI가 발전한 지금같은 시대에는 단순구현을 떠나서 AI에게 직접 코딩을 요청해서 코드 구현을 해볼까한다.

생각해보기

map함수는 정말 자주쓰이는 함수이다.
특히 데이터를 순회할때 많이 쓰이고 관련된 데이터 가공을 할때도 자주쓰이는데

MDN문서를 통해서 한번 다시 생각해보는 시간을 가져볼까한다
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map

MDN문서

문서에 따르면 map함수는 아래와같이 정의하고있다.


여기서 중요한건 새로운 배열을 생성합니다 이 부분이다.
보통 면접질문에서도 map과 forEach의 차이점에 대해서 질문하면 많이 나오는 질문인데 가장중점적인 부분이 아닐까 싶다.

매개변수

map 함수에서 받는 매개변수는 총 2개(callback, thisArg)이다

  • callback(함수) - 배열의 각 요소에 대해 실행할 함수입니다. 반환 값은 새 배열에서 단일 요소로 추가됩니다.
    • currentValue - 배열 내에서 처리할 현재 요소.
    • index - 배열 내에서 처리할 현재 요소의 인덱스.
    • array - map()을 호출한 배열.
  • thisArg - callback을 실행할 때 this로 사용되는 값.

구현사항

어떻게 구현할까?
단순하게 생각해보자 배열내부를 순회해야되므로 가장 간단한 문법은 반복문을 써서 돌리면 가장 간단하다.

반복문중에서도 for문을 써서 index와 함께 반환해버리면 단순한 구조로 구현이 가능할 것 같다.

  1. prototype 상속으로 구현할것이므로 Array.prototype.rdmap 으로 상속한다
  2. 함수의 경우 익명함수로 값을 출력한다
    1. 함수의 인자는 callback, thisArg로 통합한다 (기존 map함수와 동일하게)
  3. 관련된 로직은 for문을 통해 구현한다
    1. 변수 result 변수에 빈 배열을 선언한다
    2. for문을 통해 받은 함수의 length 만큼을 돌면서 result 배열안에 callback함수를 넣는다.
    3. 결과값을 result 리턴한다.

각 단계별로 구현을 하면 될 것이다

// 1. prototype 상속으로 구현할것이므로 Array.prototype.rdmap 으로 상속한다
Array.prototype.rdmap = 
    // 2. 함수의 경우 익명함수로 값을 출력한다
    // 함수의 인자는 callback, thisArg로 통합한다 (기존 map함수와 동일하게)
    function(callback, thisArg) {
            // 관련된 로직은 for문을 통해 구현한다
            // 변수 result 변수에 빈 배열을 선언한다
            const result = [];
            // for문을 통해 받은 함수의 length 만큼을 돌면서 result 배열안에 callback함수를 넣는다.
            for(i = 0; i < this.length; i++) {
                result.push(callback(this[i], i, this));
            }
            // 결과값을 리턴한다.
            return result 
    }

브라우저에서 테스트시 잘되는 것을 확인해볼수 있다.

아마 브라우저 코드는 더 복잡하게 구현되어 있을테지만, 구현정도는 이정도로 할 수 있을 것 같다

아래는 AI를 통해 만들어본 map함수다 (typescript적용)

Array.prototype.myMap = function <T, U>(
  callback: (value: T, index: number, array: T[]) => U,
  thisArg?: any
): U[] {
  const result: U[] = [];

  for (let i = 0; i < this.length; i++) {
    const boundCallback = callback.bind(thisArg);
    result.push(boundCallback(this[i], i, this));
  }

  return result;
};

생각해보니 thisArg부분을 생각지도 못하였다

gpt설명에 의하면 prefix값을 넣을때 예시를 알려주었는데 많이 사용하는지는 모르겠다 (아직 사용해본적 없음..)

실제 브라우저에서는 어떻게 구현되어있을까?

실제 V8엔진(크롬브라우저)에서는 어떻게 구현되어있을까?

https://github.com/v8/v8/blob/main/src/builtins/builtins-array-gen.cc

실제 V8엔진 코드의 경우 C++ 코드로 구성되어 있으나, 개발자에 의하면 다양한 언어로 개발되어 구성된다고 이야기하고있다.
각 단계별로 진행된다고 클로드한테 물어보니 대답해주었다 (정확한지는… C++ 공부를 딥하게 하지 않아서)

진입점

map함수가 호출되었을때 실행되는 함수

TF_BUILTIN(TypedArrayPrototypeMap, ArrayBuiltinsAssembler) {
  // 매개변수 추출 및 초기 설정
  TNode<IntPtrT> argc = ChangeInt32ToIntPtr(
      UncheckedParameter<Int32T>(Descriptor::kJSActualArgumentsCount));
  CodeStubArguments args(this, argc);
  auto context = Parameter<Context>(Descriptor::kContext);
  TNode<JSAny> receiver = args.GetReceiver();
  TNode<JSAny> callbackfn = args.GetOptionalArgumentValue(0);
  TNode<JSAny> this_arg = args.GetOptionalArgumentValue(1);

  // 배열 순회 함수 초기화
  InitIteratingArrayBuiltinBody(context, receiver, callbackfn, this_arg, argc);

  // TypedArray 요소를 순회하며 map 연산 수행
  GenerateIteratingTypedArrayBuiltinBody(
      "%TypedArray%.prototype.map",
      &ArrayBuiltinsAssembler::TypedArrayMapResultGenerator,
      &ArrayBuiltinsAssembler::TypedArrayMapProcessor);
}

새로운 배열 생성

map 연산결과를 저장할 배열 생성

void ArrayBuiltinsAssembler::TypedArrayMapResultGenerator() {
  // 원본 배열과 같은 타입의 새 TypedArray 생성
  TNode<JSTypedArray> original_array = CAST(o());
  const char* method_name = "%TypedArray%.prototype.map";

  TNode<JSTypedArray> a = TypedArraySpeciesCreateByLength(
      context(), method_name, original_array, len());

  // 빠른 처리를 위한 요소 유형 비교
  fast_typed_array_target_ =
      Word32Equal(LoadElementsKind(original_array), LoadElementsKind(a));
  a_ = a;
}

함수(map)처리

콜백함수를 처리하고, 새로운 배열을 리턴한다.

TNode<JSAny> ArrayBuiltinsAssembler::TypedArrayMapProcessor(
    TNode<Object> k_value, TNode<UintPtrT> k) {
  // 콜백 함수 호출
  TNode<Number> k_number = ChangeUintPtrToTagged(k);
  TNode<JSAny> mapped_value =
      Call(context(), callbackfn(), this_arg(), k_value, k_number, o());

  // 결과 저장 경로 분기 (빠른 경로와 느린 경로)
  Label fast(this), slow(this), done(this), detached(this, Label::kDeferred);
  Branch(fast_typed_array_target_, &fast, &slow);

  BIND(&fast);
  // 타입 변환 및 새 배열에 저장 (타입에 따라 처리)
  TNode<Object> num_value;
  if (IsBigIntTypedArrayElementsKind(source_elements_kind_)) {
    num_value = ToBigInt(context(), mapped_value);
  } else {
    num_value = ToNumber_Inline(context(), mapped_value);
  }

  // 결과 저장
  EmitElementStore(CAST(a()), k_number, num_value, source_elements_kind_,
                   KeyedAccessStoreMode::kInBounds, &detached, context());
  Goto(&done);

  BIND(&slow);
  {
    // 느린 경로: 일반적인 속성 설정 사용
    SetPropertyStrict(context(), a(), k_number, mapped_value);
    Goto(&done);
  }

  BIND(&detached);
  // 버퍼가 분리된 경우 오류 발생
  ThrowTypeError(context_, MessageTemplate::kDetachedOperation, name_);

  BIND(&done);
  return a();
}

C++로 작성되어서 코드를 읽기 어려웠다 (대학 2학년때 배웠던 이후로 본적이…)
하지만 클로드를 통해서 해석을 할수 있어서 좋긴하였으나, 완벽하게 해석해준지는 모르겠다
하지만 기본적인 구조를 파악할수 있기 때문에 이런 구조로 구동된다를 확인할 수 있었다.

내가 짠코드에 비해 거의 3배가량 코드가 많은거 같다. (아마 클래스나 모듈로 뺀부분이 많아서 그런 것 같다.)

마무리

브라우저 내부에서 구동되는 메소드를 직접 구현해보았다.
사실 브라우저의 경우 최적화 코드 및 다양한 언어로 개발되기 때문에 단순한 구조로 개발하는건 무리가 있다.
하지만 내부구조를 직접 개발하여서 구조도 알고, 왜 map은 새로운 배열을 뱉는 구조인지 알수 있어서 더욱 의미가 깊다고 생각된다

시리즈물로 계속 javascript 메소드를 연재해볼까 한다.

다음은 map함수하면 따라오는 forEach에 대해서 다뤄볼까한다.

반응형