선요약
JSX는 일반적인 경우 컴파일 과정에서 React.createElement()로 변환되지 않는다. 리액트 17부터는 JSX를 컴파일할 수 있는 새로운 방법을 제공하여 React를 import하지 않고도 컴파일러 쪽에서 알아서 처리가 가능하다.
JSX는 React.createElement() 가 될까?
구 리액트 공식 문서를 보면 이런 내용이 있다.
Babel은 JSX를 React.createElement() 호출로 변경한다.
친절하게 예시도 들어준다.
하지만 한 쪽의 말만 듣고 판단할 수는 없는 법. 바벨에게도 물어보자.
?
구 리액트 공식 문서는 더 이상 업데이트하지 않는다더니. 결국 이런 대참사가 일어나고 만다. 그러면 언제부터 이렇게 되어버린 걸까?
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에서의 defaultProps를 deprecate하여 해결할 예정이다.
3. children을 가변 인자(var args)로 전달함
React.createElement(타입, props, 자식1, 자식2, 자식3, ...) 는 이런 식으로 사용하니까 이 함수의 callsite에서는 정확히 본인의 인자가 몇 개인지, 어떤 모양인지 알 수 없다.
해결
jsx 함수를 보자. 실제 코드 보러가기
function jsx(type, config, maybeKey)
이런 형태로 되어 있다. children은 config의 key 중 하나로 넘길 수밖에 없게 유도하였다. 만약 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.
말인즉슨, props를 spread 했을 때 그 안에 key가 이미 들어 있을 경우 우리가 손수 적어준 key와 구분할 방법이 현재로서는 없다는 것이다. jsx() 함수를 도입한 이유 중 하나가 이 key값을 미리 추출해서 함수의 인자로 따로 넣어 주기 위함인데 그게 힘들다. 왜 구분하기 힘들까? 그건 모른다. 컴파일러 파싱 과정에서 JSX 코드에 대한 추상 구문 트리(Abstract Syntax Tree)가 어떻게 생겼는지를 봐야 할 것 같은데 거기까지 들어가기에는 내 실력이 모자라다.
현재 바벨에서는 key가 {…props} 앞에 있다면 jsx() 함수를 사용하고, 뒤에 있다면 createElement()를 사용한다.
만약 key를 넣어야 한다면 앞에 넣어주는 게 성능적인 측면에서는 유리하겠다는 생각이 든다.
마치며
사실 이 글의 내용은 맨 첫 단락이 전부다. 나머지는 바뀐 이유에 대한 이야기이니 첫 단락만 기억해도 되지 않을까?
읽어볼 만한 글들
https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md
https://so-so.dev/react/import-react-from-react/
'호기심 천국' 카테고리의 다른 글
React.ReactNode vs JSX.Element (1) | 2024.02.09 |
---|---|
리액트 서버 컴포넌트 vs 서버 사이드 렌더링 (0) | 2024.01.27 |
타입스크립트는 자바스크립트의 상위집합(superset)일까? (4) | 2023.09.30 |
React) useState는 클로저로 동작할까? (4) | 2023.09.11 |
React) 아무 함수나 커스텀 훅이 될 수 있을까? (0) | 2023.05.07 |