FEInterview Prep

Velog

자바스크립트 스코프 호이스팅은 망가졌습니다

번들러의 스코프 호이스팅은 코드 스플리팅과 만나면 모듈 실행 순서·this 바인딩이 깨진다. Parcel v3가 "기본 함수 래퍼" 로 회귀를 검토 중인 이유.

2025-08-05·8분 읽기
JavaScript빌드/도구
원문 보기 ↗

핵심 요약

스코프 호이스팅은 모듈을 함수로 감싸는 대신 단일 스코프에 인라인해 번들 크기·런타임 비용을 줄이는 최적화로 Rollup 이 대중화했다. 한 번들에서는 잘 동작하지만 코드 스플리팅을 쓰면 (1) 공유 모듈 부작용 실행 순서가 원본과 달라지고, (2) import * as foo from './foo'; foo.bar()this 바인딩이 객체 → undefined 로 바뀐다. 해결은 "공유 모듈을 함수로 감싸 호출 순서를 제어"하는 것이며, 실제 앱에서는 결국 거의 모든 모듈이 함수 래퍼를 갖게 되어 호이스팅 이점이 사라진다. webpack 의 ModuleConcatenationPlugin 은 부분 호이스팅으로 절충하지만 자주 bailout 한다. Parcel v3는 "기본 함수 래퍼" 로 정확성을 복원하는 방향을 검토 중.

스코프 호이스팅은 "각 모듈을 함수로 감싸지 말고 한 스코프에 인라인하자"는 최적화다. 번들 1개일 때는 깔끔하지만, 코드 스플리팅으로 번들이 여러 개가 되면 모듈 간 실행 순서·부작용·this 바인딩이 보장되지 않는다. 즉 호이스팅은 "스플리팅과 근본적으로 충돌"하는 최적화이며, 이점은 줄고 비용은 늘었다.

면접에서 "트리 셰이킹은 어떻게 동작하나요?"·"webpack 의 module concatenation 을 아세요?" 같은 질문이 들어왔을 때 "같은 스코프로 합친다" 정도로는 부족하다. 언제 그게 깨지는가 를 코드로 증명할 수 있어야 한다. 이 글은 4가지 번들러(Rollup/ESBuild/Rolldown/Parcel)에서 동일 사례가 어떻게 다르게 깨지는지 보여준다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

스코프 호이스팅의 본질

두 가지 방식 비교:

// 함수 래퍼 방식 (전통적)
let modules = {
  'index.js': (require, exports) => {
    let { add } = require('math.js');
    console.log(add(2, 3));
  },
  'math.js': (require, exports) => {
    exports.add = (a, b) => a + b;
  },
};

// 스코프 호이스팅 (Rollup 이후)
function add(a, b) { return a + b; }
console.log(add(2, 3));

호이스팅은 함수 래퍼·require/exports 오버헤드를 없애지만, 대신 모듈 경계를 잃는다.

scope hoistingmodule wrapperRollup
자주 하는 오해

"호이스팅 = 무조건 좋다"는 도식. 단일 번들 가정에서만 깔끔하다.

코드 스플리팅과 충돌 (1) — 부작용 실행 순서

원래 entry-a 실행 순서가 shared1 → a1 → shared2 → a2 인데, 공유 번들로 추출 + 호이스팅 후엔 shared1 → shared2 → a1 → a2 가 된다. 부작용을 가진 모듈에서 "수입 순서대로 부작용이 실행된다"는 자바스크립트 모듈의 약속이 깨지는 셈.

module side effectsimport ordershared bundle
자주 하는 오해

"순서가 바뀌어도 결과는 같다"고 가정. 폴리필·전역 패치·로깅 등 순서 의존 코드에서 즉시 깨진다.

코드 스플리팅과 충돌 (2) — `this` 바인딩

// 원본
import * as foo from './foo';
foo.bar(); // this === foo (모듈 객체)

// 호이스팅 후
bar(); // this === undefined (strict)

네임스페이스 객체를 거치지 않고 직접 호출되어 this 가 사라진다. re-export 가 섞이면 더 복잡 — this 는 "함수가 선언된 모듈"이 아니라 "가져온 모듈"이어야 한다.

this bindingnamespace importre-export
자주 하는 오해

내부 라이브러리에서 this 의존하지 않으니 안전하다고 가정. 외부 의존성에서 깨지는 경우가 더 많다.

해결책 — 함수 래퍼 또는 부분 호이스팅

공유 모듈만 함수로 감싸 호출 순서를 명시화:

