들어가기
Babel이 리액트와 JSX를 어떻게 처리하는지 알아보기 위해 JSX로 간단한 컴포넌트를 트랜스파일링해봤습니다.
// .babelrc.json
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}
React 17부터 리액트 프로젝트는
@babel/preset-react의"runtime": "automatic"옵션을 사용하여 JSX를 처리합니다. 이는 기존의React.createElement()함수를 사용하는 대신,jsx-runtime모듈의jsx함수를 사용하는 것입니다. 참고 자료
// JSX
const element = <h1 className="welcome">Hello!</h1>;
// 트랜스파일 결과
var _jsxRuntime = require("react/jsx-runtime");
var element = /*#__PURE__*/ (0, _jsxRuntime.jsx)("h1", {
className: "welcome",
children: "Hello!",
});
그러던 중, (0, \_jsxRuntime.jsx)()라는 표현이 눈에 들어왔습니다.
(0, obj.prop)()와 obj.prop() 비교하기
우선 (0, fn)()는 언뜻 보기에 fn()과 동일해보입니다.
function fn() {
return "hello";
}
console.log((0, fn)() === fn()); // true
(a, b)는 쉼표 연산자로(Comma Operator), 피연산자를 왼쪽에서 오른쪽 순서로 평가하고, 마지막 연산자의 값을 반환합니다.
자주 사용하진 않지만, 하나의 표현식을 사용해야만 하는 곳에서 여러 표현식을 사용할 때 유용합니다.
(0, fn)()는 0을 평가하고, fn을 호출하게 되니, fn()과 동일한 결과를 얻을 수 있습니다.
하지만 미묘한 차이가 있습니다. fn이 함수가 아닌 메서드이고, this에 접근한다면 어떻게 될까요?
객체의 메서드가 함수로 호출되면 this는 객체를 가리킵니다.
const person = {
name: "kang",
getName() {
return this.name;
},
};
console.log(person.getName()); // kang
하지만 (0, person.getName)()은 this가 window 전역 객체가 되어 undefined를 반환합니다. (브라우저의 경우)
const person = {
name: "kang",
getName() {
return this.name;
},
getThis() {
return this;
},
};
console.log((0, person.getName)()); // undefined
console.log((0, person.getThis)()); // Window
왜 이런 일이 발생하는 걸까요?
References와 This
앞에서 본 현상을 이해하기 위해서 ECMAScript에서 정의한 references를 살펴보고, 이를 함수 호출과 쉼표 연산자는 어떻게 처리하는지 알아야 합니다.
References
Reference Record 타입은 값의 delete, typeof, 할당 연산, the super keyword 등의 동작을 설명하는데 쓰이는 명세 타입입니다. 예를 들어, x = 1 시 x에 Reference Record가 생성됩니다. - ECMAScript 16th edition에서 Reference Record 명세
Reference Record는 4가지 필드를 가집니다.
- Base: 바인딩을 보유하는 값 또는 환경 레코드.
unresolvable이면 바인딩을 찾지 못했다는 의미 - ReferencedName: 바인딩의 이름
- Strict: strict mode에서 생성되었는지 여부
- ThisValue: 비어있지 않으면 super 키워드로 표현된 프로퍼티 바인딩을 나타냄
예시는 다음과 같습니다.
// 변수 x에 접근
{ [[Base]]: 환경 레코드, [[ReferencedName]]: "x", [[Strict]]: false, [[ThisValue]]: empty }
// 프로퍼티 접근: obj.foo
{ [[Base]]: obj, [[ReferencedName]]: "foo", [[Strict]]: false, [[ThisValue]]: empty }
이런 Reference Record를 다루는 연산이 몇 가지 있는데요, 그 중 GetValue와 GetThisValue만 알아봅시다.
GetValue(V)
Reference Record에서 실제 값을 추출하는 연산입니다.
- V가 Reference Record가 아니면 그냥 V 반환
- Base가
unresolvable이면 ReferenceError (존재하지 않는 변수) - 프로퍼티 참조면
base.[[Get]](이름, this)호출 - 환경 레코드면
base.GetBindingValue(이름, strict)호출
GetThisValue(V)
프로퍼티 접근에서 this로 사용할 값을 결정합니다.
- Super Reference면 →
V.[[ThisValue]]반환 - 그 외 →
V.[[Base]]반환
fn()과 (0, fn)()의 Reference 접근 차이
이제 Reference Record를 이해했으니, 각 방식으로 함수 호출 시 this가 어떻게 결정되는지 살펴봅시다.
fn()과 this
함수 호출 표현식은 GetThisValue를 호출합니다.
obj.method(); // this = obj
method(); // this = undefined (strict) 또는 globalThis
이 차이는 EvaluateCall 연산에서 결정됩니다. 함수 호출 표현식을 평가할 때, 엔진은 Reference Record를 EvaluateCall에 전달하고, 여기서 this 값이 결정됩니다.
EvaluateCall(func, ref, arguments, tailPosition)
-
ref가 Reference Record인 경우:
a. IsPropertyReference(ref)가 true면 (프로퍼티 접근):
→ thisValue = GetThisValue(ref)
b. 그 외 (환경 레코드에서 찾은 변수):
→ thisValue = undefined -
ref가 Reference Record가 아닌 경우:
→ thisValue = undefined
쉼표 연산자는?
쉼표연산자는 GetValue를 호출합니다.
쉼표 연산자의 명세를 보면 다음과 같습니다.
- 왼쪽 표현식 평가 → GetValue 호출 (결과는 버려짐)
- 오른쪽 표현식 평가 → GetValue 호출 → 이 값을 반환
핵심은 **GetValue의 결과(순수 값)**를 반환한다는 것입니다. Reference Record가 아닌 값이 반환되므로, 이후 함수 호출 시 EvaluateCall에서 this를 결정할 수 없게 됩니다.
정리
차이점을 정리하면 다음과 같습니다.
- 함수 호출: ref(Reference Record)를 EvaluateCall에 전달 → GetThisValue()로 this 결정 가능
- 쉼표 연산자: GetValue() 결과(순수 값)를 반환 → Reference Record 소실
결론: obj.prop() 대신 (0, obj.prop)()를 사용한 이유
이러한 동작 차이를 살펴봤으니, 도입부에서 본 Babel의 트랜스파일링 결과가 obj.prop() 대신 (0, obj.prop)()를 사용한 이유를 알 수 있습니다.
바로 this 바인딩을 제거하기 위한 패턴입니다.
jsx 함수는 this에 의존하지 않는 순수 함수로 설계되었습니다. (0, _jsxRuntime.jsx)() 패턴을 사용하면:
- this 바인딩이 명시적으로 제거되어 함수가 순수하게 실행됨
- 모듈 시스템과의 일관성 유지 (ES 모듈에서 import한 함수도 this가 undefined)
- 의도하지 않은 this 참조 방지 (실수로 this를 사용해도 _jsxRuntime 객체가 아닌 undefined가 됨)
비슷한 패턴으로 Reference Record가 소실되는 경우들이 있습니다.
(obj.method = obj.method)()- 할당 연산자(true && obj.method)()- 논리 AND 연산자(false || obj.method)()- 논리 OR 연산자
이들 모두 내부적으로 GetValue를 호출하여 Reference Record가 소실됩니다. 하지만 (0, obj.prop)() 패턴이 “this 바인딩을 의도적으로 제거한다”는 의도를 가장 명확하게 드러내기 때문에 관용적으로 사용됩니다.
Babel의 React 트랜스파일 결과를 살펴보다가, ECMAScript 명세까지 들여다보게 되었네요. 사소해 보이는 (0, fn)() 패턴 하나에도 언어의 설계 원리가 담겨 있다는 점이 흥미로웠습니다.