본문 바로가기

호기심 천국

컴파일러는 JSX를 React.createElement() 로 바꿀까?

선요약

JSX는 일반적인 경우 컴파일 과정에서 React.createElement()로 변환되지 않는다. 리액트 17부터는 JSX를 컴파일할 수 있는 새로운 방법을 제공하여 React를 import하지 않고도 컴파일러 쪽에서 알아서 처리가 가능하다.

 

JSXReact.createElement() 가 될?

구 리액트 공식 문서를 보면 이런 내용이 있다.

Babel은 JSX를 React.createElement() 호출로 변경한다.

친절하게 예시도 들어준다.

구 리액트 공식 문서의 내용 일부

하지만 한 쪽의 말만 듣고 판단할 수는 없는 법. 바벨에게도 물어보자.

바벨 공식 홈페이지의 Try It Out에서 실험한 결과

?

날 속인 거니? / Unsplash의 Håkon Grimstad님 사진

 

구 리액트 공식 문서는 더 이상 업데이트하지 않는다더니. 결국 이런 대참사가 일어나고 만다. 그러면 언제부터 이렇게 되어버린 걸까?

 

2019, 깃허브 reactjs/rfc 저장소

때는 바야흐로 2019. 어떤 사람이 이런 제안을 한다.

기존의 React.createElement를 이용한 JSX 변환은 구리다. 특히 함수형 컴포넌트와 훅이 등장하고 그 구린 정도가 더 부각되었다. 우리는 JSX의 변환 방식을 바꾸고, 버릴 건 버려야 한다.

해당 글은 RFC(Request For Comments)로 일종의 제안이다. 이 제안은 받아들여졌고 실제로 JSX 변환을 위한 jsx() 함수와 관련 코드를 새로 만들게 된다. 이 함수는 바벨과 같은 컴파일러에 적용되었고, 그렇게 React 17 부터는 새로운 JSX 변환 방식을 사용하게 되었다. 위 바벨 홈페이지 사진에서 볼 수 있는 것처럼 jsx()라는 새로운 함수를 이용해서.

 

React.createElement() 가 왜 구린데?

제안에서는 React.createElement()를 이용한 JSX 변환이 갖는 총 여덟 가지의 문제를 제시하였다. 이 글에서는 그 문제가 무엇인지, 그리고 실제로 어떤 방식으로 해결하였는지를 간략하게 다룰 예정이다. “문제 상황자체는 사실이다. 하지만 해결책의 경우에는 내가 슬쩍 찾아본 것을 바탕으로 하는 주관적인 내용이 많기 때문에 일단 의심하길 추천한다.

 

이 제안에 대해 자세하게 분석해 주신 분이 있다. 마지막에도 링크를 남기겠지만 더 궁금한 분들은 링크를 참고하는 걸 추천한다.

그 많던 import React from ‘react’는 어디로 갔을까

1. element creation call 때마다 defaultProps가 있는지 동적으로 확인해야 함

defaultProps는 천변만화(megamorphic)의 특성을 갖고 있다. 그래서 뭐가 바뀌었는지 항상 확인해야만 한다.

 

2. Creation call defaultProps를 확인한다 하더라도 React.lazy 를 쓰면 또 문제가 생김

정확한 이유는 잘 모르겠다. 내 추측으로는 lazy component create하는 곳에서는 처음에 promise pending 비스무리한 상태 느낌으로 defaultProps를 받아올 수 없는 상태이지 않을까 싶다.

해결

위의 두 문제는 function component에서의 defaultPropsdeprecate하여 해결할 예정이다.

관련 PR

 

3. children을 가변 인자(var args)로 전달함

React.createElement(타입, props, 자식1, 자식2, 자식3, ...) 는 이런 식으로 사용하니까 이 함수의 callsite에서는 정확히 본인의 인자가 몇 개인지, 어떤 모양인지 알 수 없다.

해결

jsx 함수를 보자. 실제 코드 보러가기

function jsx(type, config, maybeKey)

이런 형태로 되어 있다. childrenconfigkey 중 하나로 넘길 수밖에 없게 유도하였다. 만약 children이 여러 개라면 배열과 같은 형태로 나타내야 하므로 callsite에서도 children 배열의 length를 알 수 있다.

 

4. Dynamic property look up을 사용함

Dynamic property lookup이 뭔지 몰라서 찾아봤다.

// Static Property Lookup
const staticLookup = sparrow.color;

// Dynamic Property Lookup
const key = 'color';
const dynamicLookup = sparrow[key];

만약 sparrow라는 이름의 object가 있다고 치자. const color = sparrow.color 이거는 컴파일 시 올바른 property name인지 확인할 수 있다. 하지만 const key=’color’; const color=sparrow[key]; 는 컴파일 당시에는 검사가 안 된다. 런타임에 ‘color’라는 문자열이 확정되면서 그제서야 값을 찾아올 수 있게 된다. 후자의 방식이 dynamic property lookup이다.