// shared.bundle.js
export default {
  shared1: () => console.log('shared1'),
  shared2: () => console.log('shared2'),
};

// entry-a.bundle.js
import modules from 'shared.bundle.js';
modules.shared1();
console.log('a1');
modules.shared2();
console.log('a2');

이게 본질적으로 Parcel 의 동작 방식이며, webpack 의 ModuleConcatenationPlugin 은 "같은 번들 안에서만" 부분 호이스팅을 시도한다.

ModuleConcatenationPluginbailoutfunction wrapper
자주 하는 오해

수동으로 import 순서를 맞춰 회피하기. 다음 빌드/번들러 버전에서 다시 깨진다.

트리 셰이킹·런타임 비용 재평가

호이스팅은 트리 셰이킹 의 전제조건이라는 통념도 정확하지 않다. 번들러가 자체 정보를 가지면 함수로 감싸진 모듈에서도 트리 셰이킹이 가능. 또한 "함수 래퍼의 런타임 비용" 은 2016년 Nolan Lawson 측정 이후 V8 발전으로 격차가 줄었다 — 게다가 lazy 평가가 "한 번에 평가"보다 빠를 수도 있다.

tree shakinglazy evaluationV8 engine
자주 하는 오해

"호이스팅 없이는 트리 셰이킹 안 된다"는 옛 정설을 그대로 인용.

읽는 순서

  1. 1이론

    ESM 의 정적 import/export, 함수 래퍼 vs 인라인 스코프, 트리 셰이킹의 조건을 그림으로 정리.

  2. 2구현

    글의 예제(a1/a2/b1/b2/shared1/shared2)를 Rollup·ESBuild·Rolldown·Parcel 에 모두 넣어 출력 차이를 직접 캡처.

  3. 3실무

    사내 코드의 "순서 의존 import"(폴리필·CSS-in-JS 초기화·전역 모킹) 을 점검해 어떤 번들러에서 깨질 수 있는지 매트릭스 작성.

  4. 4설명

    "왜 Parcel v3 가 기본 호이스팅을 제거하려 하는가"를 동료에게 5분 안에 설명할 수 있게 정리.

면접 연결 질문

hard스코프 호이스팅과 트리 셰이킹의 관계를 설명해주세요.
힌트

[감점 답변] "같은 거다". [좋은 답변] 다른 최적화다. 트리 셰이킹은 "안 쓰는 export 제거"(빌드 타임 정적 분석), 호이스팅은 "모듈을 같은 스코프로 인라인"(런타임/번들 크기 최적화). 호이스팅이 트리 셰이킹을 더 쉽게 만들어 준 건 사실이지만 필수 조건은 아니다 — 번들러가 정보를 직접 수집하면 함수 래퍼 모듈에서도 가능.

hard코드 스플리팅을 켰을 때 호이스팅이 유발할 수 있는 버그 두 가지를 코드로 답해주세요.
힌트

[감점 답변] "순서가 바뀐다" 한 줄. [좋은 답변] (1) 부작용 순서 변경shared1 → a1 → shared2 → a2shared1 → shared2 → a1 → a2 가 된다(폴리필·로깅 깨짐). (2) this 바인딩 손실import * as foofoo.bar() 가 그냥 bar() 가 되어 this === undefined. 두 사례를 코드 스니펫으로 즉시 보일 수 있어야 한다.

mediumwebpack 의 `ModuleConcatenationPlugin` 과 Parcel 의 함수 래퍼 방식을 비교해주세요.
힌트

[감점 답변] "webpack 방식이 더 빠르다". [좋은 답변] webpack 은 같은 번들 내 모듈을 부분 호이스팅하다가 부작용·동적 import·side effects 등이 보이면 bailout. Parcel 은 기본 함수 래퍼로 정확성을 우선하고 가능한 곳만 호이스팅. "속도 vs 정확성"의 다른 디폴트라는 관점이 답.

자기 점검

왜 호이스팅이 "코드 스플리팅과 근본적으로 충돌"하는지 한 줄로 답해보세요.
부작용 순서공유 번들this 바인딩모듈 경계
자주 하는 오해

"순서만 좀 바뀌는 것"이라고 사소화. 실제로는 폴리필·전역 패치 같은 부작용 의존 코드가 깨진다.

`package.json` 의 `"sideEffects": false` 가 의미하는 바와 위험을 설명해보세요.
부작용 없음 선언트리 셰이킹 허용잘못 선언 시 누락
자주 하는 오해

"무조건 켜면 좋다"고 가정. 실제로는 폴리필 import 같은 모듈을 잘못 빼버려 런타임 에러를 만든다.