티스토리 뷰

es-toolkit의 PR을 구경하다가

bench: remove import overhead 라는 PR을 보게 됐다.

 

해당 PR에는 아래와 같은 수정이 있는데 이게 import의 overhead를 어떻게 줄일 수 있지라는 궁금증이 생겼다.

그리고 gpt에게 물어봤다.

첫 번째 질문

1번에서 2번으로 코드를 수정했는데 import overhead가 줄었어 어떤 차이야?

1번
import { after as afterToolkit } from 'es-toolkit';
import { after as afterLodash } from 'lodash';

2번
import { after as afterToolkit_ } from 'es-toolkit';
import { after as afterLodash_ } from 'lodash';

const afterToolkit = afterToolkit_;
const afterLodash = afterLodash_;

 

 

GPT 첫 번째 답변

1. 첫 번째 코드

import { after as afterToolkit } from 'es-toolkit'; 
import { after as afterLodash } from 'lodash';
  • 이 코드는 es-toolkit와 lodash 모듈에서 각각 after라는 함수를 가져와 직접 afterToolkit과 afterLodash에 할당합니다.
  • 결과:
    import 구문은 모듈의 모든 코드를 실행한 뒤, 필요한 부분(after)을 가져옵니다.
    만약 두 모듈(es-toolkit과 lodash)이 크거나 불필요한 부분을 포함하고 있다면 초기 로드 시간메모리 사용량이 증가할 수 있습니다.

2. 두 번째 코드

import { after as afterToolkit_ } from 'es-toolkit'; 
import { after as afterLodash_ } from 'lodash'; 

const afterToolkit = afterToolkit_;
const afterLodash = afterLodash_;
  • 이 코드는 es-toolkit와 lodash에서 after를 각각 afterToolkit_과 afterLodash_라는 이름으로 가져오고, 이후에 변수 할당으로 afterToolkit과 afterLodash를 정의합니다.
  • 결과:
    import 구문은 여전히 모듈을 실행하고 필요한 after를 가져오지만, 변수 할당을 통해 모듈 초기화 과정에서의 직접 참조를 피합니다.

차이가 발생하는 이유

1. 모듈 실행 및 바인딩

  • 첫 번째 코드: afterToolkit과 afterLodash는 직접적으로 모듈에서 가져온 값에 바인딩됩니다. 이런 방식은 모듈 초기화 시점에 더 많은 메타데이터 관리 비용이 발생할 수 있습니다.
  • 두 번째 코드: afterToolkit_과 afterLodash_를 가져온 뒤, 이를 로컬 변수에 다시 할당합니다. 이 과정은 런타임에서 바인딩 메타데이터를 단순화시켜 약간의 오버헤드 감소를 유발할 수 있습니다.

2. V8 엔진 최적화

  • V8 같은 자바스크립트 엔진은 직접 import한 값로컬 변수에 재할당한 값을 다르게 처리합니다.
    • 첫 번째 방식은 모듈 네임스페이스 참조 비용이 더 클 수 있습니다.
    • 두 번째 방식은 로컬 변수 접근이 더 빠릅니다.

결론

두 번째 방식은 재할당을 통해 import 바인딩의 최적화를 유도하며, 모듈 참조 비용을 줄이는 효과가 있습니다. 따라서 초기화 속도와 런타임 접근 성능에서 약간의 이점을 얻을 수 있습니다.

두 번째 질문

 

import 구문은 모듈의 모든 코드를 실행한 뒤, 필요한 부분(after)을 가져옵니다. 이 문장을 더 구체적으로 설명해줘

 

GPT 두 번째 답변

1. 모듈 시스템의 동작 방식

자바스크립트의 **ES 모듈(ESM)**은 다음 단계를 통해 동작합니다:

1.1 모듈 로드 및 파싱 단계

  • import 구문이 실행되면, 자바스크립트 엔진은 해당 모듈 파일(예: es-toolkit 또는 lodash)을 찾습니다.
  • 모듈 파일을 읽어 들이고 전체 코드를 파싱합니다.
  • 필요한 경우 **의존성(다른 import된 모듈)**도 재귀적으로 로드합니다.

1.2 모듈 실행 단계

  • 모듈이 파싱되면 모든 코드가 실행됩니다.
    • 예를 들어, es-toolkit이나 lodash 모듈 안에 함수 정의, 변수 초기화, 객체 생성, 로직 실행 등이 있으면 이들이 모두 실행됩니다.
  • 실행 결과로 모듈의 export 객체가 만들어집니다.
    • 이 객체는 모듈이 제공하는 모든 export된 값(함수, 변수, 클래스 등)을 포함합니다.

1.3 필요한 부분 가져오기

  • import { after } from 'lodash';처럼 부분 import를 사용할 경우, 엔진은 모듈의 export 객체에서 after라는 이름의 값을 찾아 가져옵니다.

2. 실제 동작 예시

2.1 lodash 모듈 구조 (가상 예시)

// lodash/index.js
export const after = (n, func) => { /* 정의 */ };
export const chunk = (array, size) => { /* 정의 */ };
export const cloneDeep = (value) => { /* 정의 */ };

// 추가적인 내부 로직 (모듈 실행 시 실행됨)
console.log('lodash 모듈이 로드되었습니다.');

2.2 import 구문의 동작

import { after } from 'lodash';

위 코드를 실행하면 다음이 발생합니다:

  1. lodash/index.js 파일이 로드되고 전체 코드가 실행됩니다.
    • 여기서 console.log('lodash 모듈이 로드되었습니다.');도 실행됩니다.
    • after, chunk, cloneDeep 함수가 메모리에 정의됩니다.
  2. 실행 결과로 export 객체가 생성됩니다:
{
  after: [Function: after],
  chunk: [Function: chunk],
  cloneDeep: [Function: cloneDeep]
}

   

    3. import { after }는 이 export 객체에서 after를 찾아 가져옵니다.

