createElement는 React Legacy API에 포함되어 있으므로 사용을 권장하지 않습니다.
얼마 전 리액트에서 createElement를 사용하여 리팩토링 하는 글을 봤다.
리액트의 기본 매커니즘을 활용한 방식이라 매력적으로 느껴졌다.
createElement
createElement를 사용하면 React Element를 생성할 수 있으며 이는 JSX를 작성하는 대신 사용할 수 있다.
const element = createElement(type, props, ...children)
React Element는 HTML Element와 달리 실제 DOM을 구성하는 요소는 아니다.
리액트 내부에서 사용되는 Element라 볼 수 있다.
이 Element을 사용해 실제 DOM을 구성하기 위해서는 별도의 라이브러리가 필요하다.
JSX
리액트 컴포넌트 파일의 확장자는 .jsx 또는 .tsx를 사용한다.
JSX는 얼핏 보면 JavaScript와 유사해 보이기도 하지만 HTML과 닮은 것 같기도 하다.
리액트 공식문서에서는 JavaScript를 확장한 문법으로 JavaScript 파일을 HTML 비슷하게 마크업을 작성할 수 있도록 해준다고 한다.
컴포넌트를 작성하는 다른 방법도 있지만, 대부분의 React 개발자는 JSX의 간결함을 선호하며 대부분의 코드 베이스에서 JSX를 사용한다고 한다.
여기서 말하는 "컴포넌트를 작성하는 다른 방법"이 createElement를 활용한 방법일 것이다.
또한, 대부분의 개발자가 JSX의 간결함을 선호한다고 했는데 아래의 코드를 보자.
import { createElement } from 'react';
function Test() {
return (
<div>
<h1>
<h2>
<h3>
<div />
</h3>
</h2>
</h1>
</div>
);
}
function Test() {
return createElement('div', null,
createElement('h1', null,
createElement('h2', null,
createElement('h3', null,
createElement('div', null)
)
)
)
);
}
JSX 문법을 사용하는 방법이 더 직관적인 것을 볼 수 있다.
이미 알고 있는 HTML의 방식을 그대로 사용할 수 있기 때문이다.
트랜스파일링
브라우저는 JSX 문법을 이해하지 못한다.
브라우저는 JS를 이해할 수 있으며 JSX를 브라우저에서 사용하기 위해 JS로 변환해야 한다.
이 과정을 트랜스파일링이라 한다.
컴파일과 유사하지만 컴파일은 특정 코드를 다른 언어로 바꾸는 방식이라면 트랜스파일링은 코드를 비슷한 수준의 추상화 수준을 갖는 언어로 변경하는 것이다.
컴파일을 C언어를 어셈블리어로 변환하거나, Java를 컴파일하여 바이트 코드로 변환하는 과정 등이 있다.
이와 달리 트랜스파일은 서로 다른 버전의 JavaScript 코드로 작성된 경우 호환성을 위해 낮은 버전의 JavaScript 코드로 변환하거나 TypeScript를 JavaScript로 변환하는 과정 등이 있다.
JSX의 경우 JS로 변환되는 트랜스파일링 과정이 필요한 것이다.
트랜스파일링 with createElement
(JS를 대상으로) 트랜스파일링을 수행하는 트랜스파일러는 Babel, tsc, ESBuild 등이 있다.
트랜스파일러가 JSX를 JS로 변환하는 과정에서 JSX로 표현된 Element들이 createElement로 대체된다.
결국 JSX로 작성하더라도 트랜스파일링 과정 이후에는 createElement로 변환된다는 것이다.
다양한 트랜스파일러에서 다양한 속성을 가진 트리 구조를 토큰화해 ECMAScript로 변환하는 것이 목표다.
결론적으로 JSX 내부에 트리 구조로 표현하고 싶은 다양한 것을 작성해두고 JSX 트랜스파일을 거쳐 JS(ECMAScript)가 이해할 수 있는 코드로 변환하게 된다.
createElement를 사용한 리팩토링
결론은 리액트에서 JSX 요소들을 트랜스파일링 한 결과는 결국 React.createElement가 되기 때문에 이를 응용할 수 있다.
다음과 같은 조건이 있으며 이를 해결하기 위해 코드를 작성했다고 가정해보자.
1. 로그인 버튼을 클릭했을 때, 요청이 여러번 전송되는 것을 방지하기 위해 버튼이 비활성화 되어야 함
2. 악의적인 사용자가 많으며 disabled 속성의 경우 네트워크 느린 3G를 걸고 개발자 도구를 이용해 disabled를 지울 수 있음
그렇다면 아래와 같이 작성할 수 있을 것이다.
interface LoginButtonProps {
isLoading: boolean;
}
export default function LoginButton({ isLoading }: LoginButtonProps) {
return isLoading ? (
<span className="col-span-1 flex h-full w-full items-center justify-center rounded-md bg-base-200 mobile:ml-0 mobile:mt-2 mobile:h-10">
Loading...
</span>
) : (
<button
className="col-span-1 flex h-full w-full items-center justify-center rounded-md bg-base-200 mobile:ml-0 mobile:mt-2 mobile:h-10"
type="submit"
>
로그인
</button>
);
}
여기서 createElement를 활용한다면 아래와 같이 작성할 수 있다.
import { createElement } from 'react';
interface LoginButtonProps {
isLoading: boolean;
}
export default function LoginButton({ isLoading }: LoginButtonProps) {
return createElement(
isLoading ? 'span' : 'button',
{
className: 'col-span-1 flex h-full w-full items-center justify-center rounded-md bg-base-200 mobile:ml-0 mobile:mt-2 mobile:h-10',
type: isLoading ? undefined : 'submit',
},
isLoading ? 'Loading...' : '로그인',
);
}
props에 따라 어떤 태그이름을 가질지 정할 수 있다.
추가적인 속성을 정의할 수 있고 children도 props에 따라 정할 수 있다.
- 첫 번째 인자: type (태그 이름)
- 두 번째 인자: props
- 세 번째 인자: children
But...
위와 같이 작성한다면 적은양의 코드와 조건부 렌더링 없이 (조건에 따른 분기는 하지만 조건부 렌더링에 비해 덜 복잡하다) 요소를 만들 수 있어 보인다.
그렇지만 위의 방법은 권장되지 않는다.
가장 큰 이유는 리액트 팀에서 createElement를 legacy로 분류했기 때문이다.
결론은 다음과 같다.
- 불필요한 import
- 클래스 컴포넌트에서는 createElement가 유효했지만 함수형 컴포넌트에서는 의미를 잃음
- createElement는 JSX를 염두에 두지 않았음
- 성능
- 불필요한 개념을 포함
(createElement와 해당 기능이 legacy로 분류된 이유에 관한 더 자세한 내용은 아래를 참고바랍니다.)
React - createElement 알아보기 (velog.io)
React 17 버전부터는 트랜스파일링 결과는 createElement 대신 jsx로 변환된다고 한다.
이는 트랜스파일러에 의해 import가 자동으로 추가된다는 것과 약간의 문법 차이가 있다.
function Test() {
return <h1 className="class">test</h1>
}
import { jsx as _jsx } from "react/jsx-runtime";
function Test() {
return _jsx("h1", {
className: 'class',
children: 'test'
})
}
jsx를 사용한 리팩토링
import { jsx as _jsx } from 'react/jsx-runtime';
interface LoginButtonProps {
isLoading: boolean;
}
export default function LoginButton({ isLoading }: LoginButtonProps) {
return _jsx(isLoading ? 'span' : 'button', {
className: 'col-span-1 flex h-full w-full items-center justify-center rounded-md bg-base-200 mobile:ml-0 mobile:mt-2 mobile:h-10',
type: isLoading ? undefined : 'submit',
children: isLoading ? '로딩중' : '로그인',
});
}
jsx를 활용한다면 이와 같이 작성할 수 있을 것이다.
하지만 이 함수를 직접적으로 사용하지 않고 JSX와 트랜스파일러를 이용하라고 한다.
리액트 팀에서 createElement를 대체하기 위해 _jsx를 추가했지만 이것은 트랜스파일링의 결과로 사용하기 위한 것이기 때문에 개발자가 직접 사용할 일이 없도록 하는 것 같다.
결론
리액트에서는 기존에 createElement를 사용하여 React Element를 만들었다.
createElement는 여러 이유로 legacy로 분류 되었으며 이를 대체하기 위해 jsx라는 것이 추가되었다.
하지만, 두 방법 모두 권장되지는 않는다.
createElement의 경우는 legacy기 때문이며, jsx의 경우 직접 사용하지 않고 JSX와 트랜스파일러가 사용할 수 있도록 해야 하기 때문이다.
'개발 > 개발과정' 카테고리의 다른 글
[import-visualizer] 2. 경로별칭 (0) | 2024.06.13 |
---|---|
[import-visualizer] 1. 프로젝트 시작 (요구사항 분석) (0) | 2024.06.13 |
[AlgoITNi] 홈 화면 성능 개선하기: 7. 최종 결과 (0) | 2024.04.02 |
[AlgoITNi] 홈 화면 성능 개선하기: 6. 코드 스플리팅 (3) (1) | 2024.02.28 |
[AlgoITNi] 홈 화면 성능 개선하기: 5. 코드 스플리팅 (2) (0) | 2024.01.27 |