해결?

RFC 글에서는 React.createElement의 정확히 어디에서 dynamic property lookup을 사용하는지 알려주지 않았다. 내 추측은 두 개다.

 

1. element props는 객체 형태로 표현한다. props 안쪽의 key-value쌍에 뭐가 있는지 알 수 없어서 dynamic property lookup을 사용한다.

 

만약 이걸 말한 거라면 해결되지 않았다. 애초에 object 안에 어떤 key가 있는지 미리 알 수 있는 방법이 없을 텐데 해결할 방법이 있기나 할까?

 

2. children을 하나씩 argument로 넘겨주기 때문에 children이 있는지 없는지 찾기 위해서 Function.prototype.arguments 에 동적으로 접근해야 한다.

 

만약 이걸 말한 거라면 앞의 3번에서 해결한 문제다.

 

5. 받은 props의 불변성을 몰라서-변형될 수도 있어서- 복사해야 함

6. props 안에 전달된 key ref props 객체 밖으로 제거, 추출해야 함

결국 props 객체 안에서 'key'라는 key를 제거하는 로직이 필요한데 props의 불변성을 내가 깨는 셈이다.

해결?

이건 해결했는지 모르겠다. 내가 찾아본 바로는 createElement()jsx() 모두 props를 복사해서 쓴다.

 

7. <div {...props} /> 이런식으로 사용하면 안에 key ref가 있을 수도 있고 없을 수도 있어서 확인하는데 비싼 비용을 지불함

해결?

일단 jsx()에서는 인자로 받은 mayBeKey를 이용해서 키를 따로 줄 수 있게 하긴 했는데 spread를 추가적으로 한 번 더 해서 찾아본다. 아마 구버전과의 호환을 위함이 아닐까?

 

8. react 관련 로직은 하나도 안쓰고 JSX만 쓰는 코드를 짜더라도 import React from "react" 해줘야 함

해결

Babel과 같은 컴파일러가 컴파일 시에 자동적으로 import를 추가해준다. 위쪽 바벨 예시 사진을 보면 알아서 추가되어 있는 걸 볼 수 있다. 우리가 JSX를 쓸 때 import react from “react”를 안 해도 되는 이유다.

 

해결되지 않은 문제

JSX에 적힌 key값을 분리하는 데 다음과 같은 문제가 있다.

However, in the interim, we need a way to distinguish between <div {...props} key={foo} /> and <div key={foo} {...props} />. Therefore, until we completely deprecate key spreading, we will use React.createElement to transform <div {...props} key="Hi" /> and React.jsx to transform everything else.

말인즉슨, propsspread 했을 때 그 안에 key가 이미 들어 있을 경우 우리가 손수 적어준 key와 구분할 방법이 현재로서는 없다는 것이다. jsx() 함수를 도입한 이유 중 하나가 이 key값을 미리 추출해서 함수의 인자로 따로 넣어 주기 위함인데 그게 힘들다. 왜 구분하기 힘들까? 그건 모른다. 컴파일러 파싱 과정에서 JSX 코드에 대한 추상 구문 트리(Abstract Syntax Tree)가 어떻게 생겼는지를 봐야 할 것 같은데 거기까지 들어가기에는 내 실력이 모자라다.

 

현재 바벨에서는 key{…props} 앞에 있다면 jsx() 함수를 사용하고, 뒤에 있다면 createElement()를 사용한다.

key의 위치에 따른 Babel의 컴파일 결과

만약 key를 넣어야 한다면 앞에 넣어주는 게 성능적인 측면에서는 유리하겠다는 생각이 든다.

 

마치며

사실 이 글의 내용은 맨 첫 단락이 전부다. 나머지는 바뀐 이유에 대한 이야기이니 첫 단락만 기억해도 되지 않을까?

 

읽어볼 만한 글들

https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#whats-a-jsx-transform

 

Introducing the New JSX Transform – React Blog

This blog site has been archived. Go to react.dev/blog to see the recent posts. Although React 17 doesn’t contain new features, it will provide support for a new version of the JSX transform. In this post, we will describe what it is and how to try it. W

legacy.reactjs.org

https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md

 

GitHub - reactjs/rfcs: RFCs for changes to React

RFCs for changes to React. Contribute to reactjs/rfcs development by creating an account on GitHub.

github.com

https://so-so.dev/react/import-react-from-react/

 

그 많던 import React from ‘react’는 어디로 갔을까

이 글에서는 React 17릴리즈에 포함되었던 JSX Transform의 RFC문서를 살펴보며 이 변경사항이 어떤 의미를 가지고 있는지 정리해 보았습니다. 중간중간 내용에 대한 첨언과 약간의 추측을 덧붙였습니

so-so.dev