3. 모듈 실행의 부작용

모든 모듈의 코드를 실행하는 이유는 모듈이 **부작용(side effect)**을 포함할 수 있기 때문입니다.

예시: 부작용이 있는 모듈

// example.js
console.log('Example 모듈 실행 중...');
export const exampleFunction = () => console.log('Hello, world!');

// main.js
import { exampleFunction } from './example.js';
 
  • main.js를 실행하면, example.js의 모든 코드가 실행되기 때문에 다음이 출력됩니다:
    Example 모듈 실행 중...
  • 이후 exampleFunction만 가져옵니다.

4. 요약

  • import 구문은 모듈의 전체 코드를 실행하여 필요한 export 객체를 생성합니다.
  • 필요한 부분만 가져오더라도 모듈 전체 실행은 피할 수 없습니다.
  • 따라서 큰 모듈에서는 불필요한 실행 및 메모리 사용이 성능에 영향을 줄 수 있습니다. 이를 최적화하려면 필요한 부분만 export하거나, 로직을 나눠서 import하는 방법이 있습니다.

 

세 번째 질문

 

두 번째 코드도 결국 import에서 모든 모듈을 실행하는데 import overhead가 줄어드는 이유는 뭐야?

GPT 세 번째 답변

좋은 질문입니다! 두 번째 코드에서 import overhead가 줄어드는 이유는, 모듈의 실행 자체가 아니라, 변수 접근과 바인딩 방식의 차이에 있습니다. 이를 자세히 설명하겠습니다.

1. 두 코드의 비교

첫 번째 코드:

import { after as afterToolkit } from 'es-toolkit';
import { after as afterLodash } from 'lodash';

두 번째 코드:

import { after as afterToolkit_ } from 'es-toolkit';
import { after as afterLodash_ } from 'lodash';

const afterToolkit = afterToolkit_;
const afterLodash = afterLodash_;

두 코드 모두 모듈 전체를 실행하는 것은 동일합니다. 하지만 import된 값을 사용하는 방식에서 차이가 발생하여 성능에 영향을 미칩니다.


2. import 구문과 바인딩

첫 번째 코드의 동작

  • import { after as afterToolkit }는 es-toolkit 모듈에서 after를 가져와 모듈 네임스페이스와 강하게 바인딩합니다.
    • afterToolkit은 항상 모듈 네임스페이스를 통해 값을 참조합니다.
    • 이는 모듈 내부에서 값이 변경될 가능성을 고려한 동작으로, 런타임 시점마다 모듈 네임스페이스를 확인하게 되어 약간의 오버헤드가 발생합니다.

두 번째 코드의 동작

  • import { after as afterToolkit_ }로 가져온 afterToolkit_은 모듈 네임스페이스와 바인딩되지만, 이후:
const afterToolkit = afterToolkit_;

 

로컬 변수 afterToolkit에 재할당됩니다.

  • 로컬 변수는 모듈 네임스페이스와의 연결이 끊어지며, 단순히 메모리 주소만을 참조합니다.
  • 로컬 변수 접근은 더 빠르고 메타데이터 오버헤드가 적습니다.

3. 성능 차이의 원인

3.1 모듈 네임스페이스 접근 비용

  • 모듈에서 값을 직접 import할 경우, 엔진은 모듈 네임스페이스 객체를 통해 값을 참조해야 합니다.
    • 이 객체에는 export된 모든 값과 관련된 메타데이터가 포함되어 있으며, 이로 인해 접근 비용이 조금 더 높아집니다.
    • 특히, 반복적으로 함수나 변수를 호출하거나 참조할 때 이 오버헤드가 누적될 수 있습니다.

3.2 로컬 변수 접근 비용

  • 두 번째 코드에서는 import한 값을 로컬 변수로 재할당했기 때문에, 이후에는 단순히 로컬 스코프의 변수로 접근합니다.
    • 로컬 변수는 모듈 네임스페이스와 독립적으로 작동하며, 접근 속도가 더 빠릅니다.
    • V8 같은 자바스크립트 엔진은 로컬 변수에 대해 더 효율적으로 최적화를 수행할 수 있습니다.

4. 실제 성능 차이

  • 첫 번째 코드는 모듈 네임스페이스를 통해 값을 참조하기 때문에, 런타임 접근 비용이 조금 더 높습니다.
  • 두 번째 코드는 import된 값을 로컬 변수에 재할당하여, 더 빠른 변수 접근을 가능하게 합니다.
  • 이 차이는 모듈 내 큰 함수 호출이나 반복문에서 더욱 두드러지게 나타납니다.

5. 왜 import overhead가 줄어드는가?

결론적으로, 두 번째 코드에서 import overhead가 줄어드는 이유는 다음과 같습니다:

  1. 모듈 네임스페이스 참조 대신 로컬 변수 사용으로 인해 접근 비용이 줄어듭니다.
  2. 엔진 최적화가 더 효율적으로 적용됩니다.
    • 로컬 변수는 스코프와 메모리 구조가 간단하여 최적화가 빠르게 이루어집니다.
  3. 모듈 실행 자체는 동일하지만, 런타임 접근 속도가 개선되어 반복적인 참조에서 성능 향상을 얻을 수 있습니다.

6. 적용 시점

이 방식은 특히 다음과 같은 경우 유용합니다:

  • 모듈에서 가져온 함수나 변수를 반복적으로 사용하는 경우.
  • 성능 최적화가 중요한 대규모 프로젝트에서 미세한 개선이 누적될 때.
댓글